@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
package/dist/plugin.js ADDED
@@ -0,0 +1,141 @@
1
+ import { compositeCatalog } from "./composites/catalog.js";
2
+ import { createSkillsLoader, createDiffTool, createCatalogResource } from "@intentius/chant/lexicon-plugin-helpers";
3
+ import { cplnSerializer } from "./serializer.js";
4
+ import { rules } from "./lint/rules/index.js";
5
+ import { postSynthChecks } from "./lint/post-synth/index.js";
6
+ import { cplnAuditCatalog } from "./lint/audit-catalog.js";
7
+ import { cplnReferenceCatalog } from "./reference-catalog.js";
8
+ import { CPLN_TAG_OWNERSHIP_KEYS } from "./ownership.js";
9
+ import { detectCplnTemplate } from "./detect.js";
10
+ import { cplnInitTemplates } from "./init-templates.js";
11
+ import { completions } from "./lsp/completions.js";
12
+ import { hover } from "./lsp/hover.js";
13
+ /**
14
+ * The cpln lexicon plugin.
15
+ *
16
+ * Control Plane's `tags` map is the ownership channel, and unusually it works
17
+ * on the thin read: every kind carries `tags` and every read path returns
18
+ * them, so `describeResources` resolves a real verdict rather than `unknown`.
19
+ * That is why `describeResources` is declared among the channel's read paths
20
+ * where aws — whose `describe-stack-resources` returns no tags at all — cannot
21
+ * declare it.
22
+ *
23
+ * `observeResourcesDeep` and `exportResources` are not implemented yet, so
24
+ * they are absent from `reads` rather than listed optimistically: the contract
25
+ * is that a declared path resolves a verdict, and claiming one this plugin has
26
+ * no implementation for would be a lie the checker cannot catch.
27
+ */
28
+ const ownershipChannel = {
29
+ keys: CPLN_TAG_OWNERSHIP_KEYS,
30
+ reads: ["describeResources"],
31
+ };
32
+ export const cplnPlugin = {
33
+ name: "cpln",
34
+ serializer: cplnSerializer,
35
+ // ── Required lifecycle methods ────────────────────────────────
36
+ async generate(options) {
37
+ const { generate, writeGeneratedFiles } = await import("./codegen/generate.js");
38
+ const { dirname } = await import("path");
39
+ const { fileURLToPath } = await import("url");
40
+ const result = await generate(options);
41
+ writeGeneratedFiles(result, dirname(dirname(fileURLToPath(import.meta.url))));
42
+ },
43
+ async validate() {
44
+ const { validate } = await import("./validate.js");
45
+ const { printValidationResult } = await import("@intentius/chant/codegen/validate");
46
+ printValidationResult(await validate());
47
+ },
48
+ async coverage(options) {
49
+ const { analyzeCplnCoverage } = await import("./coverage.js");
50
+ await analyzeCplnCoverage(options);
51
+ },
52
+ async package(options) {
53
+ const { packageLexicon } = await import("./codegen/package.js");
54
+ const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
55
+ const { join, dirname } = await import("path");
56
+ const { fileURLToPath } = await import("url");
57
+ const { spec, stats } = await packageLexicon(options);
58
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
59
+ writeBundleSpec(spec, join(pkgDir, "dist"));
60
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
61
+ },
62
+ // ── Lint and audit ────────────────────────────────────────────
63
+ lintRules() {
64
+ return rules;
65
+ },
66
+ postSynthChecks() {
67
+ return postSynthChecks;
68
+ },
69
+ auditCatalog: () => cplnAuditCatalog,
70
+ // ── Observation ───────────────────────────────────────────────
71
+ ownershipChannel,
72
+ referenceCatalog: cplnReferenceCatalog,
73
+ async describeResources(options) {
74
+ const { describeResources } = await import("./describe-resources.js");
75
+ return describeResources({
76
+ environment: options.environment,
77
+ buildOutput: options.buildOutput,
78
+ entityNames: options.entityNames,
79
+ entities: options.entities,
80
+ owned: options.owned,
81
+ });
82
+ },
83
+ // ── Language surface ──────────────────────────────────────────
84
+ completionProvider(ctx) {
85
+ return completions(ctx);
86
+ },
87
+ hoverProvider(ctx) {
88
+ return hover(ctx);
89
+ },
90
+ // ── Import and project surface ────────────────────────────────
91
+ detectTemplate(data) {
92
+ return detectCplnTemplate(data);
93
+ },
94
+ /**
95
+ * The three `chant init` scaffolds, named at the plugin surface so the
96
+ * available set is visible where a reader looks for it rather than only
97
+ * inside the switch that resolves them. An unrecognised name falls through
98
+ * to the default — `chant init` treats it as a hint, not a key.
99
+ */
100
+ initTemplates(template) {
101
+ if (template === "secrets")
102
+ return cplnInitTemplates("secrets");
103
+ if (template === "stateful")
104
+ return cplnInitTemplates("stateful");
105
+ return cplnInitTemplates();
106
+ },
107
+ skills: createSkillsLoader(import.meta.url, [
108
+ {
109
+ file: "chant-cpln.md",
110
+ name: "chant-cpln",
111
+ description: "Declare Control Plane (cpln) infrastructure from chant — the kinds, GVC scoping, links, and what the serializer emits",
112
+ },
113
+ {
114
+ file: "chant-cpln-workloads.md",
115
+ name: "chant-cpln-workloads",
116
+ description: "Choose a Control Plane workload type and configure autoscaling, Capacity AI, resources and probes correctly",
117
+ },
118
+ {
119
+ file: "chant-cpln-secrets.md",
120
+ name: "chant-cpln-secrets",
121
+ description: "Wire Control Plane secrets, identities and policies so a workload can actually read them",
122
+ },
123
+ ]),
124
+ composites() {
125
+ return compositeCatalog;
126
+ },
127
+ mcpTools() {
128
+ return [
129
+ createDiffTool(cplnSerializer, "Diff the current cpln manifests against the previous build output", "cpln"),
130
+ ];
131
+ },
132
+ mcpResources() {
133
+ return [
134
+ createCatalogResource(import.meta.url, "Control Plane Resource Catalog", "Every cpln resource and property type, with its Control Plane type name", "lexicon-cpln.json", "cpln"),
135
+ ];
136
+ },
137
+ async docs(options) {
138
+ const { generateDocs } = await import("./codegen/docs.js");
139
+ return generateDocs(options);
140
+ },
141
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * How observed cpln resources reference each other, so `chant graph --live` can
3
+ * rebuild the topology from a bag of nodes.
4
+ *
5
+ * Control Plane addresses resources by **link string**
6
+ * (`//gvc/prod/identity/api`), and edge reconstruction matches identifiers
7
+ * exactly, so a raw link never equals the `name` it points at. Rather than add
8
+ * an enrichment pass, `describeResources` resolves each link down to the bare
9
+ * name it ends in and files it under a stable attribute — `refs.identity`,
10
+ * `refs.gvc`, `refs.pullSecrets` — and the rules below read those. See
11
+ * {@link referenceAttributes} in `describe-resources.ts`.
12
+ *
13
+ * Names are unique per kind within an org (per GVC for the GVC-scoped kinds),
14
+ * so `name` is the identity for every kind and `targetKind` disambiguates the
15
+ * cases where two kinds could hold the same name.
16
+ *
17
+ * Containment vs reference matters for the picture: "this workload is in this
18
+ * GVC" is a boundary the renderer draws as a box, not a line from every
19
+ * resource to its GVC. It still declares `viaAttr` so a traversal can walk it —
20
+ * "which GVCs have no workloads" is a containment question.
21
+ */
22
+ import { KINDS, kindByName } from "./kinds.js";
23
+ const typeOf = (kind) => kindByName(kind).typeName;
24
+ export const cplnReferenceCatalog = {
25
+ identities: KINDS.map((kind) => ({ kind: kind.typeName, ids: ["name"] })),
26
+ refs: [
27
+ // Every GVC-scoped resource lives inside its GVC — a boundary, not a line.
28
+ ...KINDS.filter((kind) => kind.gvcScoped).map((kind) => ({
29
+ from: kind.typeName,
30
+ path: "refs.gvc",
31
+ targetKind: typeOf("gvc"),
32
+ relation: "containment",
33
+ label: "in GVC",
34
+ viaAttr: "gvc",
35
+ })),
36
+ // A workload runs as one identity.
37
+ {
38
+ from: typeOf("workload"),
39
+ path: "refs.identity",
40
+ targetKind: typeOf("identity"),
41
+ relation: "reference",
42
+ label: "identity",
43
+ viaAttr: "identityLink",
44
+ },
45
+ // A GVC pulls private images with org-scoped secrets.
46
+ {
47
+ from: typeOf("gvc"),
48
+ path: "refs.pullSecrets[]",
49
+ targetKind: typeOf("secret"),
50
+ relation: "reference",
51
+ label: "pull secret",
52
+ viaAttr: "pullSecretLinks",
53
+ },
54
+ // A domain routes into a GVC, or straight at a stateful workload.
55
+ {
56
+ from: typeOf("domain"),
57
+ path: "refs.gvc",
58
+ targetKind: typeOf("gvc"),
59
+ relation: "reference",
60
+ label: "routes to",
61
+ viaAttr: "gvcLink",
62
+ },
63
+ {
64
+ from: typeOf("domain"),
65
+ path: "refs.workloads[]",
66
+ targetKind: typeOf("workload"),
67
+ relation: "reference",
68
+ label: "routes to",
69
+ viaAttr: "workloadLink",
70
+ },
71
+ // A policy's explicit target links.
72
+ {
73
+ from: typeOf("policy"),
74
+ path: "refs.targets[]",
75
+ targetKind: typeOf("secret"),
76
+ relation: "reference",
77
+ label: "grants on",
78
+ viaAttr: "targetLinks",
79
+ },
80
+ // An IP set fronts the workload it is bound to.
81
+ {
82
+ from: typeOf("ipset"),
83
+ path: "refs.workloads[]",
84
+ targetKind: typeOf("workload"),
85
+ relation: "reference",
86
+ label: "fronts",
87
+ viaAttr: "link",
88
+ },
89
+ // A workload mounts volume sets.
90
+ {
91
+ from: typeOf("workload"),
92
+ path: "refs.volumeSets[]",
93
+ targetKind: typeOf("volumeset"),
94
+ relation: "reference",
95
+ label: "mounts",
96
+ viaAttr: "volumes",
97
+ },
98
+ ],
99
+ };
@@ -0,0 +1,217 @@
1
+ /**
2
+ * cpln serializer — emits the multi-document YAML that `cpln apply --file`
3
+ * consumes.
4
+ *
5
+ * Each declared resource becomes one document:
6
+ *
7
+ * kind: workload
8
+ * name: my-app
9
+ * gvc: my-gvc
10
+ * tags:
11
+ * chant.intentius.io/managed-by: chant
12
+ * spec:
13
+ * containers:
14
+ * - name: main
15
+ * image: nginx:1.27
16
+ *
17
+ * Three things this file is careful about.
18
+ *
19
+ * **Only authoring surface is emitted.** Control Plane's own guidance is to
20
+ * export with `-o yaml-slim` rather than `-o yaml` before re-applying, because
21
+ * the server-side fields (`status`, `id`, `created`, `lastModified`, `links`)
22
+ * break `cpln apply`. Those are attributes in this lexicon rather than
23
+ * properties, so they never reach a document in the first place — the shape a
24
+ * user can declare is already the slim shape.
25
+ *
26
+ * **References become links, not names.** Control Plane addresses resources by
27
+ * link (`//gvc/prod/identity/api`, `//secret/db-password`), and the GVC-scoped
28
+ * forms have to carry the GVC. Passing a declared resource where a link is
29
+ * expected resolves to the right link for its kind, which is the difference
30
+ * between a reference that survives a rename and a hand-spelled string that
31
+ * does not. The identity form matters especially: the bare `//identity/NAME`
32
+ * that reads perfectly plausibly is *silently ignored* by Control Plane's
33
+ * policy engine, and is documented as a common silent failure.
34
+ *
35
+ * **Document order is stable and dependency-first.** `cpln apply` resolves
36
+ * ordering itself for a multi-doc file, so this is not needed for correctness —
37
+ * it is so the emitted file diffs cleanly between builds and reads in the order
38
+ * a person would write it.
39
+ */
40
+ import { isResourceDeclarable } from "@intentius/chant/declarable";
41
+ import { ownershipEntries } from "@intentius/chant/ownership";
42
+ import { walkValue } from "@intentius/chant/serializer-walker";
43
+ import { emitYAML } from "@intentius/chant/yaml";
44
+ import { KINDS, kindByTypeName } from "./kinds.js";
45
+ import { CPLN_TAG_OWNERSHIP_KEYS } from "./ownership.js";
46
+ /**
47
+ * Document order: a GVC before anything inside it, and the org-scoped assets a
48
+ * workload references before the workload. Matches the order the API would
49
+ * accept the documents in one at a time.
50
+ */
51
+ const KIND_ORDER = ["gvc", "secret", "ipset", "policy", "identity", "volumeset", "workload", "domain"];
52
+ /**
53
+ * Manifest key order. `kind` and `name` first because that is how every
54
+ * Control Plane example reads; `spec` last because it is the long one.
55
+ */
56
+ const KEY_ORDER = ["kind", "name", "description", "gvc", "tags"];
57
+ /** Properties consumed to build the envelope rather than copied into the body. */
58
+ const ENVELOPE_KEYS = new Set(["name", "description", "gvc", "tags"]);
59
+ /**
60
+ * `satisfies` rather than a type annotation: `Serializer.serialize` returns
61
+ * `string | SerializerResult`, and cpln always emits a single YAML string.
62
+ * Annotating widens the export to the union and every caller — the tests
63
+ * included — has to narrow it back with a cast that asserts something the
64
+ * implementation already guarantees. `satisfies` checks the shape against the
65
+ * interface and keeps the narrower return type.
66
+ */
67
+ export const cplnSerializer = {
68
+ name: "cpln",
69
+ rulePrefix: "CPL",
70
+ serialize(entities, _outputs, context) {
71
+ // The walker wants Declarable → name; link resolution wants the inverse.
72
+ const entityNames = new Map();
73
+ for (const [name, entity] of entities)
74
+ entityNames.set(entity, name);
75
+ const visitor = cplnVisitor(entities);
76
+ const documents = [];
77
+ for (const [entityName, entity] of entities) {
78
+ const kind = kindByTypeName(entity.entityType);
79
+ // Not ours. A build can mix lexicons, and each serializer sees only the
80
+ // entities routed to it, but skipping defensively costs nothing.
81
+ if (!kind)
82
+ continue;
83
+ const props = readProps(entity);
84
+ const manifest = { kind: kind.kind };
85
+ // `name` falls back to the entity's own name, so a resource declared
86
+ // without one still round-trips rather than emitting `name: undefined`.
87
+ manifest.name = props.name ?? entityName;
88
+ if (props.description !== undefined)
89
+ manifest.description = walkValue(props.description, entityNames, visitor);
90
+ if (kind.gvcScoped)
91
+ manifest.gvc = walkValue(props.gvc, entityNames, visitor);
92
+ const tags = buildTags(props.tags, entityNames, visitor, context);
93
+ if (tags)
94
+ manifest.tags = tags;
95
+ for (const [key, value] of Object.entries(props)) {
96
+ if (ENVELOPE_KEYS.has(key) || value === undefined)
97
+ continue;
98
+ manifest[key] = walkValue(value, entityNames, visitor);
99
+ }
100
+ documents.push({
101
+ order: KIND_ORDER.indexOf(kind.kind),
102
+ name: String(manifest.name),
103
+ text: emitManifest(manifest),
104
+ });
105
+ }
106
+ documents.sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
107
+ return documents.map((d) => d.text).join("\n---\n");
108
+ },
109
+ };
110
+ // ── Tags and ownership ─────────────────────────────────────────────
111
+ /**
112
+ * Merge the declared tags with chant's ownership marker.
113
+ *
114
+ * The marker is stamped even when the resource declares no tags of its own —
115
+ * that is the whole point of it, since an unmarked resource is invisible to
116
+ * the owned-resource prune that lets `chant delete` be precise without a state
117
+ * file.
118
+ */
119
+ function buildTags(declared, entityNames, visitor, context) {
120
+ const walked = declared === undefined ? undefined : walkValue(declared, entityNames, visitor);
121
+ const tags = walked && typeof walked === "object" && !Array.isArray(walked) ? { ...walked } : {};
122
+ if (context?.ownership) {
123
+ Object.assign(tags, ownershipEntries(CPLN_TAG_OWNERSHIP_KEYS, context.ownership));
124
+ }
125
+ return Object.keys(tags).length > 0 ? tags : undefined;
126
+ }
127
+ // ── References ─────────────────────────────────────────────────────
128
+ /**
129
+ * Build the Control Plane link for a declared resource.
130
+ *
131
+ * GVC-scoped kinds nest under their GVC. For identities in particular the
132
+ * nested form is not cosmetic: a policy binding written against the bare
133
+ * `//identity/NAME` is accepted and then ignored.
134
+ */
135
+ export function cplnLink(kind, name, gvc) {
136
+ if (kind.kind === "gvc")
137
+ return `//gvc/${name}`;
138
+ if (kind.gvcScoped) {
139
+ if (typeof gvc !== "string" || gvc.length === 0) {
140
+ throw new Error(`Cannot build a link to ${kind.kind} "${name}": its \`gvc\` is not a plain string at build time. ` +
141
+ `GVC-scoped links must be \`//gvc/<gvc>/${kind.kind}/<name>\`, and the bare \`//${kind.kind}/<name>\` ` +
142
+ `form is silently ignored by Control Plane.`);
143
+ }
144
+ return `//gvc/${gvc}/${kind.kind}/${name}`;
145
+ }
146
+ return `//${kind.kind}/${name}`;
147
+ }
148
+ /**
149
+ * Visitor for the generic serializer walker.
150
+ *
151
+ * Property declarables unwrap to plain objects; resource references resolve to
152
+ * Control Plane links; attribute references have nowhere to go.
153
+ */
154
+ function cplnVisitor(entities) {
155
+ const linkFor = (logicalName) => {
156
+ const target = entities.get(logicalName);
157
+ const kind = target && kindByTypeName(target.entityType);
158
+ if (!target || !kind) {
159
+ // A reference to something this lexicon does not model. The logical name
160
+ // is the most useful thing left to emit, and a post-synth check reports
161
+ // links that do not resolve.
162
+ return logicalName;
163
+ }
164
+ const props = readProps(target);
165
+ const name = typeof props.name === "string" ? props.name : logicalName;
166
+ return cplnLink(kind, name, props.gvc);
167
+ };
168
+ return {
169
+ attrRef: (name, attribute) => {
170
+ throw new Error(`Cannot serialize a reference to ${name}.${attribute}: Control Plane manifests have no ` +
171
+ `template-time reference language, so an attribute known only after apply cannot be embedded in one. ` +
172
+ `Reference the resource itself — it resolves to a link — or read the attribute after apply.`);
173
+ },
174
+ resourceRef: linkFor,
175
+ propertyDeclarable: (entity, walk) => {
176
+ const props = isResourceDeclarable(entity) ? entity.props : undefined;
177
+ if (!props || typeof props !== "object")
178
+ return undefined;
179
+ const result = {};
180
+ for (const [key, value] of Object.entries(props)) {
181
+ if (value !== undefined)
182
+ result[key] = walk(value);
183
+ }
184
+ return Object.keys(result).length > 0 ? result : undefined;
185
+ },
186
+ };
187
+ }
188
+ // ── Emission ───────────────────────────────────────────────────────
189
+ function readProps(entity) {
190
+ const props = isResourceDeclarable(entity) ? entity.props : undefined;
191
+ return props && typeof props === "object" ? props : {};
192
+ }
193
+ /** Emit one manifest document, ordered keys first and the rest alphabetically. */
194
+ function emitManifest(manifest) {
195
+ const lines = [];
196
+ for (const key of KEY_ORDER) {
197
+ if (manifest[key] !== undefined)
198
+ lines.push(emitKeyValue(key, manifest[key]));
199
+ }
200
+ const rest = Object.keys(manifest)
201
+ .filter((k) => !KEY_ORDER.includes(k))
202
+ .sort();
203
+ for (const key of rest) {
204
+ if (manifest[key] !== undefined)
205
+ lines.push(emitKeyValue(key, manifest[key]));
206
+ }
207
+ return `${lines.join("\n")}\n`;
208
+ }
209
+ /** Scalars get `key: value`; blocks get the value indented beneath the key. */
210
+ function emitKeyValue(key, value) {
211
+ const yaml = emitYAML(value, 1);
212
+ return yaml.startsWith("\n") ? `${key}:${yaml}` : `${key}: ${yaml}`;
213
+ }
214
+ /** Kind order used for document sorting, exported for the serializer tests. */
215
+ export const DOCUMENT_KIND_ORDER = KIND_ORDER;
216
+ /** Every modelled kind, re-exported so consumers need not reach into `kinds`. */
217
+ export const SERIALIZED_KINDS = KINDS;
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/spec/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAQH,eAAO,MAAM,UAAU,qCAAqC,CAAC;AAS7D,eAAO,MAAM,aAAa,QAA8E,CAAC;AAEzG,yEAAyE;AACzE,eAAO,MAAM,QAAQ,kBAAkB,CAAC;AAExC;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAoB9F"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/spec/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAQH,eAAO,MAAM,UAAU,qCAAqC,CAAC;AAS7D,eAAO,MAAM,aAAa,QAAuE,CAAC;AAElG,yEAAyE;AACzE,eAAO,MAAM,QAAQ,kBAAkB,CAAC;AAExC;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAoB9F"}
@@ -0,0 +1,70 @@
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
+ import { join } from "path";
32
+ import { homedir } from "os";
33
+ import { readFileSync } from "fs";
34
+ import { moduleSourceDir } from "@intentius/chant/module-source";
35
+ import { fetchWithCache } from "@intentius/chant/codegen/fetch";
36
+ export const SCHEMA_URL = "https://api.cpln.io/openapi.json";
37
+ /**
38
+ * Cache under `~/.chant` (like fly and k8s) so CI's `~/.chant` schema cache
39
+ * covers it — an api.cpln.io hiccup then falls back to the cache before it
40
+ * falls back to the committed snapshot.
41
+ */
42
+ const CACHE_FILE = join(homedir(), ".chant", "cpln-openapi.json");
43
+ export const SNAPSHOT_FILE = join(moduleSourceDir(import.meta.url), "cpln-openapi.snapshot.json");
44
+ /** The single map key the parser is handed. The spec is one document. */
45
+ export const SPEC_KEY = "Cpln::OpenAPI";
46
+ /**
47
+ * Fetch the Control Plane OpenAPI spec, falling back to the committed pruned
48
+ * snapshot when the endpoint is unreachable. Returns a single-entry map — the
49
+ * whole spec is one document; the parser fans it out into per-kind results.
50
+ */
51
+ export async function fetchSchemas(options) {
52
+ let raw;
53
+ try {
54
+ raw = await fetchWithCache({ url: SCHEMA_URL, cacheFile: CACHE_FILE }, options?.force);
55
+ // A 404 body is still a body. Parsing proves we got a spec and not an error
56
+ // page that would otherwise be cached and then generated from.
57
+ const parsed = JSON.parse(raw.toString("utf-8"));
58
+ if (!parsed.components?.schemas) {
59
+ throw new Error("response carried no components.schemas");
60
+ }
61
+ console.error(`[cpln] spec: live ${SCHEMA_URL}`);
62
+ }
63
+ catch (err) {
64
+ console.error(`[cpln] spec: committed snapshot — could not fetch ${SCHEMA_URL} ` +
65
+ `(${err instanceof Error ? err.message.split("\n")[0] : String(err)}). ` +
66
+ `Refresh it with \`just snapshot\` on a networked machine.`);
67
+ raw = readFileSync(SNAPSHOT_FILE);
68
+ }
69
+ return new Map([[SPEC_KEY, raw]]);
70
+ }