@intentius/chant-lexicon-cpln 0.124.31 → 0.124.32

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 (75) hide show
  1. package/dist/api.js +88 -0
  2. package/dist/codegen/docs-cli.js +6 -0
  3. package/dist/codegen/docs.js +85 -0
  4. package/dist/codegen/generate-cli.js +25 -0
  5. package/dist/codegen/generate-lexicon.js +50 -0
  6. package/dist/codegen/generate-typescript.js +48 -0
  7. package/dist/codegen/generate.js +86 -0
  8. package/dist/codegen/naming.js +43 -0
  9. package/dist/codegen/package.js +31 -0
  10. package/dist/composites/catalog.js +556 -0
  11. package/dist/composites/cron-job.js +52 -0
  12. package/dist/composites/gvc-environment.js +49 -0
  13. package/dist/composites/index.js +6 -0
  14. package/dist/composites/public-domain.js +70 -0
  15. package/dist/composites/secret-access.js +82 -0
  16. package/dist/composites/serverless-service.js +58 -0
  17. package/dist/composites/stateful-service.js +87 -0
  18. package/dist/coverage.js +104 -0
  19. package/dist/describe-resources.js +264 -0
  20. package/dist/detect.js +52 -0
  21. package/dist/entity-props.js +100 -0
  22. package/dist/generated/index.js +169 -0
  23. package/dist/generated/runtime.js +4 -0
  24. package/dist/index.js +14 -0
  25. package/dist/init-templates.js +125 -0
  26. package/dist/integrity.json +2 -2
  27. package/dist/kinds.js +134 -0
  28. package/dist/lint/audit-catalog.js +90 -0
  29. package/dist/lint/audit-lineage.js +32 -0
  30. package/dist/lint/post-synth/cpl010-unrestricted-outbound.js +34 -0
  31. package/dist/lint/post-synth/cpl011-internal-firewall-scope.js +32 -0
  32. package/dist/lint/post-synth/cpl012-no-literal-credential-env.js +51 -0
  33. package/dist/lint/post-synth/cpl013-qualified-identity-principal.js +40 -0
  34. package/dist/lint/post-synth/cpl014-qualified-secret-ref.js +47 -0
  35. package/dist/lint/post-synth/cpl020-serverless-single-port.js +51 -0
  36. package/dist/lint/post-synth/cpl021-cron-job-shape.js +55 -0
  37. package/dist/lint/post-synth/cpl022-container-ports.js +53 -0
  38. package/dist/lint/post-synth/cpl023-container-resources.js +73 -0
  39. package/dist/lint/post-synth/cpl024-probe-handler.js +43 -0
  40. package/dist/lint/post-synth/cpl025-autoscaling-shape.js +84 -0
  41. package/dist/lint/post-synth/cpl026-scale-to-zero.js +59 -0
  42. package/dist/lint/post-synth/cpl027-capacity-ai-conflict.js +67 -0
  43. package/dist/lint/post-synth/cpl028-volume-set.js +155 -0
  44. package/dist/lint/post-synth/cpl029-link-targets.js +140 -0
  45. package/dist/lint/post-synth/cpl030-domain-routing.js +114 -0
  46. package/dist/lint/post-synth/cpl040-pinned-image.js +41 -0
  47. package/dist/lint/post-synth/cpl041-image-reference-form.js +52 -0
  48. package/dist/lint/post-synth/cpl042-gvc-placement.js +49 -0
  49. package/dist/lint/post-synth/cpl043-policy-scope.js +0 -0
  50. package/dist/lint/post-synth/helpers.js +102 -0
  51. package/dist/lint/post-synth/index.js +42 -0
  52. package/dist/lint/rules/cpl001-no-secret-literals.js +73 -0
  53. package/dist/lint/rules/cpl002-prefer-resource-reference.js +60 -0
  54. package/dist/lint/rules/index.js +6 -0
  55. package/dist/lsp/completions.d.ts.map +1 -1
  56. package/dist/lsp/completions.js +24 -0
  57. package/dist/lsp/hover.d.ts.map +1 -1
  58. package/dist/lsp/hover.js +106 -0
  59. package/dist/manifest.json +1 -1
  60. package/dist/ownership.js +25 -0
  61. package/dist/package-cli.js +21 -0
  62. package/dist/plugin.js +141 -0
  63. package/dist/reference-catalog.js +99 -0
  64. package/dist/serializer.js +217 -0
  65. package/dist/spec/fetch.d.ts.map +1 -1
  66. package/dist/spec/fetch.js +70 -0
  67. package/dist/spec/parse.js +338 -0
  68. package/dist/spec/snapshot-cli.js +60 -0
  69. package/dist/validate-cli.js +7 -0
  70. package/dist/validate.js +88 -0
  71. package/package.json +7 -7
  72. package/src/codegen/package.ts +2 -2
  73. package/src/lsp/completions.ts +2 -1
  74. package/src/lsp/hover.ts +2 -1
  75. package/src/spec/fetch.ts +2 -2
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Control Plane Core API OpenAPI 3.0.3 parser.
3
+ *
4
+ * The cpln spec is one document with a schema per kind. Unlike the k8s swagger
5
+ * or ARM, it is mostly *inline*: `workload.spec.containers[].readinessProbe`
6
+ * is nested object literals all the way down, with only a handful of named
7
+ * `$ref` schemas (`query`, `policy_binding`, `secret_opaque`, …). So there is
8
+ * no upstream definition table to lift property types out of — this parser
9
+ * synthesizes them from the tree, which raises two questions the naming rules
10
+ * below answer: what to call them, and where to stop.
11
+ *
12
+ * ## Naming
13
+ *
14
+ * A `$ref`'d schema keeps its upstream name (`query` → `Query`), so a shape
15
+ * shared by two kinds is emitted once and both reference it. An inline object
16
+ * is named by its path under the nearest named ancestor
17
+ * (`workload.spec.containers[].readinessProbe` → `WorkloadSpecContainersReadinessProbe`).
18
+ * Names are long, but they are derived rather than chosen, so two runs of the
19
+ * generator over the same spec agree, and a property that moves in the spec
20
+ * produces a visible rename rather than a silent re-binding.
21
+ *
22
+ * Arrays do not contribute a segment: an array of objects and a single object
23
+ * yield the same class, because `containers[]` and `containers` describe the
24
+ * same shape.
25
+ *
26
+ * ## Where it stops
27
+ *
28
+ * One subtree is deliberately untyped: `spec.sidecar.envoy` on both `gvc` and
29
+ * `workload`. It is a raw Envoy bootstrap fragment that Control Plane passes
30
+ * through verbatim — `typed_config`, `google_grpc` channel credentials, the
31
+ * lot — and expanding it costs 81 property-type classes on `gvc` alone, more
32
+ * than four times every other shape in this lexicon combined, to type a field
33
+ * whose contents are Envoy's contract and not Control Plane's. It resolves to
34
+ * `Record<string, unknown>`; see {@link LOOSENED_PATHS}.
35
+ *
36
+ * Everything else is typed in full. That comes to 137 property types across
37
+ * the eight kinds, with a natural maximum depth of 6.
38
+ */
39
+ import { extractConstraints as coreExtractConstraints, primaryType, } from "@intentius/chant/codegen/json-schema";
40
+ import { KINDS, NAMESPACE, SERVICE } from "../kinds.js";
41
+ const REF_PREFIX = "#/components/schemas/";
42
+ /** Backstop against a self-referential `$ref` the spec does not currently have. */
43
+ const MAX_DEPTH = 16;
44
+ // ── Curation ───────────────────────────────────────────────────────
45
+ /**
46
+ * Property paths that resolve to `Record<string, unknown>` instead of being
47
+ * expanded. Keyed `<kind>:<dotted path>`; see the module comment for why.
48
+ */
49
+ const LOOSENED_PATHS = new Set(["gvc:spec.sidecar.envoy", "workload:spec.sidecar.envoy"]);
50
+ /**
51
+ * Types the spec states too loosely to author against, keyed `<kind>:<path>`.
52
+ *
53
+ * `tags` is declared as a bare `{"type": "object"}` on all eight kinds, which
54
+ * resolves to `Record<string, unknown>` — technically true and useless in
55
+ * practice, since Control Plane stores tag values as strings and an object
56
+ * value is rejected at apply time. Narrowing it here turns a runtime rejection
57
+ * into a compile error, and matters more than usual for this lexicon because
58
+ * `tags` is also where the ownership marker is stamped.
59
+ */
60
+ const TYPE_OVERRIDES = new Map(KINDS.map((k) => [`${k.kind}:tags`, "Record<string, string>"]));
61
+ /**
62
+ * Top-level properties treated as read-only attributes on every kind. These
63
+ * are the envelope the API stamps on a resource, not authoring surface.
64
+ */
65
+ const ENVELOPE_ATTRIBUTES = new Set(["id", "kind", "version", "created", "lastModified", "links", "alias", "status"]);
66
+ /**
67
+ * Attribute names core's runtime already owns on every `Declarable`.
68
+ *
69
+ * `createResource` installs `lexicon`, `entityType`, `kind`, `props`,
70
+ * `attributes` and `Ref` as non-configurable instance properties, then defines
71
+ * an `AttrRef` for each declared attribute. An attribute colliding with one of
72
+ * those throws `TypeError: Cannot redefine property` in the constructor — not
73
+ * at generate time, at the moment a user writes `new Gvc({...})`.
74
+ *
75
+ * cpln collides on exactly one: every kind carries a read-only `kind`
76
+ * discriminator. Dropping it costs nothing, because the chant type already
77
+ * says which kind a resource is and the serializer emits `kind:` from the
78
+ * kinds table rather than from the resource. The rest of the set is guarded so
79
+ * a future upstream field cannot reintroduce the same failure quietly.
80
+ */
81
+ const RESERVED_ATTRIBUTE_NAMES = new Set(["lexicon", "entityType", "kind", "props", "attributes", "Ref"]);
82
+ /**
83
+ * Properties the spec fails to mark `readOnly` but which are server-computed.
84
+ *
85
+ * `workload.health` is a rollup of readiness across locations — the API
86
+ * populates it and rejects nothing if you send it, so it round-trips as
87
+ * writable and would show up as permanent drift on every workload if this
88
+ * lexicon declared it. It is an attribute here regardless of the spec.
89
+ */
90
+ const FORCED_ATTRIBUTES = new Set(["workload:health"]);
91
+ /**
92
+ * The spec marks `required: []` on every kind — nothing is required, including
93
+ * a resource's own name. That is true of a PATCH body and false of anything
94
+ * chant would synthesize, so `name` is required on every kind, and the two
95
+ * discriminators without which a manifest is meaningless are required on their
96
+ * own kind.
97
+ */
98
+ const FORCED_REQUIRED = new Set(["name", "secret:type", "policy:targetKind"]);
99
+ /**
100
+ * The `gvc` property every GVC-scoped kind carries.
101
+ *
102
+ * Synthetic only with respect to the OpenAPI document. A workload's GVC is a
103
+ * URL segment there (`/org/{org}/gvc/{gvc}/workload/{name}`) rather than a body
104
+ * field, so the `workload` schema has none; `identity` and `volumeset` do carry
105
+ * one, inconsistently — a string on the first, an object on the second.
106
+ *
107
+ * The `cpln apply` manifest format, which is what this lexicon actually emits,
108
+ * has a real top-level `gvc` key for exactly this purpose: "you can specify
109
+ * either a `gvc` property in the file or use the `--gvc` flag, but not both".
110
+ * So all three kinds get the same required `gvc: string`, it serializes
111
+ * straight through, and a manifest stays self-contained rather than depending
112
+ * on which flag the caller remembered. The Terraform provider makes the same
113
+ * call (`cpln_workload.gvc`).
114
+ */
115
+ const SYNTHETIC_GVC_PROP = {
116
+ name: "gvc",
117
+ tsType: "string",
118
+ required: true,
119
+ description: "Name of the GVC this resource belongs to.",
120
+ constraints: {},
121
+ };
122
+ // ── Parser ─────────────────────────────────────────────────────────
123
+ /**
124
+ * Parse the cpln OpenAPI spec into the modelled kinds and every property type
125
+ * reachable from them.
126
+ */
127
+ export function parseCplnOpenAPI(data) {
128
+ const spec = JSON.parse(typeof data === "string" ? data : data.toString("utf-8"));
129
+ const schemas = spec.components?.schemas ?? {};
130
+ // Synthesized property types, keyed by class name. Populated as a side effect
131
+ // of resolving each kind's properties; a shape reached twice is emitted once.
132
+ const propertyTypes = new Map();
133
+ const inProgress = new Set();
134
+ const results = [];
135
+ for (const kind of KINDS) {
136
+ const schema = schemas[kind.schema];
137
+ if (!schema) {
138
+ throw new Error(`cpln spec has no \`components.schemas.${kind.schema}\` backing kind \`${kind.kind}\` — ` +
139
+ `the upstream spec changed shape, or the snapshot is stale.`);
140
+ }
141
+ const properties = [];
142
+ const attributes = [];
143
+ for (const [name, prop] of Object.entries(schema.properties ?? {})) {
144
+ const isAttribute = prop.readOnly === true || ENVELOPE_ATTRIBUTES.has(name) || FORCED_ATTRIBUTES.has(`${kind.kind}:${name}`);
145
+ const ctx = { schemas, propertyTypes, inProgress, kind };
146
+ const tsType = resolveType(prop, ctx, kind.className + segment(name), `${kind.kind}:${name}`, 0, isAttribute);
147
+ if (isAttribute) {
148
+ if (!RESERVED_ATTRIBUTE_NAMES.has(name))
149
+ attributes.push({ name, tsType });
150
+ continue;
151
+ }
152
+ properties.push({
153
+ name,
154
+ tsType,
155
+ required: (schema.required ?? []).includes(name) || FORCED_REQUIRED.has(name) || FORCED_REQUIRED.has(`${kind.kind}:${name}`),
156
+ description: prop.description,
157
+ constraints: coreExtractConstraints(prop),
158
+ });
159
+ }
160
+ if (kind.gvcScoped) {
161
+ // Replace rather than append: `identity` and `volumeset` already declare
162
+ // a `gvc` of their own, in two different shapes. One normalized property
163
+ // beats three near-identical ones.
164
+ const existing = properties.findIndex((p) => p.name === "gvc");
165
+ if (existing >= 0)
166
+ properties.splice(existing, 1);
167
+ properties.push({ ...SYNTHETIC_GVC_PROP });
168
+ }
169
+ results.push({
170
+ resource: { typeName: kind.typeName, description: kind.summary, properties, attributes },
171
+ propertyTypes: [],
172
+ enums: [],
173
+ });
174
+ }
175
+ for (const [className, resource] of [...propertyTypes].sort(([a], [b]) => a.localeCompare(b))) {
176
+ results.push({
177
+ resource: { ...resource, typeName: `${NAMESPACE}::${SERVICE}::${className}` },
178
+ propertyTypes: [],
179
+ enums: [],
180
+ isProperty: true,
181
+ });
182
+ }
183
+ return results;
184
+ }
185
+ /**
186
+ * Resolve a schema node to a TypeScript type, emitting property-type classes
187
+ * for the object shapes it reaches.
188
+ *
189
+ * `className` is the name an inline object at this position would take;
190
+ * `path` is the `<kind>:<dotted path>` used to consult {@link LOOSENED_PATHS}.
191
+ * `readOnlyBranch` suppresses class emission under a read-only subtree —
192
+ * `status` trees are reported back as plain data, and typing them would double
193
+ * this lexicon's surface for shapes nobody authors.
194
+ */
195
+ function resolveType(node, ctx, className, path, depth, readOnlyBranch) {
196
+ if (!node || depth > MAX_DEPTH)
197
+ return "Record<string, unknown>";
198
+ if (LOOSENED_PATHS.has(path))
199
+ return "Record<string, unknown>";
200
+ const override = TYPE_OVERRIDES.get(path);
201
+ if (override)
202
+ return override;
203
+ // A `$ref` adopts the upstream schema's name, so a shape shared between two
204
+ // kinds is emitted once rather than once per path that reaches it.
205
+ if (node.$ref) {
206
+ const refName = refTarget(node.$ref);
207
+ if (!refName)
208
+ return "Record<string, unknown>";
209
+ const target = ctx.schemas[refName];
210
+ if (!target)
211
+ return "Record<string, unknown>";
212
+ return resolveType(target, ctx, schemaClassName(refName), path, depth + 1, readOnlyBranch);
213
+ }
214
+ // `allOf: [{ $ref }]` — the OpenAPI idiom for "typed as this schema".
215
+ if (node.allOf?.length) {
216
+ const withRef = node.allOf.find((s) => s.$ref);
217
+ if (withRef)
218
+ return resolveType(withRef, ctx, className, path, depth + 1, readOnlyBranch);
219
+ }
220
+ const union = node.oneOf ?? node.anyOf;
221
+ if (union?.length) {
222
+ // Named branches keep their type; inline object branches collapse to a map.
223
+ // The only union of consequence is `secret.data`, whose named branches are
224
+ // the six documented secret payloads and whose inline branches are
225
+ // provider-specific credential bags — a union of six anonymous
226
+ // `SecretDataVariant3`-style classes would be worse than one honest map.
227
+ const members = union.map((branch) => branch.$ref
228
+ ? resolveType(branch, ctx, className, path, depth + 1, readOnlyBranch)
229
+ : branch.properties
230
+ ? "Record<string, unknown>"
231
+ : resolveType(branch, ctx, className, path, depth + 1, readOnlyBranch));
232
+ return dedupeUnion(members);
233
+ }
234
+ if (node.enum?.length)
235
+ return enumUnion(node.enum);
236
+ const kind = primaryType(node.type);
237
+ if (kind === "array") {
238
+ // An array contributes no name segment: `containers[]` and `containers`
239
+ // describe the same shape.
240
+ const item = resolveType(node.items, ctx, className, path, depth + 1, readOnlyBranch);
241
+ return item.includes(" | ") ? `(${item})[]` : `${item}[]`;
242
+ }
243
+ if (kind === "object" || (!node.type && node.properties)) {
244
+ if (node.properties && Object.keys(node.properties).length > 0) {
245
+ if (readOnlyBranch)
246
+ return "Record<string, unknown>";
247
+ return emitPropertyType(node, ctx, className, path, depth);
248
+ }
249
+ if (node.additionalProperties && typeof node.additionalProperties === "object") {
250
+ const value = resolveType(node.additionalProperties, ctx, className, path, depth + 1, readOnlyBranch);
251
+ return `Record<string, ${value}>`;
252
+ }
253
+ return "Record<string, unknown>";
254
+ }
255
+ switch (kind) {
256
+ case "string":
257
+ return "string";
258
+ case "integer":
259
+ case "number":
260
+ return "number";
261
+ case "boolean":
262
+ return "boolean";
263
+ default:
264
+ return "Record<string, unknown>";
265
+ }
266
+ }
267
+ /**
268
+ * Register (or reuse) a property-type class for an object schema and return
269
+ * its class name.
270
+ */
271
+ function emitPropertyType(node, ctx, className, path, depth) {
272
+ if (ctx.propertyTypes.has(className))
273
+ return className;
274
+ // A `$ref` cycle would otherwise recurse forever. The spec has none today;
275
+ // this makes adding one a loose type rather than a hung generator.
276
+ if (ctx.inProgress.has(className))
277
+ return className;
278
+ ctx.inProgress.add(className);
279
+ const required = new Set(node.required ?? []);
280
+ const properties = [];
281
+ for (const [name, prop] of Object.entries(node.properties ?? {})) {
282
+ if (prop.readOnly === true)
283
+ continue;
284
+ properties.push({
285
+ name,
286
+ tsType: resolveType(prop, ctx, className + segment(name), `${path}.${name}`, depth + 1, false),
287
+ required: required.has(name),
288
+ description: prop.description,
289
+ constraints: coreExtractConstraints(prop),
290
+ });
291
+ }
292
+ ctx.inProgress.delete(className);
293
+ ctx.propertyTypes.set(className, {
294
+ typeName: className,
295
+ description: node.description,
296
+ properties,
297
+ attributes: [],
298
+ });
299
+ return className;
300
+ }
301
+ // ── Naming helpers ─────────────────────────────────────────────────
302
+ /**
303
+ * Upstream schema names that read better with their internal word boundary
304
+ * restored. `volumeset` and `ipset` are one lowercase token in the API and two
305
+ * words everywhere in Control Plane's own documentation.
306
+ */
307
+ const SCHEMA_NAME_ALIASES = {
308
+ volumeset: "VolumeSet",
309
+ ipset: "IpSet",
310
+ };
311
+ /** `volumeset_spec` → `VolumeSetSpec`, `policy_binding` → `PolicyBinding`. */
312
+ export function schemaClassName(schemaName) {
313
+ return schemaName
314
+ .split("_")
315
+ .map((part) => SCHEMA_NAME_ALIASES[part] ?? segment(part))
316
+ .join("");
317
+ }
318
+ /** `readinessProbe` → `ReadinessProbe`; strips anything not identifier-safe. */
319
+ function segment(name) {
320
+ const cleaned = name.replace(/[^A-Za-z0-9]/g, "");
321
+ return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
322
+ }
323
+ /** `#/components/schemas/query` → `query`. */
324
+ function refTarget(ref) {
325
+ return ref.startsWith(REF_PREFIX) ? ref.slice(REF_PREFIX.length) : undefined;
326
+ }
327
+ /** Render an enum as a sorted string-literal union. */
328
+ function enumUnion(values) {
329
+ return [...values]
330
+ .map((v) => JSON.stringify(v))
331
+ .sort()
332
+ .join(" | ");
333
+ }
334
+ /** Collapse duplicate union members, preserving first-seen order. */
335
+ function dedupeUnion(members) {
336
+ const unique = [...new Set(members)];
337
+ return unique.length === 0 ? "Record<string, unknown>" : unique.join(" | ");
338
+ }
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Refresh the committed offline spec snapshot (`just snapshot`).
4
+ *
5
+ * Fetches the live Control Plane OpenAPI document and writes back the subset
6
+ * reachable from the modelled kinds. Pruning is what makes committing it
7
+ * reasonable: the full document is 2.8 MB of which this lexicon reads 23
8
+ * schemas, and the remainder is `patch_*` mirrors and kinds not modelled here
9
+ * — churn in a file whose whole purpose is to be a stable fallback. It is
10
+ * written re-indented rather than compact so upstream changes show up as a
11
+ * readable diff.
12
+ *
13
+ * Run this on a networked machine whenever the generated surface is meant to
14
+ * move, and commit the result in the same change as the regenerated types, so
15
+ * the snapshot and the types in the tree always describe the same API.
16
+ */
17
+ import { writeFileSync } from "fs";
18
+ import { SCHEMA_URL, SNAPSHOT_FILE } from "./fetch.js";
19
+ import { KINDS } from "../kinds.js";
20
+ const REF_RE = /#\/components\/schemas\/([A-Za-z0-9_]+)/g;
21
+ async function main() {
22
+ console.error(`Fetching ${SCHEMA_URL} …`);
23
+ const response = await fetch(SCHEMA_URL);
24
+ if (!response.ok) {
25
+ throw new Error(`${SCHEMA_URL} answered ${response.status} ${response.statusText}`);
26
+ }
27
+ const spec = (await response.json());
28
+ const schemas = spec.components?.schemas;
29
+ if (!schemas)
30
+ throw new Error("response carried no components.schemas");
31
+ // Transitive closure over `$ref` from the modelled kinds.
32
+ const keep = new Set();
33
+ const queue = KINDS.map((k) => k.schema);
34
+ while (queue.length > 0) {
35
+ const name = queue.shift();
36
+ if (keep.has(name) || !(name in schemas))
37
+ continue;
38
+ keep.add(name);
39
+ for (const match of JSON.stringify(schemas[name]).matchAll(REF_RE))
40
+ queue.push(match[1]);
41
+ }
42
+ const missing = KINDS.filter((k) => !keep.has(k.schema));
43
+ if (missing.length > 0) {
44
+ throw new Error(`spec is missing schemas for: ${missing.map((k) => k.schema).join(", ")}`);
45
+ }
46
+ const pruned = {
47
+ openapi: spec.openapi,
48
+ info: spec.info,
49
+ components: {
50
+ schemas: Object.fromEntries([...keep].sort().map((name) => [name, schemas[name]])),
51
+ },
52
+ };
53
+ const json = `${JSON.stringify(pruned, null, 1)}\n`;
54
+ writeFileSync(SNAPSHOT_FILE, json);
55
+ console.error(`Wrote ${keep.size} schemas (${(json.length / 1024).toFixed(0)} KB) to ${SNAPSHOT_FILE}`);
56
+ }
57
+ main().catch((err) => {
58
+ console.error(err instanceof Error ? err.message : String(err));
59
+ process.exit(1);
60
+ });
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env tsx
2
+ import { validate } from "./validate.js";
3
+ import { printValidationResult } from "@intentius/chant/codegen/validate";
4
+ // `validate` returns a result; printing it is what turns a failed check into a
5
+ // non-zero exit. Calling it and discarding the answer exits 0 whatever the
6
+ // registry looks like — the same scaffold bug cedar carried.
7
+ printValidationResult(await validate());
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Validate the generated `lexicon-cpln` artifacts.
3
+ *
4
+ * The required-names list is the point of this file: it asserts that a
5
+ * regeneration still produced the surface consumers import. A rename upstream,
6
+ * a parser change that stops following a `$ref`, or a loosened subtree would
7
+ * otherwise leave a smaller generated surface that builds fine and is missing
8
+ * classes nobody notices until an import fails.
9
+ *
10
+ * The list spans all three layers deliberately — the eight resources, property
11
+ * types reached through several different mechanisms (inline nesting, a `$ref`,
12
+ * a `oneOf` branch, an array item), and the deepest shapes the parser walks to.
13
+ * A shallow list of the eight resource names would pass while the entire
14
+ * property-type tree vanished.
15
+ */
16
+ import { dirname } from "path";
17
+ import { fileURLToPath } from "url";
18
+ import { validateLexiconArtifacts } from "@intentius/chant/codegen/validate";
19
+ import { KINDS } from "./kinds.js";
20
+ const REQUIRED_NAMES = [
21
+ // The eight resources.
22
+ ...KINDS.map((kind) => kind.className),
23
+ // Top-level specs — inline on gvc/workload/domain/ipset, a `$ref` on volumeset.
24
+ "GvcSpec",
25
+ "WorkloadSpec",
26
+ "VolumeSetSpec",
27
+ "DomainSpec",
28
+ "IpSetSpec",
29
+ // The workload authoring surface most specs actually use.
30
+ "WorkloadSpecContainers",
31
+ "WorkloadSpecContainersPorts",
32
+ "WorkloadSpecContainersEnv",
33
+ "WorkloadSpecContainersVolumes",
34
+ "WorkloadSpecContainersReadinessProbe",
35
+ "WorkloadSpecContainersReadinessProbeHttpGet",
36
+ "WorkloadSpecContainersLivenessProbe",
37
+ "WorkloadSpecFirewallConfig",
38
+ "WorkloadSpecFirewallConfigExternal",
39
+ "WorkloadSpecFirewallConfigInternal",
40
+ "WorkloadSpecDefaultOptions",
41
+ "WorkloadSpecDefaultOptionsAutoscaling",
42
+ "WorkloadSpecJob",
43
+ "WorkloadSpecLoadBalancer",
44
+ "WorkloadSpecVm",
45
+ // GVC placement and routing.
46
+ "GvcSpecStaticPlacement",
47
+ "GvcSpecStaticPlacementLocationQuery",
48
+ "GvcSpecLoadBalancer",
49
+ "GvcSpecTracing",
50
+ "GvcSpecKeda",
51
+ // Storage.
52
+ "VolumeSetSpecSnapshots",
53
+ "VolumeSetSpecAutoscaling",
54
+ "VolumeSetSpecMountOptions",
55
+ // Domain routing.
56
+ "DomainSpecPorts",
57
+ "DomainSpecPortsRoutes",
58
+ "DomainSpecPortsTls",
59
+ // Identity provider sections — the XOR-ruled shapes.
60
+ "IdentityAws",
61
+ "IdentityGcp",
62
+ "IdentityAzure",
63
+ "NetworkResource",
64
+ // `$ref`'d schemas that keep their upstream name, and the `oneOf` branches of
65
+ // `secret.data`. These are the two naming mechanisms other than path
66
+ // derivation, so their absence would mean the parser stopped following a
67
+ // whole class of reference.
68
+ "Query",
69
+ "PolicyBinding",
70
+ "IpSetLocation",
71
+ "SecretOpaque",
72
+ "SecretTls",
73
+ "SecretKeypair",
74
+ "SecretUserpass",
75
+ ];
76
+ /**
77
+ * Validate the generated artifacts.
78
+ */
79
+ export async function validate(opts) {
80
+ const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
81
+ return validateLexiconArtifacts({
82
+ lexiconJsonFilename: "lexicon-cpln.json",
83
+ requiredNames: REQUIRED_NAMES,
84
+ basePath,
85
+ });
86
+ }
87
+ /** The names `validate` asserts, exported so a test can assert the list itself is deep. */
88
+ export const REQUIRED_ARTIFACT_NAMES = REQUIRED_NAMES;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-cpln",
3
- "version": "0.124.31",
3
+ "version": "0.124.32",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/",
@@ -13,22 +13,22 @@
13
13
  ".": {
14
14
  "development": "./src/index.ts",
15
15
  "types": "./dist/index.d.ts",
16
- "default": "./src/index.ts"
16
+ "default": "./dist/index.js"
17
17
  },
18
18
  "./*": {
19
19
  "development": "./src/*.ts",
20
20
  "types": "./dist/*.d.ts",
21
- "default": "./src/*.ts"
21
+ "default": "./dist/*.js"
22
22
  },
23
23
  "./detect": {
24
24
  "development": "./src/detect.ts",
25
25
  "types": "./dist/detect.d.ts",
26
- "default": "./src/detect.ts"
26
+ "default": "./dist/detect.js"
27
27
  },
28
28
  "./lint/post-synth": {
29
29
  "development": "./src/lint/post-synth/index.ts",
30
30
  "types": "./dist/lint/post-synth/index.d.ts",
31
- "default": "./src/lint/post-synth/index.ts"
31
+ "default": "./dist/lint/post-synth/index.js"
32
32
  },
33
33
  "./manifest": "./dist/manifest.json",
34
34
  "./meta": "./dist/meta.json",
@@ -41,7 +41,7 @@
41
41
  "docs": "tsx src/codegen/docs-cli.ts",
42
42
  "snapshot": "tsx src/spec/snapshot-cli.ts",
43
43
  "prepack": "npm run generate && npm run bundle && npm run validate && npm run build",
44
- "build": "../../scripts/tsc7.sh -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && find dist -type f \\( -name \"*.js\" -o -name \"*.js.map\" \\) -delete"
44
+ "build": "../../scripts/tsc7.sh -p tsconfig.build.json --sourceMap false && tsc-alias -p tsconfig.build.json"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@intentius/chant": "*",
@@ -49,7 +49,7 @@
49
49
  "typescript": "^5.9.3"
50
50
  },
51
51
  "peerDependencies": {
52
- "@intentius/chant": "^0.124.31"
52
+ "@intentius/chant": "^0.124.32"
53
53
  },
54
54
  "description": "Control Plane (cpln) lexicon for chant — declarative IaC in TypeScript",
55
55
  "license": "Apache-2.0",
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { readFileSync } from "fs";
6
6
  import { dirname, join } from "path";
7
- import { fileURLToPath } from "url";
7
+ import { moduleSourceDir } from "@intentius/chant/module-source";
8
8
  import { packagePipeline, collectSkills } from "@intentius/chant/codegen/package";
9
9
  import { generate } from "./generate";
10
10
  import { cplnPlugin } from "../plugin";
@@ -14,7 +14,7 @@ export async function packageLexicon(options?: { verbose?: boolean; force?: bool
14
14
  // relative to `src/`, so srcDir is the parent of this directory. Pointing it
15
15
  // at `src/codegen` globs a directory that does not exist and silently ships
16
16
  // a bundle with zero rules and zero skills.
17
- const srcDir = dirname(dirname(fileURLToPath(import.meta.url)));
17
+ const srcDir = dirname(moduleSourceDir(import.meta.url));
18
18
  const pkgJson = JSON.parse(readFileSync(join(srcDir, "..", "package.json"), "utf-8"));
19
19
 
20
20
  const { spec, stats } = await packagePipeline({
@@ -1,8 +1,9 @@
1
1
  import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
2
3
  import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
3
4
  import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
5
 
5
- const require = createRequire(import.meta.url);
6
+ const require = createRequire(moduleSourceUrl(import.meta.url));
6
7
 
7
8
  let cachedIndex: LexiconIndex | null = null;
8
9
 
package/src/lsp/hover.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
2
3
  import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
3
4
  import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
5
  import { KINDS, kindByClassName } from "../kinds";
5
6
 
6
- const require = createRequire(import.meta.url);
7
+ const require = createRequire(moduleSourceUrl(import.meta.url));
7
8
 
8
9
  let cachedIndex: LexiconIndex | null = null;
9
10
 
package/src/spec/fetch.ts CHANGED
@@ -32,7 +32,7 @@
32
32
  import { join, dirname } from "path";
33
33
  import { homedir } from "os";
34
34
  import { readFileSync } from "fs";
35
- import { fileURLToPath } from "url";
35
+ import { moduleSourceDir } from "@intentius/chant/module-source";
36
36
  import { fetchWithCache } from "@intentius/chant/codegen/fetch";
37
37
 
38
38
  export const SCHEMA_URL = "https://api.cpln.io/openapi.json";
@@ -44,7 +44,7 @@ export const SCHEMA_URL = "https://api.cpln.io/openapi.json";
44
44
  */
45
45
  const CACHE_FILE = join(homedir(), ".chant", "cpln-openapi.json");
46
46
 
47
- export const SNAPSHOT_FILE = join(dirname(fileURLToPath(import.meta.url)), "cpln-openapi.snapshot.json");
47
+ export const SNAPSHOT_FILE = join(moduleSourceDir(import.meta.url), "cpln-openapi.snapshot.json");
48
48
 
49
49
  /** The single map key the parser is handed. The spec is one document. */
50
50
  export const SPEC_KEY = "Cpln::OpenAPI";