@intentius/chant 0.45.0 → 0.49.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/core.d.ts +21 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +3 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +14 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.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/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- 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/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +10 -0
- package/dist/codegen/validate.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/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 +8 -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/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/config.d.ts +54 -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/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +15 -2
- 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.d.ts +223 -2
- 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/index.d.ts +3 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- 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/teardown.d.ts +132 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- 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/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 +12 -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/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.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 +4 -1
- package/dist/lint/rules/index.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/builders.d.ts +134 -8
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -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/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/types.d.ts +31 -1
- package/dist/op/types.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 +11 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +9 -1
- 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 +11 -2
- package/src/audit/rules-doc.ts +11 -1
- package/src/build.test.ts +41 -0
- package/src/build.ts +34 -6
- 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/audit.test.ts +53 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.test.ts +211 -0
- package/src/cli/commands/build.ts +126 -8
- package/src/cli/commands/check-lexicon.test.ts +45 -1
- package/src/cli/commands/check-lexicon.ts +45 -0
- package/src/cli/commands/lexicon-surface-diff.ts +9 -0
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +32 -7
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.ts +4 -4
- package/src/cli/handlers/lifecycle.test.ts +345 -1
- package/src/cli/handlers/lifecycle.ts +300 -11
- package/src/cli/handlers/search.ts +5 -2
- package/src/cli/main.ts +12 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +58 -1
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +7 -0
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/lexicon-regen.ts +19 -1
- package/src/codegen/validate.test.ts +33 -0
- package/src/codegen/validate.ts +21 -2
- package/src/components/auto-release.ts +6 -0
- 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 +8 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/config.test.ts +55 -0
- package/src/config.ts +88 -1
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -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/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/fold/subset.test.ts +26 -0
- package/src/fold/subset.ts +45 -19
- package/src/index.ts +4 -0
- package/src/lexicon.ts +227 -2
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/index.ts +3 -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/teardown.test.ts +568 -0
- package/src/lifecycle/teardown.ts +359 -0
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/engine.ts +7 -1
- 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/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.ts +15 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- 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 +27 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +8 -1
- package/src/lint/rules/index.ts +10 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/builders.ts +179 -8
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/index.ts +10 -3
- package/src/op/local-executor.ts +172 -25
- package/src/op/op.test.ts +25 -2
- package/src/op/receipt-store.ts +211 -0
- package/src/op/types.ts +33 -1
- 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 +12 -0
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import { cor024ReceiptSecretPointerRule } from "./cor024-receipt-secret-pointer";
|
|
4
|
+
import type { LintContext } from "../rule";
|
|
5
|
+
|
|
6
|
+
function createContext(code: string, filePath = "test.ts"): LintContext {
|
|
7
|
+
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
|
|
8
|
+
return { sourceFile, entities: [], filePath, lexicon: undefined };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("COR024: receipt inputs reference secrets by pointer (#1833)", () => {
|
|
12
|
+
test("rule metadata", () => {
|
|
13
|
+
expect(cor024ReceiptSecretPointerRule.id).toBe("COR024");
|
|
14
|
+
expect(cor024ReceiptSecretPointerRule.severity).toBe("error");
|
|
15
|
+
expect(cor024ReceiptSecretPointerRule.category).toBe("correctness");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("flags a receipt input reading a Secret entity's data attribute", () => {
|
|
19
|
+
const ctx = createContext(`
|
|
20
|
+
export const dbSecret = new SecretManagerSecret({ name: "db-password" });
|
|
21
|
+
export const seeded = EffectReceipt("seeded", {
|
|
22
|
+
effect: "db-seed",
|
|
23
|
+
flavor: "hash",
|
|
24
|
+
inputs: { password: dbSecret.data.password },
|
|
25
|
+
});
|
|
26
|
+
`);
|
|
27
|
+
const diags = cor024ReceiptSecretPointerRule.check(ctx);
|
|
28
|
+
expect(diags).toHaveLength(1);
|
|
29
|
+
expect(diags[0].ruleId).toBe("COR024");
|
|
30
|
+
expect(diags[0].severity).toBe("error");
|
|
31
|
+
expect(diags[0].message).toContain("dbSecret.data.password");
|
|
32
|
+
expect(diags[0].message).toContain("name+version pointer");
|
|
33
|
+
expect(diags[0].message).toContain("secretVersion");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("flags a .value read on a Secret entity", () => {
|
|
37
|
+
const ctx = createContext(`
|
|
38
|
+
const apiToken = new ExternalSecret({ name: "api-token" });
|
|
39
|
+
export const bootstrapped = EffectReceipt("bootstrapped", {
|
|
40
|
+
effect: "bootstrap",
|
|
41
|
+
flavor: "hash",
|
|
42
|
+
inputs: { token: apiToken.value },
|
|
43
|
+
});
|
|
44
|
+
`);
|
|
45
|
+
const diags = cor024ReceiptSecretPointerRule.check(ctx);
|
|
46
|
+
expect(diags).toHaveLength(1);
|
|
47
|
+
expect(diags[0].message).toContain("apiToken.value");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("const indirection fires: material extracted into a variable first", () => {
|
|
51
|
+
const ctx = createContext(`
|
|
52
|
+
const dbSecret = new SecretManagerSecret({ name: "db-password" });
|
|
53
|
+
const material = dbSecret.data;
|
|
54
|
+
export const seeded = EffectReceipt("seeded", {
|
|
55
|
+
effect: "db-seed",
|
|
56
|
+
flavor: "hash",
|
|
57
|
+
inputs: { password: material.password },
|
|
58
|
+
});
|
|
59
|
+
`);
|
|
60
|
+
const diags = cor024ReceiptSecretPointerRule.check(ctx);
|
|
61
|
+
expect(diags).toHaveLength(1);
|
|
62
|
+
expect(diags[0].message).toContain("material");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("bare material variable as an input fires", () => {
|
|
66
|
+
const ctx = createContext(`
|
|
67
|
+
const dbSecret = new SecretManagerSecret({ name: "db-password" });
|
|
68
|
+
const material = dbSecret.stringData;
|
|
69
|
+
export const seeded = EffectReceipt("seeded", {
|
|
70
|
+
effect: "db-seed",
|
|
71
|
+
flavor: "hash",
|
|
72
|
+
inputs: { blob: material },
|
|
73
|
+
});
|
|
74
|
+
`);
|
|
75
|
+
expect(cor024ReceiptSecretPointerRule.check(ctx)).toHaveLength(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("passes: the name+version pointer form", () => {
|
|
79
|
+
const ctx = createContext(`
|
|
80
|
+
export const dbSecret = new SecretManagerSecret({ name: "db-password" });
|
|
81
|
+
export const seeded = EffectReceipt("seeded", {
|
|
82
|
+
effect: "db-seed",
|
|
83
|
+
flavor: "hash",
|
|
84
|
+
inputs: { secretName: dbSecret.name, secretVersion: 3 },
|
|
85
|
+
});
|
|
86
|
+
`);
|
|
87
|
+
expect(cor024ReceiptSecretPointerRule.check(ctx)).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("passes: non-secret entity attributes and static inputs", () => {
|
|
91
|
+
const ctx = createContext(`
|
|
92
|
+
export const db = new Database({ name: "main" });
|
|
93
|
+
export const seeded = EffectReceipt("seeded", {
|
|
94
|
+
effect: "db-seed",
|
|
95
|
+
flavor: "hash",
|
|
96
|
+
inputs: { endpoint: db.endpoint, schema: "v3", value: db.port },
|
|
97
|
+
});
|
|
98
|
+
`);
|
|
99
|
+
expect(cor024ReceiptSecretPointerRule.check(ctx)).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("passes: secret material read OUTSIDE a receipt's inputs is not this rule's concern", () => {
|
|
103
|
+
const ctx = createContext(`
|
|
104
|
+
const dbSecret = new SecretManagerSecret({ name: "db-password" });
|
|
105
|
+
export const app = new Service({ env: { DB_PASSWORD: dbSecret.data.password } });
|
|
106
|
+
`);
|
|
107
|
+
expect(cor024ReceiptSecretPointerRule.check(ctx)).toHaveLength(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("declareSecret provenance declarations are secret-rooted", () => {
|
|
111
|
+
const ctx = createContext(`
|
|
112
|
+
const dbSecret = declareSecret("db-password", { provenance: "referenced" });
|
|
113
|
+
export const seeded = EffectReceipt("seeded", {
|
|
114
|
+
effect: "db-seed",
|
|
115
|
+
flavor: "hash",
|
|
116
|
+
inputs: { v: dbSecret.value },
|
|
117
|
+
});
|
|
118
|
+
`);
|
|
119
|
+
expect(cor024ReceiptSecretPointerRule.check(ctx)).toHaveLength(1);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { LintRule, LintContext, LintDiagnostic } from "../rule";
|
|
3
|
+
import { receiptFactoryCall } from "./cor022-receipt-leaf";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* COR024: Receipt Inputs Reference Secrets by Pointer (#1833, epic #1703;
|
|
7
|
+
* composes with the secret-provenance work, #1365/#1828)
|
|
8
|
+
*
|
|
9
|
+
* A receipt's `inputs` feed its expectation hash (../../effect-receipt.ts) —
|
|
10
|
+
* whatever lands there is canonicalized, digested, and compared against a
|
|
11
|
+
* stored value for the rest of the receipt's life. chant's constitutional
|
|
12
|
+
* line on secrets is that no code path may hold, log, hash, or compare a
|
|
13
|
+
* secret value (../../secret-provenance.ts), so a receipt input that reads a
|
|
14
|
+
* Secret-kind entity's material (`.data`, `.value`, `.stringData`, ...)
|
|
15
|
+
* violates it twice over: the material enters the hash, and the serialized
|
|
16
|
+
* placeholder ties the receipt to the secret's value rather than its
|
|
17
|
+
* identity.
|
|
18
|
+
*
|
|
19
|
+
* The pointer form is fine — and is the point: reference the secret by name
|
|
20
|
+
* and version (`{ secretName: dbSecret.name, secretVersion: 3 }`), so a
|
|
21
|
+
* rotation is an explicit version bump that re-proposes the effect, and the
|
|
22
|
+
* hash only ever sees the pointer.
|
|
23
|
+
*
|
|
24
|
+
* Recognition is the same file-local reference walk COR022 uses (const
|
|
25
|
+
* indirection included): a variable is Secret-kind when its initializer is a
|
|
26
|
+
* constructor or factory whose name says so (`new SecretManagerSecret(...)`,
|
|
27
|
+
* `new ExternalSecret(...)`, `declareSecret(...)`); an input fires when a
|
|
28
|
+
* property chain rooted at one of those crosses a material field.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** Fields that carry secret material. Mirrors secret-provenance.ts's
|
|
32
|
+
* FORBIDDEN_MATERIAL_FIELDS, plus the provider-attribute spellings
|
|
33
|
+
* (`secretString`/`secretValue`) a materialized secret exposes. */
|
|
34
|
+
const MATERIAL_FIELDS = new Set([
|
|
35
|
+
"value",
|
|
36
|
+
"data",
|
|
37
|
+
"stringdata",
|
|
38
|
+
"material",
|
|
39
|
+
"plaintext",
|
|
40
|
+
"ciphertext",
|
|
41
|
+
"secretstring",
|
|
42
|
+
"secretvalue",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
function isMaterialField(name: string): boolean {
|
|
46
|
+
return MATERIAL_FIELDS.has(name.toLowerCase());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Simple name of a `new X(...)` / `X(...)` / `ns.X(...)` initializer. */
|
|
50
|
+
function initializerName(init: ts.Expression): string | undefined {
|
|
51
|
+
if (!ts.isNewExpression(init) && !ts.isCallExpression(init)) return undefined;
|
|
52
|
+
const callee = init.expression;
|
|
53
|
+
if (ts.isIdentifier(callee)) return callee.text;
|
|
54
|
+
if (ts.isPropertyAccessExpression(callee)) return callee.name.text;
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Does this constructor/factory name declare a Secret-kind entity? */
|
|
59
|
+
function isSecretKindName(name: string): boolean {
|
|
60
|
+
return /secret/i.test(name) || name === "declareSecret";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One access chain: `sec.data.password` → root "sec", segments ["data","password"]. */
|
|
64
|
+
function decomposeChain(
|
|
65
|
+
node: ts.Expression,
|
|
66
|
+
): { root: string; segments: string[] } | undefined {
|
|
67
|
+
const segments: string[] = [];
|
|
68
|
+
let current: ts.Expression = node;
|
|
69
|
+
while (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
|
|
70
|
+
if (ts.isPropertyAccessExpression(current)) {
|
|
71
|
+
segments.unshift(current.name.text);
|
|
72
|
+
} else {
|
|
73
|
+
const arg = current.argumentExpression;
|
|
74
|
+
segments.unshift(ts.isStringLiteralLike(arg) ? arg.text : "<computed>");
|
|
75
|
+
}
|
|
76
|
+
current = current.expression;
|
|
77
|
+
}
|
|
78
|
+
if (!ts.isIdentifier(current)) return undefined;
|
|
79
|
+
return { root: current.text, segments };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface SecretVars {
|
|
83
|
+
/** Variables holding a Secret-kind entity (or an alias of one). */
|
|
84
|
+
secretRooted: Set<string>;
|
|
85
|
+
/** Variables that already crossed a material field (`const m = sec.data`). */
|
|
86
|
+
material: Set<string>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** File-local Secret-kind variables, aliases and material extractions, to a fixpoint. */
|
|
90
|
+
function collectSecretVariables(sourceFile: ts.SourceFile): SecretVars {
|
|
91
|
+
const secretRooted = new Set<string>();
|
|
92
|
+
const material = new Set<string>();
|
|
93
|
+
const derived: Array<{ name: string; root: string; crossesMaterial: boolean; isBareAlias: boolean }> = [];
|
|
94
|
+
|
|
95
|
+
function visit(node: ts.Node): void {
|
|
96
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
97
|
+
const init = node.initializer;
|
|
98
|
+
const ctorName = initializerName(init);
|
|
99
|
+
if (ctorName !== undefined && isSecretKindName(ctorName)) {
|
|
100
|
+
secretRooted.add(node.name.text);
|
|
101
|
+
} else if (ts.isIdentifier(init)) {
|
|
102
|
+
derived.push({ name: node.name.text, root: init.text, crossesMaterial: false, isBareAlias: true });
|
|
103
|
+
} else if (ts.isPropertyAccessExpression(init) || ts.isElementAccessExpression(init)) {
|
|
104
|
+
const chain = decomposeChain(init);
|
|
105
|
+
if (chain) {
|
|
106
|
+
derived.push({
|
|
107
|
+
name: node.name.text,
|
|
108
|
+
root: chain.root,
|
|
109
|
+
crossesMaterial: chain.segments.some(isMaterialField),
|
|
110
|
+
isBareAlias: false,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
ts.forEachChild(node, visit);
|
|
116
|
+
}
|
|
117
|
+
visit(sourceFile);
|
|
118
|
+
|
|
119
|
+
let grew = true;
|
|
120
|
+
while (grew) {
|
|
121
|
+
grew = false;
|
|
122
|
+
for (const d of derived) {
|
|
123
|
+
const rootIsSecret = secretRooted.has(d.root);
|
|
124
|
+
const rootIsMaterial = material.has(d.root);
|
|
125
|
+
if (!rootIsSecret && !rootIsMaterial) continue;
|
|
126
|
+
const target = rootIsMaterial || d.crossesMaterial ? material : secretRooted;
|
|
127
|
+
if (d.isBareAlias && !d.crossesMaterial && rootIsSecret) {
|
|
128
|
+
if (!secretRooted.has(d.name)) {
|
|
129
|
+
secretRooted.add(d.name);
|
|
130
|
+
grew = true;
|
|
131
|
+
}
|
|
132
|
+
} else if (!target.has(d.name)) {
|
|
133
|
+
target.add(d.name);
|
|
134
|
+
grew = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { secretRooted, material };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const cor024ReceiptSecretPointerRule: LintRule = {
|
|
142
|
+
id: "COR024",
|
|
143
|
+
severity: "error",
|
|
144
|
+
category: "correctness",
|
|
145
|
+
description:
|
|
146
|
+
"A receipt's inputs may reference a secret only by name+version pointer, never by value — secret material must not enter the expectation hash",
|
|
147
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
148
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
149
|
+
const secrets = collectSecretVariables(context.sourceFile);
|
|
150
|
+
if (secrets.secretRooted.size === 0 && secrets.material.size === 0) return diagnostics;
|
|
151
|
+
|
|
152
|
+
const report = (node: ts.Node, rendered: string, root: string): void => {
|
|
153
|
+
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
154
|
+
node.getStart(context.sourceFile),
|
|
155
|
+
);
|
|
156
|
+
diagnostics.push({
|
|
157
|
+
file: context.filePath,
|
|
158
|
+
line: line + 1,
|
|
159
|
+
column: character + 1,
|
|
160
|
+
ruleId: "COR024",
|
|
161
|
+
severity: "error",
|
|
162
|
+
message:
|
|
163
|
+
`Receipt input reads secret material (${rendered}) from "${root}" — a receipt's inputs may ` +
|
|
164
|
+
`reference a secret only by name+version pointer, never by value: the inputs are hashed into ` +
|
|
165
|
+
`the receipt's expectation, and no chant code path may hash or compare secret material. ` +
|
|
166
|
+
`Use the pointer form instead, e.g. { secretName: ${root}.name, secretVersion: 3 } — ` +
|
|
167
|
+
`a rotation then re-proposes the effect through an explicit version bump.`,
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const checkInputs = (inputs: ts.Expression): void => {
|
|
172
|
+
const visit = (node: ts.Node): void => {
|
|
173
|
+
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
174
|
+
const chain = decomposeChain(node);
|
|
175
|
+
if (chain) {
|
|
176
|
+
const rootMaterial = secrets.material.has(chain.root);
|
|
177
|
+
const crossesMaterial = chain.segments.some(isMaterialField);
|
|
178
|
+
if (rootMaterial || (secrets.secretRooted.has(chain.root) && crossesMaterial)) {
|
|
179
|
+
report(node, `\`${chain.root}.${chain.segments.join(".")}\``, chain.root);
|
|
180
|
+
return; // one diagnostic per chain, not one per link
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else if (ts.isIdentifier(node) && secrets.material.has(node.text)) {
|
|
184
|
+
const parent = node.parent;
|
|
185
|
+
const isChainBase =
|
|
186
|
+
(ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) &&
|
|
187
|
+
parent.expression === node;
|
|
188
|
+
const isKey = ts.isPropertyAssignment(parent) && parent.name === node;
|
|
189
|
+
if (!isChainBase && !isKey) report(node, `\`${node.text}\``, node.text);
|
|
190
|
+
}
|
|
191
|
+
ts.forEachChild(node, visit);
|
|
192
|
+
};
|
|
193
|
+
visit(inputs);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const walk = (node: ts.Node): void => {
|
|
197
|
+
if (ts.isVariableDeclaration(node)) {
|
|
198
|
+
const call = receiptFactoryCall(node);
|
|
199
|
+
const options = call?.arguments[1];
|
|
200
|
+
if (options && ts.isObjectLiteralExpression(options)) {
|
|
201
|
+
for (const prop of options.properties) {
|
|
202
|
+
if (
|
|
203
|
+
ts.isPropertyAssignment(prop) &&
|
|
204
|
+
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
|
|
205
|
+
prop.name.text === "inputs"
|
|
206
|
+
) {
|
|
207
|
+
checkInputs(prop.initializer);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
ts.forEachChild(node, walk);
|
|
213
|
+
};
|
|
214
|
+
walk(context.sourceFile);
|
|
215
|
+
|
|
216
|
+
return diagnostics;
|
|
217
|
+
},
|
|
218
|
+
};
|
|
@@ -104,6 +104,33 @@ describe("EVL001: non-literal-expression", () => {
|
|
|
104
104
|
expect(diags[0].ruleId).toBe("EVL001");
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
// chant #1544 — the single-action Composite() wrapper idiom
|
|
108
|
+
// (`Checkout({...}).step`) every lexicon's own docs/examples embed inline
|
|
109
|
+
// inside a `Job`'s `steps:` array must not trip EVL001.
|
|
110
|
+
test("allows a composite call's .step access embedded in an array", () => {
|
|
111
|
+
const ctx = createContext(`new Job({ "runs-on": "ubuntu-latest", steps: [Checkout({}).step] });`);
|
|
112
|
+
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
113
|
+
expect(diags).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("allows a composite call's .step access as a direct property value", () => {
|
|
117
|
+
const ctx = createContext(`new Job({ step: Checkout({ ref: "main" }).step });`);
|
|
118
|
+
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
119
|
+
expect(diags).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("still flags a call accessing a non-.step property", () => {
|
|
123
|
+
const ctx = createContext(`new Job({ steps: [Checkout({}).output] });`);
|
|
124
|
+
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
125
|
+
expect(diags).toHaveLength(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("still flags a bare call (no trailing .step access)", () => {
|
|
129
|
+
const ctx = createContext(`new Bucket({ name: getName() });`);
|
|
130
|
+
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
131
|
+
expect(diags).toHaveLength(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
107
134
|
test("flags multiple violations", () => {
|
|
108
135
|
const ctx = createContext(`new Bucket({ a: foo(), b: bar() });`);
|
|
109
136
|
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
@@ -31,7 +31,14 @@ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnos
|
|
|
31
31
|
const firstArg = node.arguments[0];
|
|
32
32
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
33
33
|
for (const prop of firstArg.properties) {
|
|
34
|
-
|
|
34
|
+
// chant #1544 — `allowCompositeStepAccess: true` opts EVL001 (and
|
|
35
|
+
// only EVL001; `fold()` never sets this) into treating
|
|
36
|
+
// `Checkout({...}).step` — the single-action Composite()-wrapper
|
|
37
|
+
// idiom every lexicon's own docs/examples embed inline inside a
|
|
38
|
+
// Job's `steps:` array — as shape-valid. See findSubsetViolation's
|
|
39
|
+
// doc comment (../../fold/subset.ts) for why this is a documented,
|
|
40
|
+
// EVL-only, more-permissive divergence rather than a shared one.
|
|
41
|
+
const violation = checkObjectMember(prop, context.intrinsics, true);
|
|
35
42
|
if (violation) {
|
|
36
43
|
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
37
44
|
violation.node.getStart(context.sourceFile),
|
package/src/lint/rules/index.ts
CHANGED
|
@@ -25,6 +25,9 @@ export { evl009CompositeNoConstantRule } from "./evl009-composite-no-constant";
|
|
|
25
25
|
export { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
|
|
26
26
|
export { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
|
|
27
27
|
export { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
|
|
28
|
+
export { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
|
|
29
|
+
export { cor022ReceiptLeafRule, collectReceiptVariables, receiptFactoryCall } from "./cor022-receipt-leaf";
|
|
30
|
+
export { cor024ReceiptSecretPointerRule } from "./cor024-receipt-secret-pointer";
|
|
28
31
|
export { isInsideCompositeFactory } from "./composite-scope";
|
|
29
32
|
|
|
30
33
|
import { flatDeclarationsRule } from "./flat-declarations";
|
|
@@ -48,9 +51,12 @@ import { evl009CompositeNoConstantRule } from "./evl009-composite-no-constant";
|
|
|
48
51
|
import { evl010CompositeNoTransformRule } from "./evl010-composite-no-transform";
|
|
49
52
|
import { cor017CompositeNameMatchRule } from "./cor017-composite-name-match";
|
|
50
53
|
import { cor018CompositePreferLexiconTypeRule } from "./cor018-composite-prefer-lexicon-type";
|
|
54
|
+
import { cor021EnvLiteralNameRule } from "./cor021-env-literal-name";
|
|
55
|
+
import { cor022ReceiptLeafRule } from "./cor022-receipt-leaf";
|
|
56
|
+
import { cor024ReceiptSecretPointerRule } from "./cor024-receipt-secret-pointer";
|
|
51
57
|
|
|
52
58
|
/**
|
|
53
|
-
* Load all
|
|
59
|
+
* Load all 24 core lint rules (COR + EVL).
|
|
54
60
|
*/
|
|
55
61
|
export function loadCoreRules(): LintRule[] {
|
|
56
62
|
return [
|
|
@@ -75,5 +81,8 @@ export function loadCoreRules(): LintRule[] {
|
|
|
75
81
|
evl010CompositeNoTransformRule,
|
|
76
82
|
cor017CompositeNameMatchRule,
|
|
77
83
|
cor018CompositePreferLexiconTypeRule,
|
|
84
|
+
cor021EnvLiteralNameRule,
|
|
85
|
+
cor022ReceiptLeafRule,
|
|
86
|
+
cor024ReceiptSecretPointerRule,
|
|
78
87
|
];
|
|
79
88
|
}
|
|
@@ -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
|
+
});
|