@intentius/chant-lexicon-gcp 0.0.16 → 0.0.22

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.
Files changed (48) hide show
  1. package/dist/integrity.json +8 -4
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +18141 -0
  4. package/dist/rules/schema-registry.ts +91 -0
  5. package/dist/rules/wgc401.ts +59 -0
  6. package/dist/rules/wgc402.ts +54 -0
  7. package/dist/rules/wgc403.ts +84 -0
  8. package/dist/skills/chant-gcp.md +4 -1
  9. package/package.json +20 -2
  10. package/src/codegen/docs.test.ts +16 -0
  11. package/src/codegen/docs.ts +3 -0
  12. package/src/codegen/generate.test.ts +18 -0
  13. package/src/codegen/generate.ts +11 -0
  14. package/src/codegen/package.test.ts +16 -0
  15. package/src/generated/lexicon-gcp.json +18141 -0
  16. package/src/import/import-fixtures.test.ts +98 -0
  17. package/src/lint/post-synth/gcp-helpers.test.ts +166 -0
  18. package/src/lint/post-synth/post-synth.test.ts +130 -0
  19. package/src/lint/post-synth/schema-registry.ts +91 -0
  20. package/src/lint/post-synth/wgc101.test.ts +39 -0
  21. package/src/lint/post-synth/wgc102.test.ts +38 -0
  22. package/src/lint/post-synth/wgc103.test.ts +38 -0
  23. package/src/lint/post-synth/wgc104.test.ts +37 -0
  24. package/src/lint/post-synth/wgc105.test.ts +46 -0
  25. package/src/lint/post-synth/wgc106.test.ts +38 -0
  26. package/src/lint/post-synth/wgc107.test.ts +38 -0
  27. package/src/lint/post-synth/wgc108.test.ts +42 -0
  28. package/src/lint/post-synth/wgc109.test.ts +46 -0
  29. package/src/lint/post-synth/wgc110.test.ts +37 -0
  30. package/src/lint/post-synth/wgc111.test.ts +46 -0
  31. package/src/lint/post-synth/wgc112.test.ts +48 -0
  32. package/src/lint/post-synth/wgc113.test.ts +36 -0
  33. package/src/lint/post-synth/wgc201.test.ts +38 -0
  34. package/src/lint/post-synth/wgc202.test.ts +38 -0
  35. package/src/lint/post-synth/wgc203.test.ts +45 -0
  36. package/src/lint/post-synth/wgc204.test.ts +42 -0
  37. package/src/lint/post-synth/wgc301.test.ts +39 -0
  38. package/src/lint/post-synth/wgc302.test.ts +36 -0
  39. package/src/lint/post-synth/wgc303.test.ts +37 -0
  40. package/src/lint/post-synth/wgc401.test.ts +46 -0
  41. package/src/lint/post-synth/wgc401.ts +59 -0
  42. package/src/lint/post-synth/wgc402.test.ts +40 -0
  43. package/src/lint/post-synth/wgc402.ts +54 -0
  44. package/src/lint/post-synth/wgc403.test.ts +59 -0
  45. package/src/lint/post-synth/wgc403.ts +84 -0
  46. package/src/plugin.test.ts +4 -1
  47. package/src/plugin.ts +172 -3
  48. package/src/skills/chant-gcp.md +4 -1
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Schema registry for GCP Config Connector resources.
3
+ *
4
+ * Loads lexicon-gcp.json and builds a lookup from CRD kind to field schema,
5
+ * used by post-synth rules to validate YAML against known CRD schemas.
6
+ */
7
+
8
+ import { createRequire } from "module";
9
+ const require = createRequire(import.meta.url);
10
+
11
+ export interface FieldSchema {
12
+ type: string;
13
+ required: boolean;
14
+ enum?: string[];
15
+ ref?: boolean;
16
+ }
17
+
18
+ export interface ResourceSchema {
19
+ fields: Record<string, FieldSchema>;
20
+ required: string[];
21
+ }
22
+
23
+ interface LexiconEntry {
24
+ kind: "resource" | "property";
25
+ gvkKind?: string;
26
+ schema?: ResourceSchema;
27
+ }
28
+
29
+ let cachedRegistry: Map<string, ResourceSchema> | null = null;
30
+
31
+ /**
32
+ * Get the schema registry: Map<gvkKind, ResourceSchema>.
33
+ */
34
+ export function getSchemaRegistry(): Map<string, ResourceSchema> {
35
+ if (cachedRegistry) return cachedRegistry;
36
+
37
+ cachedRegistry = new Map();
38
+ try {
39
+ const lexicon = require("../../generated/lexicon-gcp.json") as Record<string, LexiconEntry>;
40
+ for (const entry of Object.values(lexicon)) {
41
+ if (entry.kind === "resource" && entry.gvkKind && entry.schema) {
42
+ cachedRegistry.set(entry.gvkKind, entry.schema);
43
+ }
44
+ }
45
+ } catch {
46
+ // Lexicon JSON not yet generated — return empty registry
47
+ }
48
+
49
+ return cachedRegistry;
50
+ }
51
+
52
+ /**
53
+ * Levenshtein distance between two strings.
54
+ */
55
+ export function levenshtein(a: string, b: string): number {
56
+ const m = a.length;
57
+ const n = b.length;
58
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
59
+
60
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
61
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
62
+
63
+ for (let i = 1; i <= m; i++) {
64
+ for (let j = 1; j <= n; j++) {
65
+ dp[i][j] = a[i - 1] === b[j - 1]
66
+ ? dp[i - 1][j - 1]
67
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
68
+ }
69
+ }
70
+
71
+ return dp[m][n];
72
+ }
73
+
74
+ /**
75
+ * Find the closest field name by Levenshtein distance.
76
+ * Returns the suggestion if distance ≤ 3, otherwise undefined.
77
+ */
78
+ export function suggestField(unknown: string, knownFields: string[]): string | undefined {
79
+ let best: string | undefined;
80
+ let bestDist = 4; // threshold
81
+
82
+ for (const field of knownFields) {
83
+ const dist = levenshtein(unknown.toLowerCase(), field.toLowerCase());
84
+ if (dist < bestDist) {
85
+ bestDist = dist;
86
+ best = field;
87
+ }
88
+ }
89
+
90
+ return best;
91
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * WGC401: Unknown spec field
3
+ *
4
+ * Flags fields in a Config Connector resource spec that are not defined
5
+ * in the CRD schema. Includes "did you mean?" suggestion via Levenshtein.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { parseGcpManifests, isConfigConnectorResource, getResourceName, getSpec } from "./gcp-helpers";
10
+ import { getSchemaRegistry, suggestField } from "./schema-registry";
11
+
12
+ export const wgc401: PostSynthCheck = {
13
+ id: "WGC401",
14
+ description: "Config Connector resource spec contains unknown field",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+ const registry = getSchemaRegistry();
19
+
20
+ for (const [_lexicon, output] of ctx.outputs) {
21
+ if (typeof output !== "string") continue;
22
+
23
+ const manifests = parseGcpManifests(output);
24
+
25
+ for (const manifest of manifests) {
26
+ if (!isConfigConnectorResource(manifest)) continue;
27
+
28
+ const kind = manifest.kind;
29
+ if (!kind) continue;
30
+
31
+ const schema = registry.get(kind);
32
+ if (!schema) continue; // No schema available for this kind
33
+
34
+ const spec = getSpec(manifest);
35
+ if (!spec) continue;
36
+
37
+ const knownFields = Object.keys(schema.fields);
38
+ const resourceName = getResourceName(manifest);
39
+
40
+ for (const field of Object.keys(spec)) {
41
+ if (field in schema.fields) continue;
42
+
43
+ const suggestion = suggestField(field, knownFields);
44
+ const hint = suggestion ? ` — did you mean "${suggestion}"?` : "";
45
+
46
+ diagnostics.push({
47
+ checkId: "WGC401",
48
+ severity: "error",
49
+ message: `Resource "${resourceName}" (${kind}): unknown spec field "${field}"${hint}`,
50
+ entity: resourceName,
51
+ lexicon: "gcp",
52
+ });
53
+ }
54
+ }
55
+ }
56
+
57
+ return diagnostics;
58
+ },
59
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * WGC402: Missing required spec field
3
+ *
4
+ * Flags Config Connector resources that are missing required fields
5
+ * according to their CRD schema.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { parseGcpManifests, isConfigConnectorResource, getResourceName, getSpec } from "./gcp-helpers";
10
+ import { getSchemaRegistry } from "./schema-registry";
11
+
12
+ export const wgc402: PostSynthCheck = {
13
+ id: "WGC402",
14
+ description: "Config Connector resource is missing a required spec field",
15
+
16
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+ const registry = getSchemaRegistry();
19
+
20
+ for (const [_lexicon, output] of ctx.outputs) {
21
+ if (typeof output !== "string") continue;
22
+
23
+ const manifests = parseGcpManifests(output);
24
+
25
+ for (const manifest of manifests) {
26
+ if (!isConfigConnectorResource(manifest)) continue;
27
+
28
+ const kind = manifest.kind;
29
+ if (!kind) continue;
30
+
31
+ const schema = registry.get(kind);
32
+ if (!schema) continue;
33
+
34
+ const spec = getSpec(manifest);
35
+ const specKeys = new Set(spec ? Object.keys(spec) : []);
36
+ const resourceName = getResourceName(manifest);
37
+
38
+ for (const requiredField of schema.required) {
39
+ if (!specKeys.has(requiredField)) {
40
+ diagnostics.push({
41
+ checkId: "WGC402",
42
+ severity: "error",
43
+ message: `Resource "${resourceName}" (${kind}): missing required field "${requiredField}"`,
44
+ entity: resourceName,
45
+ lexicon: "gcp",
46
+ });
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ return diagnostics;
53
+ },
54
+ };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * WGC403: Type/structure mismatch in spec field
3
+ *
4
+ * Flags spec fields where the value type doesn't match the CRD schema:
5
+ * - String where number expected (e.g. availableMemoryMb: "512")
6
+ * - Scalar string where resourceRef object expected (e.g. topicRef: "name")
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { parseGcpManifests, isConfigConnectorResource, getResourceName, getSpec } from "./gcp-helpers";
11
+ import { getSchemaRegistry } from "./schema-registry";
12
+
13
+ export const wgc403: PostSynthCheck = {
14
+ id: "WGC403",
15
+ description: "Config Connector resource spec field has wrong type or structure",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+ const registry = getSchemaRegistry();
20
+
21
+ for (const [_lexicon, output] of ctx.outputs) {
22
+ if (typeof output !== "string") continue;
23
+
24
+ const manifests = parseGcpManifests(output);
25
+
26
+ for (const manifest of manifests) {
27
+ if (!isConfigConnectorResource(manifest)) continue;
28
+
29
+ const kind = manifest.kind;
30
+ if (!kind) continue;
31
+
32
+ const schema = registry.get(kind);
33
+ if (!schema) continue;
34
+
35
+ const spec = getSpec(manifest);
36
+ if (!spec) continue;
37
+
38
+ const resourceName = getResourceName(manifest);
39
+
40
+ for (const [field, value] of Object.entries(spec)) {
41
+ const fieldSchema = schema.fields[field];
42
+ if (!fieldSchema) continue; // Unknown fields handled by WGC401
43
+
44
+ // Check: resourceRef field expects an object, got a scalar
45
+ if (fieldSchema.ref && typeof value === "string") {
46
+ diagnostics.push({
47
+ checkId: "WGC403",
48
+ severity: "error",
49
+ message: `Resource "${resourceName}" (${kind}): field "${field}" expects a resourceRef object (e.g. { name, kind }), got string "${value}"`,
50
+ entity: resourceName,
51
+ lexicon: "gcp",
52
+ });
53
+ continue;
54
+ }
55
+
56
+ // Check: number field got a string that looks numeric
57
+ if (fieldSchema.type === "number" && typeof value === "string") {
58
+ diagnostics.push({
59
+ checkId: "WGC403",
60
+ severity: "error",
61
+ message: `Resource "${resourceName}" (${kind}): field "${field}" expects a number, got string "${value}"`,
62
+ entity: resourceName,
63
+ lexicon: "gcp",
64
+ });
65
+ continue;
66
+ }
67
+
68
+ // Check: boolean field got a string
69
+ if (fieldSchema.type === "boolean" && typeof value === "string") {
70
+ diagnostics.push({
71
+ checkId: "WGC403",
72
+ severity: "error",
73
+ message: `Resource "${resourceName}" (${kind}): field "${field}" expects a boolean, got string "${value}"`,
74
+ entity: resourceName,
75
+ lexicon: "gcp",
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ return diagnostics;
83
+ },
84
+ };
@@ -9,7 +9,7 @@ user-invocable: true
9
9
 
10
10
  ## How chant and Config Connector relate
11
11
 
12
- chant is a **synthesis-only** tool — it compiles TypeScript source files into Config Connector YAML manifests. chant does NOT call GCP APIs. Your job as an agent is to bridge the two:
12
+ chant is a **synthesis compiler** — it compiles TypeScript source files into Config Connector YAML manifests. `chant build` does not call GCP APIs; synthesis is pure and deterministic. Config Connector resources are Kubernetes objects, so the optional `chant state snapshot` command queries the Kubernetes API to capture deployment metadata (resource status, conditions, observed state) for observability. Your job as an agent is to bridge synthesis and deployment:
13
13
 
14
14
  - Use **chant** for: build, lint, diff (local YAML comparison)
15
15
  - Use **kubectl** for: apply, rollback, monitoring, troubleshooting
@@ -49,6 +49,9 @@ chant lint src/
49
49
  # Build
50
50
  chant build src/ --output manifests.yaml
51
51
 
52
+ # See what changed since last deploy (compares current build against last snapshot's digest)
53
+ chant state diff staging gcp
54
+
52
55
  # Dry run
53
56
  kubectl apply -f manifests.yaml --dry-run=server
54
57
 
package/package.json CHANGED
@@ -1,7 +1,25 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gcp",
3
- "version": "0.0.16",
3
+ "version": "0.0.22",
4
+ "description": "Google Cloud lexicon for chant — declarative IaC in TypeScript",
4
5
  "license": "Apache-2.0",
6
+ "homepage": "https://intentius.io/chant",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/intentius/chant.git",
10
+ "directory": "lexicons/gcp"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/intentius/chant/issues"
14
+ },
15
+ "keywords": [
16
+ "infrastructure-as-code",
17
+ "iac",
18
+ "typescript",
19
+ "gcp",
20
+ "google-cloud",
21
+ "chant"
22
+ ],
5
23
  "type": "module",
6
24
  "files": [
7
25
  "src/",
@@ -25,7 +43,7 @@
25
43
  "prepack": "bun run generate && bun run bundle && bun run validate"
26
44
  },
27
45
  "dependencies": {
28
- "@intentius/chant": "0.0.15",
46
+ "@intentius/chant": "0.0.22",
29
47
  "fflate": "^0.8.2",
30
48
  "js-yaml": "^4.1.0"
31
49
  },
@@ -0,0 +1,16 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generateDocs } from "./docs";
3
+
4
+ describe("generateDocs", () => {
5
+ test("generateDocs function exists and is callable", () => {
6
+ expect(typeof generateDocs).toBe("function");
7
+ });
8
+
9
+ test("generateDocs returns a promise", () => {
10
+ const result = generateDocs({ verbose: false });
11
+ expect(result).toBeDefined();
12
+ expect(typeof result.then).toBe("function");
13
+ // Suppress unhandled rejection if it fails due to missing data
14
+ result.catch(() => {});
15
+ });
16
+ });
@@ -807,6 +807,9 @@ The \`chant-gcp\` skill covers:
807
807
  | \`examples/basic-bucket\` | Example StorageBucket code |`,
808
808
  },
809
809
  ],
810
+ sidebarExtra: [
811
+ { label: "Deploying to GKE", slug: "gke-kubernetes" },
812
+ ],
810
813
  };
811
814
 
812
815
  const result = docsPipeline(config);
@@ -0,0 +1,18 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generate } from "./generate";
3
+
4
+ describe("generate", () => {
5
+ test("generate function exists and is callable", () => {
6
+ expect(typeof generate).toBe("function");
7
+ });
8
+
9
+ test("generate returns a promise", { timeout: 15000 }, () => {
10
+ // Call with no-op options to verify it returns a promise-like object.
11
+ // We don't await because it may require network/file access.
12
+ const result = generate({ dryRun: true });
13
+ expect(result).toBeDefined();
14
+ expect(typeof result.then).toBe("function");
15
+ // Suppress unhandled rejection if it fails due to missing spec
16
+ result.catch(() => {});
17
+ });
18
+ });
@@ -113,6 +113,17 @@ function generateLexiconJSON(results: GcpParseResult[], naming: NamingStrategy):
113
113
  gvkKind: r.gvk.kind,
114
114
  group: r.gvk.group,
115
115
  properties: r.resource.properties.length,
116
+ schema: {
117
+ fields: Object.fromEntries(
118
+ r.resource.properties.map((p) => [p.name, {
119
+ type: p.tsType,
120
+ required: p.required,
121
+ ...(p.enum && { enum: p.enum }),
122
+ ...(p.isResourceRef && { ref: true }),
123
+ }]),
124
+ ),
125
+ required: r.resource.properties.filter((p) => p.required).map((p) => p.name),
126
+ },
116
127
  attrs: Object.fromEntries(
117
128
  r.resource.attributes.map((a) => [a.name, a.tsType]),
118
129
  ),
@@ -0,0 +1,16 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { packageLexicon } from "./package";
3
+
4
+ describe("packageLexicon", () => {
5
+ test("packageLexicon function exists and is callable", () => {
6
+ expect(typeof packageLexicon).toBe("function");
7
+ });
8
+
9
+ test("packageLexicon returns a promise", () => {
10
+ const result = packageLexicon({ verbose: false });
11
+ expect(result).toBeDefined();
12
+ expect(typeof result.then).toBe("function");
13
+ // Suppress unhandled rejection if it fails due to missing artifacts
14
+ result.catch(() => {});
15
+ });
16
+ });