@intentius/chant-lexicon-cpln 0.53.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.
Files changed (266) hide show
  1. package/README.md +85 -0
  2. package/dist/api.d.ts +77 -0
  3. package/dist/api.d.ts.map +1 -0
  4. package/dist/codegen/docs-cli.d.ts +3 -0
  5. package/dist/codegen/docs-cli.d.ts.map +1 -0
  6. package/dist/codegen/docs.d.ts +17 -0
  7. package/dist/codegen/docs.d.ts.map +1 -0
  8. package/dist/codegen/generate-cli.d.ts +6 -0
  9. package/dist/codegen/generate-cli.d.ts.map +1 -0
  10. package/dist/codegen/generate-lexicon.d.ts +36 -0
  11. package/dist/codegen/generate-lexicon.d.ts.map +1 -0
  12. package/dist/codegen/generate-typescript.d.ts +11 -0
  13. package/dist/codegen/generate-typescript.d.ts.map +1 -0
  14. package/dist/codegen/generate.d.ts +15 -0
  15. package/dist/codegen/generate.d.ts.map +1 -0
  16. package/dist/codegen/naming.d.ts +30 -0
  17. package/dist/codegen/naming.d.ts.map +1 -0
  18. package/dist/codegen/package.d.ts +17 -0
  19. package/dist/codegen/package.d.ts.map +1 -0
  20. package/dist/composites/cron-job.d.ts +60 -0
  21. package/dist/composites/cron-job.d.ts.map +1 -0
  22. package/dist/composites/gvc-environment.d.ts +47 -0
  23. package/dist/composites/gvc-environment.d.ts.map +1 -0
  24. package/dist/composites/index.d.ts +13 -0
  25. package/dist/composites/index.d.ts.map +1 -0
  26. package/dist/composites/public-domain.d.ts +68 -0
  27. package/dist/composites/public-domain.d.ts.map +1 -0
  28. package/dist/composites/secret-access.d.ts +78 -0
  29. package/dist/composites/secret-access.d.ts.map +1 -0
  30. package/dist/composites/serverless-service.d.ts +78 -0
  31. package/dist/composites/serverless-service.d.ts.map +1 -0
  32. package/dist/composites/stateful-service.d.ts +79 -0
  33. package/dist/composites/stateful-service.d.ts.map +1 -0
  34. package/dist/coverage.d.ts +59 -0
  35. package/dist/coverage.d.ts.map +1 -0
  36. package/dist/describe-resources.d.ts +71 -0
  37. package/dist/describe-resources.d.ts.map +1 -0
  38. package/dist/detect.d.ts +14 -0
  39. package/dist/detect.d.ts.map +1 -0
  40. package/dist/entity-props.d.ts +34 -0
  41. package/dist/entity-props.d.ts.map +1 -0
  42. package/dist/generated/index.d.ts +151 -0
  43. package/dist/generated/index.d.ts.map +1 -0
  44. package/dist/generated/runtime.d.ts +5 -0
  45. package/dist/generated/runtime.d.ts.map +1 -0
  46. package/dist/index.d.ts +8 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/init-templates.d.ts +16 -0
  49. package/dist/init-templates.d.ts.map +1 -0
  50. package/dist/integrity.json +35 -0
  51. package/dist/kinds.d.ts +70 -0
  52. package/dist/kinds.d.ts.map +1 -0
  53. package/dist/lint/audit-catalog.d.ts +19 -0
  54. package/dist/lint/audit-catalog.d.ts.map +1 -0
  55. package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts +12 -0
  56. package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts.map +1 -0
  57. package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts +11 -0
  58. package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts.map +1 -0
  59. package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts +11 -0
  60. package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts.map +1 -0
  61. package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts +11 -0
  62. package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts.map +1 -0
  63. package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts +14 -0
  64. package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts.map +1 -0
  65. package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts +10 -0
  66. package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts.map +1 -0
  67. package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts +11 -0
  68. package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts.map +1 -0
  69. package/dist/lint/post-synth/cpl022-container-ports.d.ts +10 -0
  70. package/dist/lint/post-synth/cpl022-container-ports.d.ts.map +1 -0
  71. package/dist/lint/post-synth/cpl023-container-resources.d.ts +12 -0
  72. package/dist/lint/post-synth/cpl023-container-resources.d.ts.map +1 -0
  73. package/dist/lint/post-synth/cpl024-probe-handler.d.ts +10 -0
  74. package/dist/lint/post-synth/cpl024-probe-handler.d.ts.map +1 -0
  75. package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts +11 -0
  76. package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts.map +1 -0
  77. package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts +11 -0
  78. package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts.map +1 -0
  79. package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts +13 -0
  80. package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts.map +1 -0
  81. package/dist/lint/post-synth/cpl028-volume-set.d.ts +3 -0
  82. package/dist/lint/post-synth/cpl028-volume-set.d.ts.map +1 -0
  83. package/dist/lint/post-synth/cpl029-link-targets.d.ts +3 -0
  84. package/dist/lint/post-synth/cpl029-link-targets.d.ts.map +1 -0
  85. package/dist/lint/post-synth/cpl030-domain-routing.d.ts +3 -0
  86. package/dist/lint/post-synth/cpl030-domain-routing.d.ts.map +1 -0
  87. package/dist/lint/post-synth/cpl040-pinned-image.d.ts +11 -0
  88. package/dist/lint/post-synth/cpl040-pinned-image.d.ts.map +1 -0
  89. package/dist/lint/post-synth/cpl041-image-reference-form.d.ts +12 -0
  90. package/dist/lint/post-synth/cpl041-image-reference-form.d.ts.map +1 -0
  91. package/dist/lint/post-synth/cpl042-gvc-placement.d.ts +10 -0
  92. package/dist/lint/post-synth/cpl042-gvc-placement.d.ts.map +1 -0
  93. package/dist/lint/post-synth/cpl043-policy-scope.d.ts +16 -0
  94. package/dist/lint/post-synth/cpl043-policy-scope.d.ts.map +1 -0
  95. package/dist/lint/post-synth/helpers.d.ts +45 -0
  96. package/dist/lint/post-synth/helpers.d.ts.map +1 -0
  97. package/dist/lint/post-synth/index.d.ts +3 -0
  98. package/dist/lint/post-synth/index.d.ts.map +1 -0
  99. package/dist/lint/rules/cpl001-no-secret-literals.d.ts +3 -0
  100. package/dist/lint/rules/cpl001-no-secret-literals.d.ts.map +1 -0
  101. package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts +3 -0
  102. package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts.map +1 -0
  103. package/dist/lint/rules/index.d.ts +6 -0
  104. package/dist/lint/rules/index.d.ts.map +1 -0
  105. package/dist/lsp/completions.d.ts +7 -0
  106. package/dist/lsp/completions.d.ts.map +1 -0
  107. package/dist/lsp/hover.d.ts +4 -0
  108. package/dist/lsp/hover.d.ts.map +1 -0
  109. package/dist/manifest.json +6 -0
  110. package/dist/meta.json +2110 -0
  111. package/dist/okf/index.md +39 -0
  112. package/dist/okf/rules/CPL001.md +22 -0
  113. package/dist/okf/rules/CPL002.md +22 -0
  114. package/dist/okf/rules/CPL010.md +15 -0
  115. package/dist/okf/rules/CPL011.md +15 -0
  116. package/dist/okf/rules/CPL012.md +15 -0
  117. package/dist/okf/rules/CPL013.md +15 -0
  118. package/dist/okf/rules/CPL014.md +16 -0
  119. package/dist/okf/rules/CPL020.md +11 -0
  120. package/dist/okf/rules/CPL021.md +15 -0
  121. package/dist/okf/rules/CPL022.md +15 -0
  122. package/dist/okf/rules/CPL023.md +15 -0
  123. package/dist/okf/rules/CPL024.md +15 -0
  124. package/dist/okf/rules/CPL025.md +15 -0
  125. package/dist/okf/rules/CPL026.md +15 -0
  126. package/dist/okf/rules/CPL027.md +15 -0
  127. package/dist/okf/rules/CPL028.md +15 -0
  128. package/dist/okf/rules/CPL029.md +15 -0
  129. package/dist/okf/rules/CPL030.md +16 -0
  130. package/dist/okf/rules/CPL040.md +15 -0
  131. package/dist/okf/rules/CPL041.md +15 -0
  132. package/dist/okf/rules/CPL042.md +11 -0
  133. package/dist/okf/rules/CPL043.md +15 -0
  134. package/dist/okf/types/Domain.md +22 -0
  135. package/dist/okf/types/Gvc.md +21 -0
  136. package/dist/okf/types/Identity.md +29 -0
  137. package/dist/okf/types/IpSet.md +21 -0
  138. package/dist/okf/types/Policy.md +28 -0
  139. package/dist/okf/types/Secret.md +23 -0
  140. package/dist/okf/types/VolumeSet.md +22 -0
  141. package/dist/okf/types/Workload.md +38 -0
  142. package/dist/ownership.d.ts +23 -0
  143. package/dist/ownership.d.ts.map +1 -0
  144. package/dist/package-cli.d.ts +3 -0
  145. package/dist/package-cli.d.ts.map +1 -0
  146. package/dist/plugin.d.ts +3 -0
  147. package/dist/plugin.d.ts.map +1 -0
  148. package/dist/reference-catalog.d.ts +24 -0
  149. package/dist/reference-catalog.d.ts.map +1 -0
  150. package/dist/rules/cpl001-no-secret-literals.ts +83 -0
  151. package/dist/rules/cpl002-prefer-resource-reference.ts +71 -0
  152. package/dist/rules/cpl010-unrestricted-outbound.ts +41 -0
  153. package/dist/rules/cpl011-internal-firewall-scope.ts +38 -0
  154. package/dist/rules/cpl012-no-literal-credential-env.ts +58 -0
  155. package/dist/rules/cpl013-qualified-identity-principal.ts +45 -0
  156. package/dist/rules/cpl014-qualified-secret-ref.ts +54 -0
  157. package/dist/rules/cpl020-serverless-single-port.ts +58 -0
  158. package/dist/rules/cpl021-cron-job-shape.ts +64 -0
  159. package/dist/rules/cpl022-container-ports.ts +64 -0
  160. package/dist/rules/cpl023-container-resources.ts +87 -0
  161. package/dist/rules/cpl024-probe-handler.ts +51 -0
  162. package/dist/rules/cpl025-autoscaling-shape.ts +100 -0
  163. package/dist/rules/cpl026-scale-to-zero.ts +67 -0
  164. package/dist/rules/cpl027-capacity-ai-conflict.ts +78 -0
  165. package/dist/rules/cpl028-volume-set.ts +183 -0
  166. package/dist/rules/cpl029-link-targets.ts +155 -0
  167. package/dist/rules/cpl030-domain-routing.ts +137 -0
  168. package/dist/rules/cpl040-pinned-image.ts +48 -0
  169. package/dist/rules/cpl041-image-reference-form.ts +60 -0
  170. package/dist/rules/cpl042-gvc-placement.ts +57 -0
  171. package/dist/rules/cpl043-policy-scope.ts +0 -0
  172. package/dist/rules/helpers.ts +114 -0
  173. package/dist/serializer.d.ts +69 -0
  174. package/dist/serializer.d.ts.map +1 -0
  175. package/dist/skills/chant-cpln-secrets.md +110 -0
  176. package/dist/skills/chant-cpln-workloads.md +78 -0
  177. package/dist/skills/chant-cpln.md +103 -0
  178. package/dist/spec/fetch.d.ts +43 -0
  179. package/dist/spec/fetch.d.ts.map +1 -0
  180. package/dist/spec/parse.d.ts +79 -0
  181. package/dist/spec/parse.d.ts.map +1 -0
  182. package/dist/spec/snapshot-cli.d.ts +18 -0
  183. package/dist/spec/snapshot-cli.d.ts.map +1 -0
  184. package/dist/types/index.d.ts +1649 -0
  185. package/dist/validate-cli.d.ts +3 -0
  186. package/dist/validate-cli.d.ts.map +1 -0
  187. package/dist/validate.d.ts +26 -0
  188. package/dist/validate.d.ts.map +1 -0
  189. package/package.json +74 -0
  190. package/src/api.ts +134 -0
  191. package/src/codegen/docs-cli.ts +7 -0
  192. package/src/codegen/docs.ts +94 -0
  193. package/src/codegen/generate-cli.ts +34 -0
  194. package/src/codegen/generate-lexicon.ts +88 -0
  195. package/src/codegen/generate-typescript.ts +71 -0
  196. package/src/codegen/generate.ts +110 -0
  197. package/src/codegen/naming.ts +61 -0
  198. package/src/codegen/package.ts +35 -0
  199. package/src/composites/composites.test.ts +252 -0
  200. package/src/composites/cron-job.ts +124 -0
  201. package/src/composites/gvc-environment.ts +104 -0
  202. package/src/composites/index.ts +17 -0
  203. package/src/composites/public-domain.ts +144 -0
  204. package/src/composites/secret-access.ts +136 -0
  205. package/src/composites/serverless-service.ts +143 -0
  206. package/src/composites/stateful-service.ts +179 -0
  207. package/src/coverage.ts +149 -0
  208. package/src/describe-resources.test.ts +252 -0
  209. package/src/describe-resources.ts +310 -0
  210. package/src/detect.ts +58 -0
  211. package/src/entity-props.ts +98 -0
  212. package/src/generated/index.d.ts +1649 -0
  213. package/src/generated/index.ts +156 -0
  214. package/src/generated/lexicon-cpln.json +2110 -0
  215. package/src/generated/runtime.ts +4 -0
  216. package/src/index.ts +31 -0
  217. package/src/init-templates.ts +131 -0
  218. package/src/kinds.ts +173 -0
  219. package/src/lint/audit-catalog.ts +230 -0
  220. package/src/lint/post-synth/cpl010-unrestricted-outbound.ts +41 -0
  221. package/src/lint/post-synth/cpl011-internal-firewall-scope.ts +38 -0
  222. package/src/lint/post-synth/cpl012-no-literal-credential-env.ts +58 -0
  223. package/src/lint/post-synth/cpl013-qualified-identity-principal.ts +45 -0
  224. package/src/lint/post-synth/cpl014-qualified-secret-ref.ts +54 -0
  225. package/src/lint/post-synth/cpl020-serverless-single-port.ts +58 -0
  226. package/src/lint/post-synth/cpl021-cron-job-shape.ts +64 -0
  227. package/src/lint/post-synth/cpl022-container-ports.ts +64 -0
  228. package/src/lint/post-synth/cpl023-container-resources.ts +87 -0
  229. package/src/lint/post-synth/cpl024-probe-handler.ts +51 -0
  230. package/src/lint/post-synth/cpl025-autoscaling-shape.ts +100 -0
  231. package/src/lint/post-synth/cpl026-scale-to-zero.ts +67 -0
  232. package/src/lint/post-synth/cpl027-capacity-ai-conflict.ts +78 -0
  233. package/src/lint/post-synth/cpl028-volume-set.ts +183 -0
  234. package/src/lint/post-synth/cpl029-link-targets.ts +155 -0
  235. package/src/lint/post-synth/cpl030-domain-routing.ts +137 -0
  236. package/src/lint/post-synth/cpl040-pinned-image.ts +48 -0
  237. package/src/lint/post-synth/cpl041-image-reference-form.ts +60 -0
  238. package/src/lint/post-synth/cpl042-gvc-placement.ts +57 -0
  239. package/src/lint/post-synth/cpl043-policy-scope.ts +0 -0
  240. package/src/lint/post-synth/helpers.ts +114 -0
  241. package/src/lint/post-synth/index.ts +45 -0
  242. package/src/lint/post-synth/post-synth.test.ts +737 -0
  243. package/src/lint/rules/cpl001-no-secret-literals.ts +83 -0
  244. package/src/lint/rules/cpl002-prefer-resource-reference.ts +71 -0
  245. package/src/lint/rules/index.ts +9 -0
  246. package/src/lint/rules/rules.test.ts +108 -0
  247. package/src/lsp/completions.test.ts +44 -0
  248. package/src/lsp/completions.ts +27 -0
  249. package/src/lsp/hover.test.ts +53 -0
  250. package/src/lsp/hover.ts +130 -0
  251. package/src/ownership.ts +28 -0
  252. package/src/package-cli.ts +25 -0
  253. package/src/plugin.test.ts +131 -0
  254. package/src/plugin.ts +187 -0
  255. package/src/reference-catalog.ts +110 -0
  256. package/src/serializer.test.ts +233 -0
  257. package/src/serializer.ts +253 -0
  258. package/src/skills/chant-cpln-secrets.md +110 -0
  259. package/src/skills/chant-cpln-workloads.md +78 -0
  260. package/src/skills/chant-cpln.md +103 -0
  261. package/src/spec/cpln-openapi.snapshot.json +10651 -0
  262. package/src/spec/fetch.ts +77 -0
  263. package/src/spec/parse.ts +451 -0
  264. package/src/spec/snapshot-cli.ts +71 -0
  265. package/src/validate-cli.ts +8 -0
  266. package/src/validate.ts +102 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Control Plane Core API OpenAPI 3.0.3 spec.
3
+ *
4
+ * `https://api.cpln.io/openapi.json` is an unversioned live endpoint: it always
5
+ * serves the current spec, and there is no tag, release asset, or `info.version`
6
+ * bump to pin against — `info.version` has read `1.0.0` for as long as the
7
+ * endpoint has existed. There is consequently nothing for the self-upgrade
8
+ * tooling to bump, so this plugin declares no `upstreamPin`, the same call fly
9
+ * makes for `docs.machines.dev/openapi.json`. Re-running `generate` picks up
10
+ * whatever the endpoint currently serves.
11
+ *
12
+ * Unlike fly, this lexicon keeps a committed offline fallback. Two reasons the
13
+ * live-only route was not good enough here:
14
+ *
15
+ * - The endpoint is the production API's own doc route, not a docs CDN. It is
16
+ * subject to the same maintenance windows as the API, and a generate step
17
+ * that fails when Control Plane is briefly unreachable makes CI a function of
18
+ * someone else's uptime.
19
+ * - The full document is 2.8 MB, almost all of which is the `patch_*` mirror
20
+ * schemas and kinds this lexicon does not model. The snapshot is pruned to
21
+ * the schemas actually reachable from the modelled kinds — 23 of 106 — which
22
+ * is small enough to commit, and to re-indent for a readable diff, without
23
+ * turning every regeneration into a megabyte-scale change.
24
+ *
25
+ * The fallback is never silent. Both routes log which one was taken, because a
26
+ * fallback nobody can see is how you end up debugging types that came from
27
+ * somewhere you did not expect.
28
+ *
29
+ * Refresh the snapshot with `just snapshot` (see `snapshot-cli.ts`).
30
+ */
31
+
32
+ import { join, dirname } from "path";
33
+ import { homedir } from "os";
34
+ import { readFileSync } from "fs";
35
+ import { fileURLToPath } from "url";
36
+ import { fetchWithCache } from "@intentius/chant/codegen/fetch";
37
+
38
+ export const SCHEMA_URL = "https://api.cpln.io/openapi.json";
39
+
40
+ /**
41
+ * Cache under `~/.chant` (like fly and k8s) so CI's `~/.chant` schema cache
42
+ * covers it — an api.cpln.io hiccup then falls back to the cache before it
43
+ * falls back to the committed snapshot.
44
+ */
45
+ const CACHE_FILE = join(homedir(), ".chant", "cpln-openapi.json");
46
+
47
+ export const SNAPSHOT_FILE = join(dirname(fileURLToPath(import.meta.url)), "cpln-openapi.snapshot.json");
48
+
49
+ /** The single map key the parser is handed. The spec is one document. */
50
+ export const SPEC_KEY = "Cpln::OpenAPI";
51
+
52
+ /**
53
+ * Fetch the Control Plane OpenAPI spec, falling back to the committed pruned
54
+ * snapshot when the endpoint is unreachable. Returns a single-entry map — the
55
+ * whole spec is one document; the parser fans it out into per-kind results.
56
+ */
57
+ export async function fetchSchemas(options?: { force?: boolean }): Promise<Map<string, Buffer>> {
58
+ let raw: Buffer;
59
+ try {
60
+ raw = await fetchWithCache({ url: SCHEMA_URL, cacheFile: CACHE_FILE }, options?.force);
61
+ // A 404 body is still a body. Parsing proves we got a spec and not an error
62
+ // page that would otherwise be cached and then generated from.
63
+ const parsed = JSON.parse(raw.toString("utf-8")) as { components?: { schemas?: unknown } };
64
+ if (!parsed.components?.schemas) {
65
+ throw new Error("response carried no components.schemas");
66
+ }
67
+ console.error(`[cpln] spec: live ${SCHEMA_URL}`);
68
+ } catch (err) {
69
+ console.error(
70
+ `[cpln] spec: committed snapshot — could not fetch ${SCHEMA_URL} ` +
71
+ `(${err instanceof Error ? err.message.split("\n")[0] : String(err)}). ` +
72
+ `Refresh it with \`just snapshot\` on a networked machine.`,
73
+ );
74
+ raw = readFileSync(SNAPSHOT_FILE);
75
+ }
76
+ return new Map([[SPEC_KEY, raw]]);
77
+ }
@@ -0,0 +1,451 @@
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
+
40
+ import {
41
+ extractConstraints as coreExtractConstraints,
42
+ primaryType,
43
+ type JsonSchemaProperty,
44
+ type PropertyConstraints,
45
+ } from "@intentius/chant/codegen/json-schema";
46
+ import { KINDS, NAMESPACE, SERVICE, type CplnKind } from "../kinds";
47
+
48
+ // ── Types ──────────────────────────────────────────────────────────
49
+
50
+ export type { PropertyConstraints };
51
+
52
+ export interface ParsedProperty {
53
+ name: string;
54
+ tsType: string;
55
+ required: boolean;
56
+ description?: string;
57
+ constraints: PropertyConstraints;
58
+ }
59
+
60
+ export interface ParsedResource {
61
+ typeName: string;
62
+ description?: string;
63
+ properties: ParsedProperty[];
64
+ attributes: Array<{ name: string; tsType: string }>;
65
+ }
66
+
67
+ export interface CplnParseResult {
68
+ resource: ParsedResource;
69
+ /** Always empty — cpln emits property types as standalone results, like fly. */
70
+ propertyTypes: Array<{ name: string; defType: string }>;
71
+ /** Always empty — enums are inlined as string-literal unions. */
72
+ enums: Array<{ name: string; values: string[] }>;
73
+ /** Whether this entity is a property type rather than a resource. */
74
+ isProperty?: boolean;
75
+ }
76
+
77
+ // ── OpenAPI shapes ─────────────────────────────────────────────────
78
+
79
+ interface OpenAPISchema extends JsonSchemaProperty {
80
+ oneOf?: OpenAPISchema[];
81
+ anyOf?: OpenAPISchema[];
82
+ allOf?: OpenAPISchema[];
83
+ additionalProperties?: boolean | OpenAPISchema;
84
+ items?: OpenAPISchema;
85
+ properties?: Record<string, OpenAPISchema>;
86
+ readOnly?: boolean;
87
+ }
88
+
89
+ interface OpenAPISpec {
90
+ components?: { schemas?: Record<string, OpenAPISchema> };
91
+ }
92
+
93
+ const REF_PREFIX = "#/components/schemas/";
94
+
95
+ /** Backstop against a self-referential `$ref` the spec does not currently have. */
96
+ const MAX_DEPTH = 16;
97
+
98
+ // ── Curation ───────────────────────────────────────────────────────
99
+
100
+ /**
101
+ * Property paths that resolve to `Record<string, unknown>` instead of being
102
+ * expanded. Keyed `<kind>:<dotted path>`; see the module comment for why.
103
+ */
104
+ const LOOSENED_PATHS = new Set(["gvc:spec.sidecar.envoy", "workload:spec.sidecar.envoy"]);
105
+
106
+ /**
107
+ * Types the spec states too loosely to author against, keyed `<kind>:<path>`.
108
+ *
109
+ * `tags` is declared as a bare `{"type": "object"}` on all eight kinds, which
110
+ * resolves to `Record<string, unknown>` — technically true and useless in
111
+ * practice, since Control Plane stores tag values as strings and an object
112
+ * value is rejected at apply time. Narrowing it here turns a runtime rejection
113
+ * into a compile error, and matters more than usual for this lexicon because
114
+ * `tags` is also where the ownership marker is stamped.
115
+ */
116
+ const TYPE_OVERRIDES = new Map<string, string>(KINDS.map((k) => [`${k.kind}:tags`, "Record<string, string>"]));
117
+
118
+ /**
119
+ * Top-level properties treated as read-only attributes on every kind. These
120
+ * are the envelope the API stamps on a resource, not authoring surface.
121
+ */
122
+ const ENVELOPE_ATTRIBUTES = new Set(["id", "kind", "version", "created", "lastModified", "links", "alias", "status"]);
123
+
124
+ /**
125
+ * Attribute names core's runtime already owns on every `Declarable`.
126
+ *
127
+ * `createResource` installs `lexicon`, `entityType`, `kind`, `props`,
128
+ * `attributes` and `Ref` as non-configurable instance properties, then defines
129
+ * an `AttrRef` for each declared attribute. An attribute colliding with one of
130
+ * those throws `TypeError: Cannot redefine property` in the constructor — not
131
+ * at generate time, at the moment a user writes `new Gvc({...})`.
132
+ *
133
+ * cpln collides on exactly one: every kind carries a read-only `kind`
134
+ * discriminator. Dropping it costs nothing, because the chant type already
135
+ * says which kind a resource is and the serializer emits `kind:` from the
136
+ * kinds table rather than from the resource. The rest of the set is guarded so
137
+ * a future upstream field cannot reintroduce the same failure quietly.
138
+ */
139
+ const RESERVED_ATTRIBUTE_NAMES = new Set(["lexicon", "entityType", "kind", "props", "attributes", "Ref"]);
140
+
141
+ /**
142
+ * Properties the spec fails to mark `readOnly` but which are server-computed.
143
+ *
144
+ * `workload.health` is a rollup of readiness across locations — the API
145
+ * populates it and rejects nothing if you send it, so it round-trips as
146
+ * writable and would show up as permanent drift on every workload if this
147
+ * lexicon declared it. It is an attribute here regardless of the spec.
148
+ */
149
+ const FORCED_ATTRIBUTES = new Set(["workload:health"]);
150
+
151
+ /**
152
+ * The spec marks `required: []` on every kind — nothing is required, including
153
+ * a resource's own name. That is true of a PATCH body and false of anything
154
+ * chant would synthesize, so `name` is required on every kind, and the two
155
+ * discriminators without which a manifest is meaningless are required on their
156
+ * own kind.
157
+ */
158
+ const FORCED_REQUIRED = new Set(["name", "secret:type", "policy:targetKind"]);
159
+
160
+ /**
161
+ * The `gvc` property every GVC-scoped kind carries.
162
+ *
163
+ * Synthetic only with respect to the OpenAPI document. A workload's GVC is a
164
+ * URL segment there (`/org/{org}/gvc/{gvc}/workload/{name}`) rather than a body
165
+ * field, so the `workload` schema has none; `identity` and `volumeset` do carry
166
+ * one, inconsistently — a string on the first, an object on the second.
167
+ *
168
+ * The `cpln apply` manifest format, which is what this lexicon actually emits,
169
+ * has a real top-level `gvc` key for exactly this purpose: "you can specify
170
+ * either a `gvc` property in the file or use the `--gvc` flag, but not both".
171
+ * So all three kinds get the same required `gvc: string`, it serializes
172
+ * straight through, and a manifest stays self-contained rather than depending
173
+ * on which flag the caller remembered. The Terraform provider makes the same
174
+ * call (`cpln_workload.gvc`).
175
+ */
176
+ const SYNTHETIC_GVC_PROP: ParsedProperty = {
177
+ name: "gvc",
178
+ tsType: "string",
179
+ required: true,
180
+ description: "Name of the GVC this resource belongs to.",
181
+ constraints: {},
182
+ };
183
+
184
+ // ── Parser ─────────────────────────────────────────────────────────
185
+
186
+ /**
187
+ * Parse the cpln OpenAPI spec into the modelled kinds and every property type
188
+ * reachable from them.
189
+ */
190
+ export function parseCplnOpenAPI(data: string | Buffer): CplnParseResult[] {
191
+ const spec: OpenAPISpec = JSON.parse(typeof data === "string" ? data : data.toString("utf-8"));
192
+ const schemas = spec.components?.schemas ?? {};
193
+
194
+ // Synthesized property types, keyed by class name. Populated as a side effect
195
+ // of resolving each kind's properties; a shape reached twice is emitted once.
196
+ const propertyTypes = new Map<string, ParsedResource>();
197
+ const inProgress = new Set<string>();
198
+
199
+ const results: CplnParseResult[] = [];
200
+
201
+ for (const kind of KINDS) {
202
+ const schema = schemas[kind.schema];
203
+ if (!schema) {
204
+ throw new Error(
205
+ `cpln spec has no \`components.schemas.${kind.schema}\` backing kind \`${kind.kind}\` — ` +
206
+ `the upstream spec changed shape, or the snapshot is stale.`,
207
+ );
208
+ }
209
+
210
+ const properties: ParsedProperty[] = [];
211
+ const attributes: Array<{ name: string; tsType: string }> = [];
212
+
213
+ for (const [name, prop] of Object.entries(schema.properties ?? {})) {
214
+ const isAttribute =
215
+ prop.readOnly === true || ENVELOPE_ATTRIBUTES.has(name) || FORCED_ATTRIBUTES.has(`${kind.kind}:${name}`);
216
+
217
+ const ctx: WalkContext = { schemas, propertyTypes, inProgress, kind };
218
+ const tsType = resolveType(prop, ctx, kind.className + segment(name), `${kind.kind}:${name}`, 0, isAttribute);
219
+
220
+ if (isAttribute) {
221
+ if (!RESERVED_ATTRIBUTE_NAMES.has(name)) attributes.push({ name, tsType });
222
+ continue;
223
+ }
224
+
225
+ properties.push({
226
+ name,
227
+ tsType,
228
+ required: (schema.required ?? []).includes(name) || FORCED_REQUIRED.has(name) || FORCED_REQUIRED.has(`${kind.kind}:${name}`),
229
+ description: prop.description,
230
+ constraints: coreExtractConstraints(prop as JsonSchemaProperty),
231
+ });
232
+ }
233
+
234
+ if (kind.gvcScoped) {
235
+ // Replace rather than append: `identity` and `volumeset` already declare
236
+ // a `gvc` of their own, in two different shapes. One normalized property
237
+ // beats three near-identical ones.
238
+ const existing = properties.findIndex((p) => p.name === "gvc");
239
+ if (existing >= 0) properties.splice(existing, 1);
240
+ properties.push({ ...SYNTHETIC_GVC_PROP });
241
+ }
242
+
243
+ results.push({
244
+ resource: { typeName: kind.typeName, description: kind.summary, properties, attributes },
245
+ propertyTypes: [],
246
+ enums: [],
247
+ });
248
+ }
249
+
250
+ for (const [className, resource] of [...propertyTypes].sort(([a], [b]) => a.localeCompare(b))) {
251
+ results.push({
252
+ resource: { ...resource, typeName: `${NAMESPACE}::${SERVICE}::${className}` },
253
+ propertyTypes: [],
254
+ enums: [],
255
+ isProperty: true,
256
+ });
257
+ }
258
+
259
+ return results;
260
+ }
261
+
262
+ // ── Type resolution ────────────────────────────────────────────────
263
+
264
+ interface WalkContext {
265
+ schemas: Record<string, OpenAPISchema>;
266
+ propertyTypes: Map<string, ParsedResource>;
267
+ inProgress: Set<string>;
268
+ kind: CplnKind;
269
+ }
270
+
271
+ /**
272
+ * Resolve a schema node to a TypeScript type, emitting property-type classes
273
+ * for the object shapes it reaches.
274
+ *
275
+ * `className` is the name an inline object at this position would take;
276
+ * `path` is the `<kind>:<dotted path>` used to consult {@link LOOSENED_PATHS}.
277
+ * `readOnlyBranch` suppresses class emission under a read-only subtree —
278
+ * `status` trees are reported back as plain data, and typing them would double
279
+ * this lexicon's surface for shapes nobody authors.
280
+ */
281
+ function resolveType(
282
+ node: OpenAPISchema | undefined,
283
+ ctx: WalkContext,
284
+ className: string,
285
+ path: string,
286
+ depth: number,
287
+ readOnlyBranch: boolean,
288
+ ): string {
289
+ if (!node || depth > MAX_DEPTH) return "Record<string, unknown>";
290
+
291
+ if (LOOSENED_PATHS.has(path)) return "Record<string, unknown>";
292
+
293
+ const override = TYPE_OVERRIDES.get(path);
294
+ if (override) return override;
295
+
296
+ // A `$ref` adopts the upstream schema's name, so a shape shared between two
297
+ // kinds is emitted once rather than once per path that reaches it.
298
+ if (node.$ref) {
299
+ const refName = refTarget(node.$ref);
300
+ if (!refName) return "Record<string, unknown>";
301
+ const target = ctx.schemas[refName];
302
+ if (!target) return "Record<string, unknown>";
303
+ return resolveType(target, ctx, schemaClassName(refName), path, depth + 1, readOnlyBranch);
304
+ }
305
+
306
+ // `allOf: [{ $ref }]` — the OpenAPI idiom for "typed as this schema".
307
+ if (node.allOf?.length) {
308
+ const withRef = node.allOf.find((s) => s.$ref);
309
+ if (withRef) return resolveType(withRef, ctx, className, path, depth + 1, readOnlyBranch);
310
+ }
311
+
312
+ const union = node.oneOf ?? node.anyOf;
313
+ if (union?.length) {
314
+ // Named branches keep their type; inline object branches collapse to a map.
315
+ // The only union of consequence is `secret.data`, whose named branches are
316
+ // the six documented secret payloads and whose inline branches are
317
+ // provider-specific credential bags — a union of six anonymous
318
+ // `SecretDataVariant3`-style classes would be worse than one honest map.
319
+ const members = union.map((branch) =>
320
+ branch.$ref
321
+ ? resolveType(branch, ctx, className, path, depth + 1, readOnlyBranch)
322
+ : branch.properties
323
+ ? "Record<string, unknown>"
324
+ : resolveType(branch, ctx, className, path, depth + 1, readOnlyBranch),
325
+ );
326
+ return dedupeUnion(members);
327
+ }
328
+
329
+ if (node.enum?.length) return enumUnion(node.enum);
330
+
331
+ const kind = primaryType(node.type);
332
+
333
+ if (kind === "array") {
334
+ // An array contributes no name segment: `containers[]` and `containers`
335
+ // describe the same shape.
336
+ const item = resolveType(node.items, ctx, className, path, depth + 1, readOnlyBranch);
337
+ return item.includes(" | ") ? `(${item})[]` : `${item}[]`;
338
+ }
339
+
340
+ if (kind === "object" || (!node.type && node.properties)) {
341
+ if (node.properties && Object.keys(node.properties).length > 0) {
342
+ if (readOnlyBranch) return "Record<string, unknown>";
343
+ return emitPropertyType(node, ctx, className, path, depth);
344
+ }
345
+ if (node.additionalProperties && typeof node.additionalProperties === "object") {
346
+ const value = resolveType(node.additionalProperties, ctx, className, path, depth + 1, readOnlyBranch);
347
+ return `Record<string, ${value}>`;
348
+ }
349
+ return "Record<string, unknown>";
350
+ }
351
+
352
+ switch (kind) {
353
+ case "string":
354
+ return "string";
355
+ case "integer":
356
+ case "number":
357
+ return "number";
358
+ case "boolean":
359
+ return "boolean";
360
+ default:
361
+ return "Record<string, unknown>";
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Register (or reuse) a property-type class for an object schema and return
367
+ * its class name.
368
+ */
369
+ function emitPropertyType(
370
+ node: OpenAPISchema,
371
+ ctx: WalkContext,
372
+ className: string,
373
+ path: string,
374
+ depth: number,
375
+ ): string {
376
+ if (ctx.propertyTypes.has(className)) return className;
377
+
378
+ // A `$ref` cycle would otherwise recurse forever. The spec has none today;
379
+ // this makes adding one a loose type rather than a hung generator.
380
+ if (ctx.inProgress.has(className)) return className;
381
+ ctx.inProgress.add(className);
382
+
383
+ const required = new Set(node.required ?? []);
384
+ const properties: ParsedProperty[] = [];
385
+
386
+ for (const [name, prop] of Object.entries(node.properties ?? {})) {
387
+ if (prop.readOnly === true) continue;
388
+ properties.push({
389
+ name,
390
+ tsType: resolveType(prop, ctx, className + segment(name), `${path}.${name}`, depth + 1, false),
391
+ required: required.has(name),
392
+ description: prop.description,
393
+ constraints: coreExtractConstraints(prop as JsonSchemaProperty),
394
+ });
395
+ }
396
+
397
+ ctx.inProgress.delete(className);
398
+ ctx.propertyTypes.set(className, {
399
+ typeName: className,
400
+ description: node.description,
401
+ properties,
402
+ attributes: [],
403
+ });
404
+
405
+ return className;
406
+ }
407
+
408
+ // ── Naming helpers ─────────────────────────────────────────────────
409
+
410
+ /**
411
+ * Upstream schema names that read better with their internal word boundary
412
+ * restored. `volumeset` and `ipset` are one lowercase token in the API and two
413
+ * words everywhere in Control Plane's own documentation.
414
+ */
415
+ const SCHEMA_NAME_ALIASES: Record<string, string> = {
416
+ volumeset: "VolumeSet",
417
+ ipset: "IpSet",
418
+ };
419
+
420
+ /** `volumeset_spec` → `VolumeSetSpec`, `policy_binding` → `PolicyBinding`. */
421
+ export function schemaClassName(schemaName: string): string {
422
+ return schemaName
423
+ .split("_")
424
+ .map((part) => SCHEMA_NAME_ALIASES[part] ?? segment(part))
425
+ .join("");
426
+ }
427
+
428
+ /** `readinessProbe` → `ReadinessProbe`; strips anything not identifier-safe. */
429
+ function segment(name: string): string {
430
+ const cleaned = name.replace(/[^A-Za-z0-9]/g, "");
431
+ return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
432
+ }
433
+
434
+ /** `#/components/schemas/query` → `query`. */
435
+ function refTarget(ref: string): string | undefined {
436
+ return ref.startsWith(REF_PREFIX) ? ref.slice(REF_PREFIX.length) : undefined;
437
+ }
438
+
439
+ /** Render an enum as a sorted string-literal union. */
440
+ function enumUnion(values: unknown[]): string {
441
+ return [...values]
442
+ .map((v) => JSON.stringify(v))
443
+ .sort()
444
+ .join(" | ");
445
+ }
446
+
447
+ /** Collapse duplicate union members, preserving first-seen order. */
448
+ function dedupeUnion(members: string[]): string {
449
+ const unique = [...new Set(members)];
450
+ return unique.length === 0 ? "Record<string, unknown>" : unique.join(" | ");
451
+ }
@@ -0,0 +1,71 @@
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
+
18
+ import { writeFileSync } from "fs";
19
+ import { SCHEMA_URL, SNAPSHOT_FILE } from "./fetch";
20
+ import { KINDS } from "../kinds";
21
+
22
+ const REF_RE = /#\/components\/schemas\/([A-Za-z0-9_]+)/g;
23
+
24
+ interface Spec {
25
+ openapi?: string;
26
+ info?: unknown;
27
+ components?: { schemas?: Record<string, unknown> };
28
+ }
29
+
30
+ async function main(): Promise<void> {
31
+ console.error(`Fetching ${SCHEMA_URL} …`);
32
+ const response = await fetch(SCHEMA_URL);
33
+ if (!response.ok) {
34
+ throw new Error(`${SCHEMA_URL} answered ${response.status} ${response.statusText}`);
35
+ }
36
+ const spec = (await response.json()) as Spec;
37
+ const schemas = spec.components?.schemas;
38
+ if (!schemas) throw new Error("response carried no components.schemas");
39
+
40
+ // Transitive closure over `$ref` from the modelled kinds.
41
+ const keep = new Set<string>();
42
+ const queue = KINDS.map((k) => k.schema);
43
+ while (queue.length > 0) {
44
+ const name = queue.shift()!;
45
+ if (keep.has(name) || !(name in schemas)) continue;
46
+ keep.add(name);
47
+ for (const match of JSON.stringify(schemas[name]).matchAll(REF_RE)) queue.push(match[1]);
48
+ }
49
+
50
+ const missing = KINDS.filter((k) => !keep.has(k.schema));
51
+ if (missing.length > 0) {
52
+ throw new Error(`spec is missing schemas for: ${missing.map((k) => k.schema).join(", ")}`);
53
+ }
54
+
55
+ const pruned = {
56
+ openapi: spec.openapi,
57
+ info: spec.info,
58
+ components: {
59
+ schemas: Object.fromEntries([...keep].sort().map((name) => [name, schemas[name]])),
60
+ },
61
+ };
62
+
63
+ const json = `${JSON.stringify(pruned, null, 1)}\n`;
64
+ writeFileSync(SNAPSHOT_FILE, json);
65
+ console.error(`Wrote ${keep.size} schemas (${(json.length / 1024).toFixed(0)} KB) to ${SNAPSHOT_FILE}`);
66
+ }
67
+
68
+ main().catch((err) => {
69
+ console.error(err instanceof Error ? err.message : String(err));
70
+ process.exit(1);
71
+ });
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env tsx
2
+ import { validate } from "./validate";
3
+ import { printValidationResult } from "@intentius/chant/codegen/validate";
4
+
5
+ // `validate` returns a result; printing it is what turns a failed check into a
6
+ // non-zero exit. Calling it and discarding the answer exits 0 whatever the
7
+ // registry looks like — the same scaffold bug cedar carried.
8
+ printValidationResult(await validate());