@intentius/chant 0.41.19 → 0.42.1
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/cli/build-params-cli.d.ts +7 -4
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/carve-apply.d.ts +9 -0
- package/dist/cli/commands/carve-apply.d.ts.map +1 -1
- package/dist/cli/commands/carve-bridge.d.ts +7 -0
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/carve-emit.d.ts +7 -0
- package/dist/cli/commands/carve-emit.d.ts.map +1 -1
- package/dist/cli/commands/import.d.ts +16 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/handlers/carve-apply.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/explain.d.ts +11 -0
- package/dist/cli/handlers/explain.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +4 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts +11 -0
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/codegen/okf-lexicon.d.ts +36 -0
- package/dist/codegen/okf-lexicon.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +2 -1
- package/dist/codegen/package.d.ts.map +1 -1
- package/dist/components/deploy-units.d.ts.map +1 -1
- package/dist/components/verbs/sbom.d.ts +1 -1
- package/dist/components/verbs/sbom.d.ts.map +1 -1
- package/dist/components/verbs/vuln-gate.d.ts +15 -4
- package/dist/components/verbs/vuln-gate.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts +18 -6
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +13 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/governance.d.ts +37 -0
- package/dist/governance.d.ts.map +1 -0
- package/dist/lexicon.d.ts +3 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/okf.d.ts +57 -0
- package/dist/okf.d.ts.map +1 -0
- package/dist/reconcile.d.ts +15 -0
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/terraform/__fixtures__/build-graph.d.ts +13 -0
- package/dist/terraform/__fixtures__/build-graph.d.ts.map +1 -0
- package/dist/terraform/adopt-state.d.ts +33 -1
- package/dist/terraform/adopt-state.d.ts.map +1 -1
- package/dist/terraform/bridge.d.ts +7 -1
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve.d.ts +12 -0
- package/dist/terraform/carve.d.ts.map +1 -1
- package/dist/terraform/excise.d.ts +31 -0
- package/dist/terraform/excise.d.ts.map +1 -0
- package/dist/terraform/graduate.d.ts +12 -0
- package/dist/terraform/graduate.d.ts.map +1 -1
- package/dist/terraform/graph.d.ts +35 -5
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/manifest.d.ts +89 -0
- package/dist/terraform/manifest.d.ts.map +1 -0
- package/dist/terraform/parse.d.ts +9 -4
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +7 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/dist/terraform/unified-diff.d.ts +11 -0
- package/dist/terraform/unified-diff.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +381 -0
- package/src/cli/build-params-cli.test.ts +47 -3
- package/src/cli/build-params-cli.ts +22 -5
- package/src/cli/commands/build.test.ts +44 -0
- package/src/cli/commands/build.ts +15 -2
- package/src/cli/commands/carve-apply.test.ts +94 -2
- package/src/cli/commands/carve-apply.ts +62 -11
- package/src/cli/commands/carve-bridge.test.ts +82 -4
- package/src/cli/commands/carve-bridge.ts +79 -11
- package/src/cli/commands/carve-emit-state.test.ts +110 -1
- package/src/cli/commands/carve-emit.ts +184 -8
- package/src/cli/commands/import.ts +53 -5
- package/src/cli/commands/init.ts +1 -1
- package/src/cli/handlers/build.test.ts +3 -1
- package/src/cli/handlers/carve-apply.ts +1 -0
- package/src/cli/handlers/components.ts +4 -2
- package/src/cli/handlers/explain.test.ts +93 -0
- package/src/cli/handlers/explain.ts +60 -0
- package/src/cli/handlers/lifecycle.ts +9 -7
- package/src/cli/handlers/misc.ts +40 -1
- package/src/cli/handlers/run.test.ts +3 -1
- package/src/cli/main.ts +25 -5
- package/src/cli/mcp/server.test.ts +17 -0
- package/src/cli/mcp/tools/explain.ts +15 -3
- package/src/cli/registry.ts +4 -0
- package/src/codegen/__snapshots__/okf-lexicon.test.ts.snap +170 -0
- package/src/codegen/docs-rule-scanning.ts +35 -14
- package/src/codegen/okf-lexicon.test.ts +248 -0
- package/src/codegen/okf-lexicon.ts +303 -0
- package/src/codegen/package.ts +29 -4
- package/src/codegen/publish-order.test.ts +1 -1
- package/src/codegen/release-wiring.test.ts +7 -3
- package/src/components/config-defaults.test.ts +30 -0
- package/src/components/deploy-units.ts +3 -0
- package/src/components/verbs/__fixtures__/grype-with-kev-epss.json +1 -0
- package/src/components/verbs/__fixtures__/trivy-with-kev-epss.json +631 -0
- package/src/components/verbs/exploitability-roundtrip.test.ts +190 -0
- package/src/components/verbs/sbom.ts +1 -1
- package/src/components/verbs/vuln-gate.test.ts +144 -1
- package/src/components/verbs/vuln-gate.ts +61 -10
- package/src/components/verbs/vuln-scan.test.ts +94 -0
- package/src/components/verbs/vuln-scan.ts +44 -9
- package/src/config.test.ts +21 -0
- package/src/config.ts +18 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +32 -13
- package/src/governance.test.ts +72 -0
- package/src/governance.ts +54 -0
- package/src/lexicon.ts +3 -0
- package/src/lifecycle/observe.test.ts +18 -0
- package/src/lifecycle/observe.ts +10 -3
- package/src/lifecycle/snapshot.ts +5 -4
- package/src/okf.test.ts +169 -0
- package/src/okf.ts +308 -0
- package/src/reconcile.ts +32 -1
- package/src/terraform/__fixtures__/build-graph.ts +25 -0
- package/src/terraform/adopt-state.test.ts +34 -1
- package/src/terraform/adopt-state.ts +69 -2
- package/src/terraform/bridge.test.ts +38 -14
- package/src/terraform/bridge.ts +48 -14
- package/src/terraform/carve.test.ts +9 -7
- package/src/terraform/carve.ts +0 -0
- package/src/terraform/excise.test.ts +92 -0
- package/src/terraform/excise.ts +139 -0
- package/src/terraform/graduate.test.ts +73 -6
- package/src/terraform/graduate.ts +49 -0
- package/src/terraform/graph.test.ts +56 -10
- package/src/terraform/graph.ts +112 -45
- package/src/terraform/manifest.test.ts +105 -0
- package/src/terraform/manifest.ts +162 -0
- package/src/terraform/parse.test.ts +39 -1
- package/src/terraform/parse.ts +36 -9
- package/src/terraform/score.test.ts +8 -8
- package/src/terraform/state.test.ts +4 -4
- package/src/terraform/types.ts +7 -0
- package/src/terraform/unified-diff.test.ts +77 -0
- package/src/terraform/unified-diff.ts +146 -0
package/src/okf.test.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { buildOkfBundle, okfConformanceProblems, splitFrontmatter, OKF_VERSION, type OkfFile } from "./okf";
|
|
5
|
+
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
6
|
+
import { setProvenance } from "./provenance";
|
|
7
|
+
import { parseYAML } from "./yaml";
|
|
8
|
+
import { discover } from "./discovery/index";
|
|
9
|
+
|
|
10
|
+
function decl<T extends object>(base: T): Declarable & T {
|
|
11
|
+
return { [DECLARABLE_MARKER]: true, ...base } as Declarable & T;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function fileMap(bundle: OkfFile[]): Map<string, string> {
|
|
15
|
+
return new Map(bundle.map((f) => [f.path, f.content]));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Assert the four OKF v0.2 conformance criteria (spec §11) over a bundle via
|
|
20
|
+
* the shared checker (`okfConformanceProblems`) — the same one the lexicon
|
|
21
|
+
* bundle tests (#1060) run — plus the version pin our own emitter guarantees.
|
|
22
|
+
*/
|
|
23
|
+
function assertConformant(bundle: OkfFile[]): void {
|
|
24
|
+
expect(okfConformanceProblems(bundle)).toEqual([]);
|
|
25
|
+
const index = bundle.find((f) => f.path === "index.md");
|
|
26
|
+
expect(index).toBeDefined();
|
|
27
|
+
const split = splitFrontmatter(index!.content);
|
|
28
|
+
expect(split).toBeDefined();
|
|
29
|
+
expect(parseYAML(split!.frontmatter)).toEqual({ okf_version: OKF_VERSION });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("buildOkfBundle", () => {
|
|
33
|
+
test("emits one concept per entity with parseable frontmatter and a non-empty type", () => {
|
|
34
|
+
const bucket = decl({ lexicon: "aws", entityType: "AWS::S3::Bucket", kind: "resource" as const });
|
|
35
|
+
setProvenance(bucket, { sourceFile: "/proj/src/storage.ts" });
|
|
36
|
+
const vpc = decl({ lexicon: "gcp", entityType: "Vpc" });
|
|
37
|
+
const entities = new Map<string, Declarable>([
|
|
38
|
+
["myBucket", bucket],
|
|
39
|
+
["vpc", vpc],
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const bundle = buildOkfBundle({ entities, dependencies: new Map() }, "/proj");
|
|
43
|
+
const files = fileMap(bundle);
|
|
44
|
+
expect([...files.keys()]).toEqual(["aws/myBucket.md", "gcp/vpc.md", "index.md"]);
|
|
45
|
+
|
|
46
|
+
const concept = splitFrontmatter(files.get("aws/myBucket.md")!)!;
|
|
47
|
+
const front = parseYAML(concept.frontmatter);
|
|
48
|
+
expect(front.type).toBe("AWS::S3::Bucket");
|
|
49
|
+
expect(front.title).toBe("myBucket");
|
|
50
|
+
expect(front.lexicon).toBe("aws");
|
|
51
|
+
expect(front.kind).toBe("resource");
|
|
52
|
+
expect(front.source).toBe("src/storage.ts");
|
|
53
|
+
expect(concept.body).toContain("Declared in `src/storage.ts`.");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("dependency edges become bundle-relative markdown links, both directions", () => {
|
|
57
|
+
const vpc = decl({ lexicon: "gcp", entityType: "Vpc" });
|
|
58
|
+
const subnet = decl({ lexicon: "gcp", entityType: "Subnet" });
|
|
59
|
+
const entities = new Map<string, Declarable>([
|
|
60
|
+
["vpc", vpc],
|
|
61
|
+
["subnet", subnet],
|
|
62
|
+
]);
|
|
63
|
+
const dependencies = new Map([["subnet", new Set(["vpc"])]]);
|
|
64
|
+
|
|
65
|
+
const files = fileMap(buildOkfBundle({ entities, dependencies }));
|
|
66
|
+
expect(files.get("gcp/subnet.md")).toContain("## Depends on");
|
|
67
|
+
expect(files.get("gcp/subnet.md")).toContain("- [vpc](/gcp/vpc.md)");
|
|
68
|
+
expect(files.get("gcp/vpc.md")).toContain("## Referenced by");
|
|
69
|
+
expect(files.get("gcp/vpc.md")).toContain("- [subnet](/gcp/subnet.md)");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("an unresolved dependency emits a link rather than failing — broken links are permitted", () => {
|
|
73
|
+
const app = decl({ lexicon: "k8s", entityType: "Deployment" });
|
|
74
|
+
const entities = new Map<string, Declarable>([["app", app]]);
|
|
75
|
+
const dependencies = new Map([["app", new Set(["ghost"])]]);
|
|
76
|
+
|
|
77
|
+
const bundle = buildOkfBundle({ entities, dependencies });
|
|
78
|
+
expect(fileMap(bundle).get("k8s/app.md")).toContain("- [ghost](/ghost.md)");
|
|
79
|
+
assertConformant(bundle);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("index.md groups entities into per-lexicon sections with okf_version frontmatter", () => {
|
|
83
|
+
const bucket = decl({ lexicon: "aws", entityType: "AWS::S3::Bucket" });
|
|
84
|
+
const job = decl({ lexicon: "gitlab", entityType: "GitLab::Job" });
|
|
85
|
+
const entities = new Map<string, Declarable>([
|
|
86
|
+
["assets", bucket],
|
|
87
|
+
["buildJob", job],
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const index = fileMap(buildOkfBundle({ entities, dependencies: new Map() })).get("index.md")!;
|
|
91
|
+
expect(index).toContain(`okf_version: '${OKF_VERSION}'`);
|
|
92
|
+
expect(index).toContain("# Lexicon: aws");
|
|
93
|
+
expect(index).toContain("# Lexicon: gitlab");
|
|
94
|
+
expect(index).toContain("* [assets](/aws/assets.md) - aws resource of type AWS::S3::Bucket");
|
|
95
|
+
expect(index).toContain("* [buildJob](/gitlab/buildJob.md) - gitlab resource of type GitLab::Job");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("composite provenance rides along as extra frontmatter keys", () => {
|
|
99
|
+
const dep = decl({ lexicon: "k8s", entityType: "K8s::Apps::Deployment" });
|
|
100
|
+
setProvenance(dep, { sourceFile: "/proj/src/web.ts", composite: "WebApp", compositeInstance: "prodApp" });
|
|
101
|
+
const entities = new Map<string, Declarable>([["prodAppDeployment", dep]]);
|
|
102
|
+
|
|
103
|
+
const front = parseYAML(
|
|
104
|
+
splitFrontmatter(fileMap(buildOkfBundle({ entities, dependencies: new Map() }, "/proj")).get("k8s/prodAppDeployment.md")!)!.frontmatter,
|
|
105
|
+
);
|
|
106
|
+
expect(front.composite).toBe("WebApp");
|
|
107
|
+
expect(front.composite_instance).toBe("prodApp");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("entity names that slug to the same path are deduped, never overwritten", () => {
|
|
111
|
+
const a = decl({ lexicon: "aws", entityType: "AWS::S3::Bucket" });
|
|
112
|
+
const b = decl({ lexicon: "aws", entityType: "AWS::S3::Bucket" });
|
|
113
|
+
const entities = new Map<string, Declarable>([
|
|
114
|
+
["my/bucket", a],
|
|
115
|
+
["my-bucket", b],
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const paths = buildOkfBundle({ entities, dependencies: new Map() }).map((f) => f.path);
|
|
119
|
+
expect(new Set(paths).size).toBe(paths.length);
|
|
120
|
+
expect(paths).toContain("aws/my-bucket.md");
|
|
121
|
+
expect(paths).toContain("aws/my-bucket-2.md");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("an empty project still emits a conformant bundle (just the index)", () => {
|
|
125
|
+
const bundle = buildOkfBundle({ entities: new Map(), dependencies: new Map() });
|
|
126
|
+
expect(bundle.map((f) => f.path)).toEqual(["index.md"]);
|
|
127
|
+
assertConformant(bundle);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("deterministic: the same input yields byte-identical files", () => {
|
|
131
|
+
const vpc = decl({ lexicon: "gcp", entityType: "Vpc" });
|
|
132
|
+
const subnet = decl({ lexicon: "gcp", entityType: "Subnet" });
|
|
133
|
+
const entities = new Map<string, Declarable>([
|
|
134
|
+
["vpc", vpc],
|
|
135
|
+
["subnet", subnet],
|
|
136
|
+
]);
|
|
137
|
+
const dependencies = new Map([["subnet", new Set(["vpc"])]]);
|
|
138
|
+
|
|
139
|
+
expect(buildOkfBundle({ entities, dependencies })).toEqual(buildOkfBundle({ entities, dependencies }));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Shipped examples, spanning lexicons (#1058 acceptance): each bundle is
|
|
145
|
+
// asserted conformant against the four v0.2 criteria and snapshot-tested.
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
const repoRoot = resolve(fileURLToPath(import.meta.url), "../../../..");
|
|
149
|
+
|
|
150
|
+
const exampleCases: Array<[string, string]> = [
|
|
151
|
+
["k8s layered-config", "lexicons/k8s/examples/layered-config/src"],
|
|
152
|
+
["aws lambda-s3", "lexicons/aws/examples/lambda-s3/src"],
|
|
153
|
+
["gitlab node-pipeline", "lexicons/gitlab/examples/node-pipeline/src"],
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
describe("buildOkfBundle over shipped examples", () => {
|
|
157
|
+
for (const [name, rel] of exampleCases) {
|
|
158
|
+
test(`${name} emits a conformant, snapshot-stable bundle`, async () => {
|
|
159
|
+
const projectPath = resolve(repoRoot, rel);
|
|
160
|
+
const result = await discover(projectPath);
|
|
161
|
+
expect(result.errors).toEqual([]);
|
|
162
|
+
expect(result.entities.size).toBeGreaterThan(0);
|
|
163
|
+
|
|
164
|
+
const bundle = buildOkfBundle(result, projectPath);
|
|
165
|
+
assertConformant(bundle);
|
|
166
|
+
expect(bundle).toMatchSnapshot();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
package/src/okf.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { relative, isAbsolute } from "node:path";
|
|
2
|
+
import type { Declarable } from "./declarable";
|
|
3
|
+
import { buildGraphIr } from "./graph-ir";
|
|
4
|
+
import { isLexiconOutput, type LexiconOutput } from "./lexicon-output";
|
|
5
|
+
import { getProvenance } from "./provenance";
|
|
6
|
+
import { emitYAML, parseYAML } from "./yaml";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OKF bundle emitter (#1058, epic #1057) — projects a discovered entity graph
|
|
10
|
+
* into an Open Knowledge Format v0.2 bundle: a directory of markdown files
|
|
11
|
+
* with YAML frontmatter, per the spec at
|
|
12
|
+
* https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md
|
|
13
|
+
*
|
|
14
|
+
* One concept document per entity, `type` carrying the entity's resource type
|
|
15
|
+
* string, dependency edges as bundle-relative markdown links (the form the
|
|
16
|
+
* spec recommends for stability), and a root `index.md` grouping concepts into
|
|
17
|
+
* per-lexicon sections. Emit-only: OKF as project *input* is #1059.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const OKF_VERSION = "0.2";
|
|
21
|
+
|
|
22
|
+
/** One file of an OKF bundle: a bundle-relative path and its full content. */
|
|
23
|
+
export interface OkfFile {
|
|
24
|
+
/** Bundle-relative path, e.g. "aws/myBucket.md" or "index.md". */
|
|
25
|
+
path: string;
|
|
26
|
+
content: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The subset of a discovery result the emitter reads. */
|
|
30
|
+
export interface OkfBundleInput {
|
|
31
|
+
entities: Map<string, Declarable>;
|
|
32
|
+
dependencies: Map<string, Set<string>>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Make an entity or lexicon name safe as a single path segment. */
|
|
36
|
+
export function slug(name: string): string {
|
|
37
|
+
const cleaned = name.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^\.+/, "");
|
|
38
|
+
return cleaned === "" ? "unnamed" : cleaned;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function relFile(file: string | undefined, projectPath?: string): string | undefined {
|
|
42
|
+
if (!file) return undefined;
|
|
43
|
+
if (projectPath && isAbsolute(file)) {
|
|
44
|
+
const rel = relative(projectPath, file);
|
|
45
|
+
return rel.startsWith("..") ? file : rel;
|
|
46
|
+
}
|
|
47
|
+
return file;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Render a flat frontmatter mapping as a YAML block. Keys with undefined
|
|
52
|
+
* values are dropped; scalar quoting is `emitYAML`'s.
|
|
53
|
+
*/
|
|
54
|
+
export function frontmatter(fields: Record<string, string | undefined>): string {
|
|
55
|
+
const lines: string[] = ["---"];
|
|
56
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
57
|
+
if (value === undefined) continue;
|
|
58
|
+
lines.push(`${key}: ${emitYAML(value, 0)}`);
|
|
59
|
+
}
|
|
60
|
+
lines.push("---");
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Split a concept document into its frontmatter block and body. Returns
|
|
66
|
+
* undefined when the document does not start with a `---` delimited block.
|
|
67
|
+
* Exported for the conformance tests (and any future OKF consumer, #1059).
|
|
68
|
+
*/
|
|
69
|
+
export function splitFrontmatter(content: string): { frontmatter: string; body: string } | undefined {
|
|
70
|
+
const lines = content.split("\n");
|
|
71
|
+
if (lines[0] !== "---") return undefined;
|
|
72
|
+
const end = lines.indexOf("---", 1);
|
|
73
|
+
if (end === -1) return undefined;
|
|
74
|
+
return {
|
|
75
|
+
frontmatter: lines.slice(1, end).join("\n"),
|
|
76
|
+
body: lines.slice(end + 1).join("\n"),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check a bundle against the OKF v0.2 conformance criteria (spec §11) and
|
|
82
|
+
* return every violation found: parseable frontmatter on every non-reserved
|
|
83
|
+
* `.md`, a non-empty `type` everywhere, a well-formed root `index.md`, and
|
|
84
|
+
* generated cross-link entries that are bundle-absolute (or external URLs).
|
|
85
|
+
* Empty result means conformant. Shared by the project-bundle tests (#1058)
|
|
86
|
+
* and every lexicon's bundle tests (#1060).
|
|
87
|
+
*/
|
|
88
|
+
export function okfConformanceProblems(files: OkfFile[]): string[] {
|
|
89
|
+
const problems: string[] = [];
|
|
90
|
+
const reserved = new Set(["index.md", "log.md"]);
|
|
91
|
+
const linkTarget = /\]\(([^)]+)\)/g;
|
|
92
|
+
const externalOrAbsolute = (target: string): boolean =>
|
|
93
|
+
target.startsWith("/") || /^[a-z][a-z0-9+.-]*:/i.test(target);
|
|
94
|
+
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
if (!file.path.endsWith(".md")) problems.push(`${file.path}: not a markdown file`);
|
|
97
|
+
if (file.path.startsWith("/") || file.path.includes("..")) {
|
|
98
|
+
problems.push(`${file.path}: path is not bundle-relative`);
|
|
99
|
+
}
|
|
100
|
+
if (reserved.has(file.path.split("/").pop()!)) continue;
|
|
101
|
+
|
|
102
|
+
const split = splitFrontmatter(file.content);
|
|
103
|
+
if (!split) {
|
|
104
|
+
problems.push(`${file.path}: no frontmatter block`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
let parsed: unknown;
|
|
108
|
+
try {
|
|
109
|
+
parsed = parseYAML(split.frontmatter);
|
|
110
|
+
} catch {
|
|
111
|
+
problems.push(`${file.path}: unparseable frontmatter`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
115
|
+
problems.push(`${file.path}: frontmatter is not a mapping`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const type = (parsed as Record<string, unknown>).type;
|
|
119
|
+
if (typeof type !== "string" || type.length === 0) {
|
|
120
|
+
problems.push(`${file.path}: frontmatter has no non-empty type`);
|
|
121
|
+
}
|
|
122
|
+
// Cross-link entries (the "- [name](target)" lines the emitters generate)
|
|
123
|
+
// must point inside the bundle or at an external URL. Broken targets are
|
|
124
|
+
// permitted (not-yet-written knowledge); malformed ones are not. Free-text
|
|
125
|
+
// knowledge bodies may carry arbitrary links and are not checked.
|
|
126
|
+
for (const line of split.body.split("\n")) {
|
|
127
|
+
if (!line.trimStart().startsWith("- [")) continue;
|
|
128
|
+
for (const match of line.matchAll(linkTarget)) {
|
|
129
|
+
if (!externalOrAbsolute(match[1])) {
|
|
130
|
+
problems.push(`${file.path}: link target "${match[1]}" is neither bundle-absolute nor external`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const index = files.find((f) => f.path === "index.md");
|
|
137
|
+
if (!index) {
|
|
138
|
+
problems.push("bundle has no root index.md");
|
|
139
|
+
return problems;
|
|
140
|
+
}
|
|
141
|
+
const split = splitFrontmatter(index.content);
|
|
142
|
+
if (!split) {
|
|
143
|
+
problems.push("index.md: no frontmatter block");
|
|
144
|
+
return problems;
|
|
145
|
+
}
|
|
146
|
+
const parsed = parseYAML(split.frontmatter);
|
|
147
|
+
for (const key of Object.keys(parsed ?? {})) {
|
|
148
|
+
if (key !== "okf_version") problems.push(`index.md: frontmatter carries "${key}" — okf_version is the only key an index is allowed`);
|
|
149
|
+
}
|
|
150
|
+
for (const line of split.body.split("\n")) {
|
|
151
|
+
if (line.startsWith("*") && !/^\* \[[^\]]+\]\([^)]+\) - .+$/.test(line)) {
|
|
152
|
+
problems.push(`index.md: malformed entry line: ${line}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return problems;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface Concept {
|
|
159
|
+
name: string;
|
|
160
|
+
entity: Declarable;
|
|
161
|
+
/** Bundle-relative concept path, e.g. "aws/myBucket.md". */
|
|
162
|
+
path: string;
|
|
163
|
+
lexicon: string;
|
|
164
|
+
/** The frontmatter `type` — the entity's resource type string. */
|
|
165
|
+
type: string;
|
|
166
|
+
kind: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The `type`/`lexicon`/`kind` a concept carries. Most entities state all
|
|
171
|
+
* three; a `LexiconOutput` (an `output(...)` bridge) carries none of them
|
|
172
|
+
* directly, so its facts come from its producing side — `type` must be
|
|
173
|
+
* non-empty for the bundle to conform, never blank.
|
|
174
|
+
*/
|
|
175
|
+
function conceptFacts(entity: Declarable): { type: string; lexicon: string; kind: string } {
|
|
176
|
+
if (isLexiconOutput(entity)) {
|
|
177
|
+
const out = entity as unknown as LexiconOutput;
|
|
178
|
+
return { type: "Output", lexicon: out.sourceLexicon || "unknown", kind: "output" };
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
type: entity.entityType || "Entity",
|
|
182
|
+
lexicon: entity.lexicon || "unknown",
|
|
183
|
+
kind: entity.kind ?? "resource",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Build the OKF bundle for a discovered graph. Pure and deterministic: the
|
|
189
|
+
* same entities yield byte-identical files, so bundles are diffable and
|
|
190
|
+
* snapshot-testable. `projectPath` relativizes source-file paths so the
|
|
191
|
+
* bundle is portable.
|
|
192
|
+
*/
|
|
193
|
+
export function buildOkfBundle(input: OkfBundleInput, projectPath?: string): OkfFile[] {
|
|
194
|
+
// Assign each entity a stable concept path: <lexicon>/<name>.md, deduped
|
|
195
|
+
// when slugging collides two distinct names.
|
|
196
|
+
const concepts = new Map<string, Concept>();
|
|
197
|
+
const takenPaths = new Set<string>();
|
|
198
|
+
const names = [...input.entities.keys()].sort((a, b) => a.localeCompare(b));
|
|
199
|
+
for (const name of names) {
|
|
200
|
+
const entity = input.entities.get(name)!;
|
|
201
|
+
const facts = conceptFacts(entity);
|
|
202
|
+
const base = `${slug(facts.lexicon)}/${slug(name)}`;
|
|
203
|
+
let path = `${base}.md`;
|
|
204
|
+
for (let n = 2; takenPaths.has(path); n++) path = `${base}-${n}.md`;
|
|
205
|
+
takenPaths.add(path);
|
|
206
|
+
concepts.set(name, { name, entity, path, ...facts });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Dependency edges: the graph IR's AttrRef/Ref-derived edges (the same
|
|
210
|
+
// derivation `chant graph --format ir` ships) unioned with discovery's
|
|
211
|
+
// declared dependency map — the map alone misses references that live only
|
|
212
|
+
// in attribute values.
|
|
213
|
+
const dependsOn = new Map<string, Set<string>>();
|
|
214
|
+
const addEdge = (from: string, to: string): void => {
|
|
215
|
+
if (!dependsOn.has(from)) dependsOn.set(from, new Set());
|
|
216
|
+
dependsOn.get(from)!.add(to);
|
|
217
|
+
};
|
|
218
|
+
for (const edge of buildGraphIr(input.entities, projectPath).edges) addEdge(edge.from, edge.to);
|
|
219
|
+
for (const [from, deps] of input.dependencies) for (const to of deps) addEdge(from, to);
|
|
220
|
+
|
|
221
|
+
// Reverse edges: who references each entity.
|
|
222
|
+
const referencedBy = new Map<string, string[]>();
|
|
223
|
+
for (const [from, deps] of dependsOn) {
|
|
224
|
+
for (const to of deps) {
|
|
225
|
+
if (!referencedBy.has(to)) referencedBy.set(to, []);
|
|
226
|
+
referencedBy.get(to)!.push(from);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const files: OkfFile[] = [];
|
|
231
|
+
|
|
232
|
+
for (const name of names) {
|
|
233
|
+
const concept = concepts.get(name)!;
|
|
234
|
+
const { entity } = concept;
|
|
235
|
+
const prov = getProvenance(entity);
|
|
236
|
+
const source = relFile(prov?.sourceFile, projectPath);
|
|
237
|
+
|
|
238
|
+
const head = frontmatter({
|
|
239
|
+
type: concept.type,
|
|
240
|
+
title: name,
|
|
241
|
+
description: `${concept.lexicon} ${concept.kind} of type ${concept.type}`,
|
|
242
|
+
name,
|
|
243
|
+
lexicon: concept.lexicon,
|
|
244
|
+
kind: concept.kind,
|
|
245
|
+
source,
|
|
246
|
+
composite: prov?.composite,
|
|
247
|
+
composite_instance: prov?.compositeInstance,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const body: string[] = [""];
|
|
251
|
+
body.push(source ? `Declared in \`${source}\`.` : `Declared by the ${concept.lexicon} lexicon.`);
|
|
252
|
+
|
|
253
|
+
// Cross-resource references become bundle-relative links. A dependency
|
|
254
|
+
// whose target is not in the bundle still emits a link — the spec permits
|
|
255
|
+
// broken links (not-yet-written knowledge), so unresolved never fails.
|
|
256
|
+
const deps = [...(dependsOn.get(name) ?? [])].sort((a, b) => a.localeCompare(b));
|
|
257
|
+
if (deps.length > 0) {
|
|
258
|
+
body.push("", "## Depends on", "");
|
|
259
|
+
for (const dep of deps) {
|
|
260
|
+
const target = concepts.get(dep)?.path ?? `${slug(dep)}.md`;
|
|
261
|
+
body.push(`- [${dep}](/${target})`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const consumers = (referencedBy.get(name) ?? []).sort((a, b) => a.localeCompare(b));
|
|
266
|
+
if (consumers.length > 0) {
|
|
267
|
+
body.push("", "## Referenced by", "");
|
|
268
|
+
for (const consumer of consumers) {
|
|
269
|
+
const target = concepts.get(consumer)?.path ?? `${slug(consumer)}.md`;
|
|
270
|
+
body.push(`- [${consumer}](/${target})`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
body.push("");
|
|
275
|
+
files.push({ path: concept.path, content: head + body.join("\n") });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
files.push({ path: "index.md", content: buildIndex(concepts, names) });
|
|
279
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
280
|
+
return files;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The bundle-root `index.md` (spec §8): one section per lexicon, each entry a
|
|
285
|
+
* bundle-relative link plus the concept's description — the spec's
|
|
286
|
+
* progressive-disclosure mechanism. Root indexes may carry `okf_version`
|
|
287
|
+
* frontmatter (spec §12), the only frontmatter an index is allowed.
|
|
288
|
+
*/
|
|
289
|
+
function buildIndex(concepts: Map<string, Concept>, names: string[]): string {
|
|
290
|
+
const byLexicon = new Map<string, Concept[]>();
|
|
291
|
+
for (const name of names) {
|
|
292
|
+
const concept = concepts.get(name)!;
|
|
293
|
+
if (!byLexicon.has(concept.lexicon)) byLexicon.set(concept.lexicon, []);
|
|
294
|
+
byLexicon.get(concept.lexicon)!.push(concept);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const lines: string[] = ["---", `okf_version: '${OKF_VERSION}'`, "---", ""];
|
|
298
|
+
const lexicons = [...byLexicon.keys()].sort((a, b) => a.localeCompare(b));
|
|
299
|
+
for (const lexicon of lexicons) {
|
|
300
|
+
lines.push(`# Lexicon: ${lexicon}`, "");
|
|
301
|
+
for (const concept of byLexicon.get(lexicon)!) {
|
|
302
|
+
lines.push(`* [${concept.name}](/${concept.path}) - ${lexicon} ${concept.kind} of type ${concept.type}`);
|
|
303
|
+
}
|
|
304
|
+
lines.push("");
|
|
305
|
+
}
|
|
306
|
+
if (lexicons.length === 0) lines.push("No entities discovered.", "");
|
|
307
|
+
return lines.join("\n");
|
|
308
|
+
}
|
package/src/reconcile.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* only part that drives I/O (through the provider's `Cycle` implementations).
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { GOVERNANCE_VERBS, type GovernanceVerb } from "./governance.js";
|
|
23
|
+
|
|
22
24
|
// ---------------------------------------------------------------------------
|
|
23
25
|
// Change-set model
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
@@ -38,6 +40,12 @@ export interface ChangeSetEntry {
|
|
|
38
40
|
kind: ChangeKind;
|
|
39
41
|
/** High-level resource category (e.g. "team", "member", "branch-protection"). */
|
|
40
42
|
resourceType: string;
|
|
43
|
+
/**
|
|
44
|
+
* Cross-provider governance category (#790). `resourceType` stays the
|
|
45
|
+
* provider-specific display string; the verb is the shared grammar SCM and
|
|
46
|
+
* cloud plans group by. Stamped by `runReconcile` from the cycle's `verb`.
|
|
47
|
+
*/
|
|
48
|
+
verb?: GovernanceVerb;
|
|
41
49
|
/**
|
|
42
50
|
* Unique key identifying this resource within its type.
|
|
43
51
|
* - For top-level resources: a single name (team slug, member login, …).
|
|
@@ -211,8 +219,17 @@ export function renderChangeSet(cs: ChangeSet): string {
|
|
|
211
219
|
|
|
212
220
|
const ORDER: ChangeKind[] = ["create", "update", "delete"];
|
|
213
221
|
for (const kind of ORDER) {
|
|
214
|
-
|
|
222
|
+
let group = byKind[kind];
|
|
215
223
|
if (group.length === 0) continue;
|
|
224
|
+
// Verb-aware grouping (#790): when entries carry governance verbs, order
|
|
225
|
+
// each section by verb (vocabulary order, unverbed entries last) so mixed
|
|
226
|
+
// plans read category-by-category. Stable, and line format is unchanged —
|
|
227
|
+
// a verbless change set renders exactly as before.
|
|
228
|
+
if (group.some((e) => e.verb)) {
|
|
229
|
+
const rank = (e: ChangeSetEntry): number =>
|
|
230
|
+
e.verb ? GOVERNANCE_VERBS.indexOf(e.verb) : GOVERNANCE_VERBS.length;
|
|
231
|
+
group = [...group].sort((a, b) => rank(a) - rank(b));
|
|
232
|
+
}
|
|
216
233
|
lines.push(`\n${kind.toUpperCase()}:`);
|
|
217
234
|
for (const e of group) {
|
|
218
235
|
lines.push(` [${e.resourceType}] ${e.key}`);
|
|
@@ -399,6 +416,12 @@ class MutableRateBudget implements RateBudget {
|
|
|
399
416
|
export interface Cycle<TClient, TConfig, TLive, TScope = unknown> {
|
|
400
417
|
/** Human-readable name, e.g. "branch-protection". */
|
|
401
418
|
name: string;
|
|
419
|
+
/**
|
|
420
|
+
* Cross-provider governance category this cycle reconciles (#790). Every
|
|
421
|
+
* SCM warden cycle stamps one; cloud cycles (epic #787 C2) must. Optional
|
|
422
|
+
* only so provider-external Cycle implementations don't break.
|
|
423
|
+
*/
|
|
424
|
+
verb?: GovernanceVerb;
|
|
402
425
|
fetchLive(client: TClient, scopeId: string, scope: TScope, budget: RateBudget): Promise<TLive>;
|
|
403
426
|
buildDesired(config: TConfig, scopeId: string, scope: TScope): TConfig;
|
|
404
427
|
apply(
|
|
@@ -413,6 +436,8 @@ export interface Cycle<TClient, TConfig, TLive, TScope = unknown> {
|
|
|
413
436
|
/** Per-cycle outcome recorded in the run result. */
|
|
414
437
|
export interface CycleResult {
|
|
415
438
|
name: string;
|
|
439
|
+
/** The cycle's governance verb (#790), when it stamps one. */
|
|
440
|
+
verb?: GovernanceVerb;
|
|
416
441
|
/** Scope id this result is for (e.g. an org login). */
|
|
417
442
|
org: string;
|
|
418
443
|
counts: { create: number; update: number; delete: number };
|
|
@@ -535,6 +560,11 @@ export async function runReconcile<TClient, TConfig, TLive, TScope = unknown>(
|
|
|
535
560
|
}
|
|
536
561
|
|
|
537
562
|
const changeSet = diffFn(scopeId, desired, live, diffOptions);
|
|
563
|
+
// Stamp the cycle's governance verb (#790) onto entries that don't
|
|
564
|
+
// carry one, so provider diffs stay verb-unaware.
|
|
565
|
+
if (cycle.verb) {
|
|
566
|
+
for (const e of changeSet.entries) e.verb ??= cycle.verb;
|
|
567
|
+
}
|
|
538
568
|
const guardrailResult = guardrails(changeSet, live);
|
|
539
569
|
|
|
540
570
|
const counts = { create: 0, update: 0, delete: 0 };
|
|
@@ -542,6 +572,7 @@ export async function runReconcile<TClient, TConfig, TLive, TScope = unknown>(
|
|
|
542
572
|
|
|
543
573
|
const cycleResult: CycleResult = {
|
|
544
574
|
name: cycle.name,
|
|
575
|
+
verb: cycle.verb,
|
|
545
576
|
org: scopeId,
|
|
546
577
|
counts,
|
|
547
578
|
guardrails: guardrailResult,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { buildGraph, collectExpressions, type ExpressionRefs } from "../graph";
|
|
2
|
+
import type { Hcl2JsonTree, TfGraph } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Test-only stand-in for the hcl2json expression AST (`parse.ts`'s
|
|
6
|
+
* `resolveExpressionRefs`). Fixture expressions are simple `${<accessor>}`
|
|
7
|
+
* templates, so each one's accessor list is just its interpolation bodies.
|
|
8
|
+
* Production never takes this path — `parseTerraformDir` resolves every
|
|
9
|
+
* expression through `getReferencesInExpression`.
|
|
10
|
+
*/
|
|
11
|
+
export function fixtureExprRefs(tree: Hcl2JsonTree): ExpressionRefs {
|
|
12
|
+
const refs = new Map<string, string[]>();
|
|
13
|
+
for (const expr of collectExpressions(tree)) {
|
|
14
|
+
refs.set(
|
|
15
|
+
expr,
|
|
16
|
+
[...expr.matchAll(/\$\{([^}]+)\}/g)].map((m) => m[1].trim()),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return refs;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `buildGraph` over a hand-written fixture tree, accessors derived per {@link fixtureExprRefs}. */
|
|
23
|
+
export function buildFixtureGraph(tree: Hcl2JsonTree): TfGraph {
|
|
24
|
+
return buildGraph(tree, fixtureExprRefs(tree));
|
|
25
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { adoptFromState, canAdoptFromState, supportedStateAdoptionTypes } from "./adopt-state";
|
|
2
|
+
import { adoptFromState, canAdoptFromState, supportedStateAdoptionTypes, type DeferredParam } from "./adopt-state";
|
|
3
3
|
import type { StateResource } from "./state";
|
|
4
4
|
|
|
5
5
|
describe("adoptFromState", () => {
|
|
@@ -45,6 +45,39 @@ describe("adoptFromState", () => {
|
|
|
45
45
|
expect(out.content).toContain("RetentionInDays: 30");
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
test("a deferred input renders as a params reference, not the state literal (#998)", () => {
|
|
49
|
+
const subnet: StateResource = {
|
|
50
|
+
type: "aws_subnet",
|
|
51
|
+
name: "a",
|
|
52
|
+
attributes: { id: "subnet-0aa", vpc_id: "vpc-0abc", cidr_block: "10.0.1.0/24" },
|
|
53
|
+
};
|
|
54
|
+
const params: DeferredParam[] = [
|
|
55
|
+
{ name: "vpc_id", tfAttr: "vpc_id", survivor: "aws_vpc.main", attrs: ["id"], default: "vpc-0abc" },
|
|
56
|
+
];
|
|
57
|
+
const out = adoptFromState(subnet, params)!;
|
|
58
|
+
expect(out.parameterized).toEqual(["vpc_id"]);
|
|
59
|
+
expect(out.content).toContain('import { params } from "@intentius/chant/params";');
|
|
60
|
+
expect(out.content).toContain("VpcId: params.vpc_id as string,");
|
|
61
|
+
expect(out.content).not.toContain('VpcId: "vpc-0abc"');
|
|
62
|
+
// Non-deferred props keep their state literals.
|
|
63
|
+
expect(out.content).toContain('CidrBlock: "10.0.1.0/24"');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a deferred input on an unmapped attribute leaves the source alone", () => {
|
|
67
|
+
const lambda: StateResource = {
|
|
68
|
+
type: "aws_lambda_function",
|
|
69
|
+
name: "api",
|
|
70
|
+
attributes: { id: "myapp-api", function_name: "myapp-api", environment: [{ variables: { B: "b" } }] },
|
|
71
|
+
};
|
|
72
|
+
const params: DeferredParam[] = [
|
|
73
|
+
{ name: "environment", tfAttr: "environment", survivor: "aws_s3_bucket.assets", attrs: ["bucket"] },
|
|
74
|
+
];
|
|
75
|
+
const out = adoptFromState(lambda, params)!;
|
|
76
|
+
expect(out.parameterized).toEqual([]);
|
|
77
|
+
expect(out.content).not.toContain("@intentius/chant/params");
|
|
78
|
+
expect(out.content).toContain('FunctionName: "myapp-api"');
|
|
79
|
+
});
|
|
80
|
+
|
|
48
81
|
test("canAdoptFromState gates on a known native constructor", () => {
|
|
49
82
|
expect(canAdoptFromState("aws_s3_bucket")).toBe(true);
|
|
50
83
|
expect(canAdoptFromState("random_pet")).toBe(false);
|