@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,52 @@
1
+ import { readArray, readString } from "../../entity-props.js";
2
+ import { WORKLOAD, entitiesOfType } from "./helpers.js";
3
+ /**
4
+ * CPL041: registry prefixes Control Plane does not want in a workload spec.
5
+ *
6
+ * Two documented traps. `docker.io/` must never be added to a public image —
7
+ * the exact string `nginx:latest` works and `docker.io/library/nginx:latest`
8
+ * does not. And `<org>.registry.cpln.io/…` is the hostname for `docker login`
9
+ * and `docker push` only; inside a workload spec the org's own images are
10
+ * addressed as `//image/NAME:TAG`.
11
+ */
12
+ export const imageReferenceFormCheck = {
13
+ id: "CPL041",
14
+ description: "Image references must not use docker.io/ or the registry hostname form",
15
+ check(ctx) {
16
+ const diagnostics = [];
17
+ for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
18
+ for (const container of readArray(entity, "spec", "containers")) {
19
+ const containerName = readString(container, "name") ?? "?";
20
+ const image = readString(container, "image");
21
+ if (!image)
22
+ continue;
23
+ if (image.startsWith("docker.io/")) {
24
+ const bare = image.replace(/^docker\.io\/(library\/)?/, "");
25
+ diagnostics.push({
26
+ checkId: "CPL041",
27
+ severity: "error",
28
+ message: `Workload "${name}" container "${containerName}" uses image "${image}". Control Plane rejects ` +
29
+ `the docker.io/ prefix on public images — use "${bare}".`,
30
+ entity: name,
31
+ lexicon: "cpln",
32
+ });
33
+ continue;
34
+ }
35
+ const selfHosted = /^([a-z0-9-]+)\.registry\.cpln\.io\/(.+)$/.exec(image);
36
+ if (selfHosted) {
37
+ diagnostics.push({
38
+ checkId: "CPL041",
39
+ severity: "warning",
40
+ message: `Workload "${name}" container "${containerName}" uses image "${image}". The ` +
41
+ `<org>.registry.cpln.io hostname is for \`docker login\` and \`docker push\`; in a workload ` +
42
+ `spec, an image in this org is "//image/${selfHosted[2]}". (A cross-org pull does use the ` +
43
+ `hostname form — ignore this if "${selfHosted[1]}" is another org.)`,
44
+ entity: name,
45
+ lexicon: "cpln",
46
+ });
47
+ }
48
+ }
49
+ }
50
+ return diagnostics;
51
+ },
52
+ };
@@ -0,0 +1,49 @@
1
+ import { readArray, readPath, readString } from "../../entity-props.js";
2
+ import { GVC, entitiesOfType } from "./helpers.js";
3
+ /** Location links look like `/org/ORG/location/aws-us-east-1` or `//location/aws-us-east-1`. */
4
+ const LOCATION_LINK = /\/location\/((aws|gcp|azure)-[a-z0-9-]+|[a-z0-9-]+)$/;
5
+ /**
6
+ * CPL042: a GVC with nowhere to run.
7
+ *
8
+ * Placement is a GVC-level decision, and a GVC with neither `locationLinks` nor
9
+ * a `locationQuery` accepts workloads and schedules them nowhere. The workloads
10
+ * apply cleanly and never become ready.
11
+ */
12
+ export const gvcPlacementCheck = {
13
+ id: "CPL042",
14
+ description: "A GVC must declare where its workloads run",
15
+ check(ctx) {
16
+ const diagnostics = [];
17
+ for (const [entityName, entity] of entitiesOfType(ctx.entities, GVC)) {
18
+ const name = readString(entity, "name") ?? entityName;
19
+ const links = readArray(entity, "spec", "staticPlacement", "locationLinks");
20
+ const query = readPath(entity, "spec", "staticPlacement", "locationQuery");
21
+ if (links.length === 0 && query === undefined) {
22
+ diagnostics.push({
23
+ checkId: "CPL042",
24
+ severity: "error",
25
+ message: `GVC "${name}" declares no placement. Set spec.staticPlacement.locationLinks (or a ` +
26
+ `locationQuery) — without one, its workloads apply cleanly and are scheduled nowhere.`,
27
+ entity: entityName,
28
+ lexicon: "cpln",
29
+ });
30
+ continue;
31
+ }
32
+ for (const link of links) {
33
+ if (typeof link !== "string")
34
+ continue;
35
+ if (LOCATION_LINK.test(link))
36
+ continue;
37
+ diagnostics.push({
38
+ checkId: "CPL042",
39
+ severity: "warning",
40
+ message: `GVC "${name}" has location link "${link}", which does not look like a location. Expected ` +
41
+ `/org/<org>/location/<provider>-<region>, e.g. /org/acme/location/aws-us-east-1.`,
42
+ entity: entityName,
43
+ lexicon: "cpln",
44
+ });
45
+ }
46
+ }
47
+ return diagnostics;
48
+ },
49
+ };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Shared reading and parsing for the CPL post-synth checks.
3
+ *
4
+ * Each check lives in its own file named for its id, so `CPL027` is greppable
5
+ * to one place. What they have in common lands here rather than being repeated
6
+ * or, worse, drifting: the workload-type default, the port enumeration that has
7
+ * to span both `port` and `ports`, and the quantity parsers.
8
+ *
9
+ * Named `helpers.ts` deliberately — the completeness checker's post-synth count
10
+ * excludes that filename, so a support module is not miscounted as a check.
11
+ */
12
+ import { readArray, readNumber, readPath, readString } from "../../entity-props.js";
13
+ import { kindByName } from "../../kinds.js";
14
+ export const WORKLOAD = kindByName("workload").typeName;
15
+ export const GVC = kindByName("gvc").typeName;
16
+ export const POLICY = kindByName("policy").typeName;
17
+ export const DOMAIN = kindByName("domain").typeName;
18
+ export const VOLUMESET = kindByName("volumeset").typeName;
19
+ /** Control Plane defaults an unset workload type to `serverless`. */
20
+ export function workloadType(entity) {
21
+ return readString(entity, "spec", "type") ?? "serverless";
22
+ }
23
+ /** The autoscaling block from `defaultOptions`. */
24
+ export function autoscalingOf(entity) {
25
+ return readPath(entity, "spec", "defaultOptions", "autoscaling");
26
+ }
27
+ /** Every port a workload's containers expose, across both `port` and `ports`. */
28
+ export function exposedPorts(entity) {
29
+ const ports = [];
30
+ for (const container of readArray(entity, "spec", "containers")) {
31
+ const single = readNumber(container, "port");
32
+ if (single !== undefined)
33
+ ports.push(single);
34
+ for (const port of readArray(container, "ports")) {
35
+ const number = readNumber(port, "number");
36
+ if (number !== undefined)
37
+ ports.push(number);
38
+ }
39
+ }
40
+ return ports;
41
+ }
42
+ /** Parse a CPU quantity (`"50m"`, `"1"`, `"1.5"`) to millicores. */
43
+ export function parseCpuMillicores(value) {
44
+ if (!value)
45
+ return undefined;
46
+ const match = /^(\d+(?:\.\d+)?)(m?)$/.exec(value.trim());
47
+ if (!match)
48
+ return undefined;
49
+ const amount = Number(match[1]);
50
+ return match[2] === "m" ? amount : amount * 1000;
51
+ }
52
+ /** Parse a memory quantity (`"128Mi"`, `"1Gi"`, `"512M"`) to MiB. */
53
+ export function parseMemoryMib(value) {
54
+ if (!value)
55
+ return undefined;
56
+ const match = /^(\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti|K|M|G|T)?$/.exec(value.trim());
57
+ if (!match)
58
+ return undefined;
59
+ const amount = Number(match[1]);
60
+ const factors = {
61
+ Ki: 1 / 1024,
62
+ Mi: 1,
63
+ Gi: 1024,
64
+ Ti: 1024 * 1024,
65
+ // Decimal suffixes, converted to MiB so one comparison covers both families.
66
+ K: 1000 / (1024 * 1024),
67
+ M: 1000000 / (1024 * 1024),
68
+ G: 1000000000 / (1024 * 1024),
69
+ T: 1000000000000 / (1024 * 1024),
70
+ };
71
+ const suffix = match[2];
72
+ if (!suffix)
73
+ return amount / (1024 * 1024); // bare bytes
74
+ return amount * factors[suffix];
75
+ }
76
+ /**
77
+ * The tag of an image reference, or undefined when untagged.
78
+ *
79
+ * A colon in a registry host's port (`registry:5000/app`) is not a tag, so only
80
+ * a colon after the last slash counts.
81
+ */
82
+ export function imageTag(image) {
83
+ const lastSlash = image.lastIndexOf("/");
84
+ const colon = image.indexOf(":", lastSlash + 1);
85
+ return colon === -1 ? undefined : image.slice(colon + 1);
86
+ }
87
+ /** `//gvc/prod/identity/api` → kind `identity`, name `api`, gvc `prod`. */
88
+ const GVC_SCOPED_LINK = /^\/\/gvc\/([^/]+)\/([a-z]+)\/([^/]+)$/;
89
+ /** `//secret/db-password` → kind `secret`, name `db-password`. */
90
+ const ORG_SCOPED_LINK = /^\/\/([a-z]+)\/([^/]+)$/;
91
+ /** Parse a Control Plane link, or return undefined if it is not one. */
92
+ export function parseLink(value) {
93
+ const scoped = GVC_SCOPED_LINK.exec(value);
94
+ if (scoped)
95
+ return { gvc: scoped[1], kind: scoped[2], name: scoped[3] };
96
+ const org = ORG_SCOPED_LINK.exec(value);
97
+ if (org)
98
+ return { kind: org[1], name: org[2] };
99
+ return undefined;
100
+ }
101
+ /** Iterate entities of one type. Re-exported so checks import one module. */
102
+ export { entitiesOfType } from "../../entity-props.js";
@@ -0,0 +1,42 @@
1
+ import { unrestrictedOutboundCheck } from "./cpl010-unrestricted-outbound.js";
2
+ import { internalFirewallScopeCheck } from "./cpl011-internal-firewall-scope.js";
3
+ import { noLiteralCredentialEnvCheck } from "./cpl012-no-literal-credential-env.js";
4
+ import { qualifiedIdentityPrincipalCheck } from "./cpl013-qualified-identity-principal.js";
5
+ import { qualifiedSecretRefCheck } from "./cpl014-qualified-secret-ref.js";
6
+ import { serverlessSinglePortCheck } from "./cpl020-serverless-single-port.js";
7
+ import { cronJobShapeCheck } from "./cpl021-cron-job-shape.js";
8
+ import { containerPortsCheck } from "./cpl022-container-ports.js";
9
+ import { containerResourcesCheck } from "./cpl023-container-resources.js";
10
+ import { probeHandlerCheck } from "./cpl024-probe-handler.js";
11
+ import { autoscalingShapeCheck } from "./cpl025-autoscaling-shape.js";
12
+ import { scaleToZeroCheck } from "./cpl026-scale-to-zero.js";
13
+ import { capacityAiConflictCheck } from "./cpl027-capacity-ai-conflict.js";
14
+ import { volumeSetCheck } from "./cpl028-volume-set.js";
15
+ import { linkTargetsCheck } from "./cpl029-link-targets.js";
16
+ import { domainRoutingCheck } from "./cpl030-domain-routing.js";
17
+ import { pinnedImageCheck } from "./cpl040-pinned-image.js";
18
+ import { imageReferenceFormCheck } from "./cpl041-image-reference-form.js";
19
+ import { gvcPlacementCheck } from "./cpl042-gvc-placement.js";
20
+ import { policyScopeCheck } from "./cpl043-policy-scope.js";
21
+ export const postSynthChecks = [
22
+ unrestrictedOutboundCheck,
23
+ internalFirewallScopeCheck,
24
+ noLiteralCredentialEnvCheck,
25
+ qualifiedIdentityPrincipalCheck,
26
+ qualifiedSecretRefCheck,
27
+ serverlessSinglePortCheck,
28
+ cronJobShapeCheck,
29
+ containerPortsCheck,
30
+ containerResourcesCheck,
31
+ probeHandlerCheck,
32
+ autoscalingShapeCheck,
33
+ scaleToZeroCheck,
34
+ capacityAiConflictCheck,
35
+ volumeSetCheck,
36
+ linkTargetsCheck,
37
+ domainRoutingCheck,
38
+ pinnedImageCheck,
39
+ imageReferenceFormCheck,
40
+ gvcPlacementCheck,
41
+ policyScopeCheck,
42
+ ];
@@ -0,0 +1,73 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /**
3
+ * CPL001: no literal credential material in a cpln declaration.
4
+ *
5
+ * This is a source-level rule rather than a post-synth check on purpose. By
6
+ * the time the manifest exists, a literal is just a string in a `data` block
7
+ * and there is nothing left to point at; here there is a file and a line, which
8
+ * is what makes the finding actionable — and, more to the point, this is the
9
+ * form the credential is in when it enters git history.
10
+ *
11
+ * `Secret` is the obvious carrier, but a credential pasted into a workload's
12
+ * env or an identity's cloud config is the same leak, so the rule fires
13
+ * anywhere inside a `new <cpln kind>(…)`.
14
+ *
15
+ * Complementary to CPL012, which catches the shape this cannot see — a
16
+ * plausible-looking value in a credential-named env var — from the model.
17
+ */
18
+ const CPLN_KINDS = new Set(["Gvc", "Workload", "Identity", "VolumeSet", "Secret", "Policy", "Domain", "IpSet"]);
19
+ const CREDENTIAL_SHAPES = [
20
+ { pattern: /^-----BEGIN [A-Z ]*PRIVATE KEY-----/, label: "private key material" },
21
+ { pattern: /^AKIA[0-9A-Z]{16}$/, label: "AWS access key id" },
22
+ { pattern: /^(ghp|gho|ghs|ghu)_[A-Za-z0-9]{20,}$/, label: "GitHub token" },
23
+ { pattern: /^github_pat_[A-Za-z0-9_]{20,}$/, label: "GitHub fine-grained token" },
24
+ { pattern: /^sk-[A-Za-z0-9_-]{20,}$/, label: "secret API key (sk-)" },
25
+ { pattern: /^xox[baprs]-[A-Za-z0-9-]{10,}$/, label: "Slack token" },
26
+ { pattern: /^ya29\.[A-Za-z0-9_-]{20,}$/, label: "Google OAuth access token" },
27
+ { pattern: /^eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}$/, label: "JWT" },
28
+ {
29
+ pattern: /^postgres(ql)?:\/\/[^:@/\s]+:[^@/\s]+@/,
30
+ label: "database URL with an inline password",
31
+ },
32
+ { pattern: /^mysql:\/\/[^:@/\s]+:[^@/\s]+@/, label: "database URL with an inline password" },
33
+ { pattern: /^mongodb(\+srv)?:\/\/[^:@/\s]+:[^@/\s]+@/, label: "database URL with an inline password" },
34
+ ];
35
+ export const noSecretLiteralsRule = {
36
+ id: "CPL001",
37
+ severity: "error",
38
+ category: "security",
39
+ description: "No literal credential material in cpln declarations — reference a secret instead",
40
+ check(context) {
41
+ const diagnostics = [];
42
+ const source = context.sourceFile;
43
+ const visit = (node, insideCplnNew) => {
44
+ let inside = insideCplnNew;
45
+ if (ts.isNewExpression(node)) {
46
+ const name = node.expression.getText(source);
47
+ const short = name.split(".").pop() ?? name;
48
+ if (CPLN_KINDS.has(short))
49
+ inside = true;
50
+ }
51
+ if (inside && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))) {
52
+ for (const { pattern, label } of CREDENTIAL_SHAPES) {
53
+ if (!pattern.test(node.text))
54
+ continue;
55
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
56
+ diagnostics.push({
57
+ ruleId: "CPL001",
58
+ severity: "error",
59
+ message: `Literal ${label} in a cpln declaration. Store it as a Secret and reference it as ` +
60
+ `\`cpln://secret/NAME.FIELD\` — a literal here is a credential in git history.`,
61
+ file: context.filePath,
62
+ line: line + 1,
63
+ column: character + 1,
64
+ });
65
+ break;
66
+ }
67
+ }
68
+ ts.forEachChild(node, (child) => visit(child, inside));
69
+ };
70
+ visit(source, false);
71
+ return diagnostics;
72
+ },
73
+ };
@@ -0,0 +1,60 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /**
3
+ * CPL002: prefer a resource reference over a hand-written link.
4
+ *
5
+ * The cpln serializer resolves a declared resource passed where a link is
6
+ * expected into the correct link for its kind — including the GVC qualifier
7
+ * that the identity form needs and that is silently ignored when missing. A
8
+ * hand-spelled `"//gvc/prod/identity/api"` gets none of that: it does not
9
+ * follow a rename, it is not checked against anything at author time, and
10
+ * getting the shape subtly wrong fails at runtime rather than at apply.
11
+ *
12
+ * This is a style rule, not a correctness one — the literal form is valid, and
13
+ * is the only option for a resource this stack does not declare. It fires on
14
+ * the link syntax specifically, so ordinary strings are untouched.
15
+ *
16
+ * Not flagged: `cpln://secret/…` and `cpln://volumeset/…`. Those are runtime
17
+ * *resolution* URIs read by the container, not links between resources, and
18
+ * they have no reference form to prefer.
19
+ */
20
+ const CPLN_KINDS = new Set(["Gvc", "Workload", "Identity", "VolumeSet", "Secret", "Policy", "Domain", "IpSet"]);
21
+ /** `//gvc/prod/identity/api` or `//secret/db-password`. */
22
+ const LINK = /^\/\/(?:gvc\/[^/\s]+\/)?(gvc|workload|identity|volumeset|secret|policy|domain|ipset)\/[^/\s]+$/;
23
+ export const preferResourceReferenceRule = {
24
+ id: "CPL002",
25
+ severity: "info",
26
+ category: "style",
27
+ description: "Prefer passing a declared resource over a hand-written Control Plane link",
28
+ check(context) {
29
+ const diagnostics = [];
30
+ const source = context.sourceFile;
31
+ const visit = (node, insideCplnNew) => {
32
+ let inside = insideCplnNew;
33
+ if (ts.isNewExpression(node)) {
34
+ const name = node.expression.getText(source);
35
+ const short = name.split(".").pop() ?? name;
36
+ if (CPLN_KINDS.has(short))
37
+ inside = true;
38
+ }
39
+ if (inside && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))) {
40
+ const match = LINK.exec(node.text);
41
+ if (match) {
42
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
43
+ diagnostics.push({
44
+ ruleId: "CPL002",
45
+ severity: "info",
46
+ message: `Hand-written link "${node.text}". If the ${match[1]} is declared in this stack, pass the ` +
47
+ `resource itself — the serializer emits the correct link, including the GVC qualifier, and ` +
48
+ `the reference follows a rename.`,
49
+ file: context.filePath,
50
+ line: line + 1,
51
+ column: character + 1,
52
+ });
53
+ }
54
+ }
55
+ ts.forEachChild(node, (child) => visit(child, inside));
56
+ };
57
+ visit(source, false);
58
+ return diagnostics;
59
+ },
60
+ };
@@ -0,0 +1,6 @@
1
+ import { noSecretLiteralsRule } from "./cpl001-no-secret-literals.js";
2
+ import { preferResourceReferenceRule } from "./cpl002-prefer-resource-reference.js";
3
+ export { noSecretLiteralsRule } from "./cpl001-no-secret-literals.js";
4
+ export { preferResourceReferenceRule } from "./cpl002-prefer-resource-reference.js";
5
+ /** All source-level lint rules provided by this lexicon (imported by plugin.ts). */
6
+ export const rules = [noSecretLiteralsRule, preferResourceReferenceRule];
@@ -1 +1 @@
1
- {"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAmBpF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAEpE"}
1
+ {"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAmBpF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAEpE"}
@@ -0,0 +1,24 @@
1
+ import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
3
+ import { LexiconIndex, lexiconCompletions } from "@intentius/chant/lsp/lexicon-providers";
4
+ const require = createRequire(moduleSourceUrl(import.meta.url));
5
+ let cachedIndex = null;
6
+ /**
7
+ * The generated registry, loaded once. It carries every cpln resource and
8
+ * property type with its constraints, which is exactly what the shared
9
+ * completion provider indexes — so completions cannot drift from the types.
10
+ */
11
+ function getIndex() {
12
+ if (cachedIndex)
13
+ return cachedIndex;
14
+ const data = require("../generated/lexicon-cpln.json");
15
+ cachedIndex = new LexiconIndex(data);
16
+ return cachedIndex;
17
+ }
18
+ /**
19
+ * LSP completions for cpln resources — class names after `new `, property
20
+ * names inside a constructor.
21
+ */
22
+ export function completions(ctx) {
23
+ return lexiconCompletions(ctx, getIndex(), "cpln resource");
24
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AA6H1E,wDAAwD;AACxD,wBAAgB,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAE9D"}
1
+ {"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AA6H1E,wDAAwD;AACxD,wBAAgB,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAE9D"}
@@ -0,0 +1,106 @@
1
+ import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
3
+ import { LexiconIndex, lexiconHover } from "@intentius/chant/lsp/lexicon-providers";
4
+ import { KINDS, kindByClassName } from "../kinds.js";
5
+ const require = createRequire(moduleSourceUrl(import.meta.url));
6
+ let cachedIndex = null;
7
+ function getIndex() {
8
+ if (cachedIndex)
9
+ return cachedIndex;
10
+ const data = require("../generated/lexicon-cpln.json");
11
+ cachedIndex = new LexiconIndex(data);
12
+ return cachedIndex;
13
+ }
14
+ /**
15
+ * Per-kind notes.
16
+ *
17
+ * These carry the rules an author cannot infer from the property names, and
18
+ * specifically the ones whose violation fails *silently* — an unqualified
19
+ * identity link, a scale-to-zero that never happens, an immutable field. The
20
+ * property list is already in the types; what hover adds is the part the types
21
+ * cannot say.
22
+ */
23
+ const KIND_NOTES = {
24
+ Gvc: "The placement and networking boundary. Workloads, identities and volume sets live inside one.\n\n" +
25
+ "Placement is set here, not per workload (`spec.staticPlacement.locationLinks`, e.g. " +
26
+ "`/org/acme/location/aws-us-east-1`). **Pull secrets are also GVC-level** (`spec.pullSecretLinks`) — " +
27
+ "only `docker`, `ecr` and `gcp` secret types are valid. KEDA must be enabled here before a standard or " +
28
+ "stateful workload in the GVC can use it.",
29
+ Workload: "A running unit of work. **`type` and `name` are immutable** — changing either means delete and recreate.\n\n" +
30
+ "- `serverless` — exactly one HTTP port, required. Scales to zero with `rps` or `concurrency`.\n" +
31
+ "- `standard` — multiple ports, non-HTTP allowed. Scale to zero via KEDA only.\n" +
32
+ "- `stateful` — stable replica identity; the only type that mounts `ext4`/`xfs` volumes or uses " +
33
+ "`replicaDirect` load balancing.\n" +
34
+ "- `cron` — must set `spec.job.schedule` and expose no ports. Probes, autoscaling and `timeoutSeconds` " +
35
+ "are accepted and ignored.\n\n" +
36
+ "Capacity AI is **on by default** for serverless, standard and cron, and conflicts with CPU-utilization " +
37
+ "autoscaling, multi-metric autoscaling, and GPUs. Memory(MiB)/CPU(millicores) must be ≤ 8.",
38
+ Identity: "A workload's identity: cloud-provider access, network resources, and the principal a policy grants to.\n\n" +
39
+ "**GVC-scoped and not shareable** — an identity cannot be used from another GVC; declare one per GVC with " +
40
+ "the same spec. A workload has at most one. Each provider section allows one account " +
41
+ "(AWS `roleName` ⊻ `policyRefs`, GCP `serviceAccount` ⊻ `bindings`).\n\n" +
42
+ "In a policy binding the principal must be `//gvc/GVC/identity/NAME`. The bare `//identity/NAME` is " +
43
+ "**silently ignored** (CPL013).",
44
+ VolumeSet: "Persistent storage inside one GVC.\n\n" +
45
+ "**`fileSystemType` and `performanceClass` are both immutable** — changing either means delete, recreate, " +
46
+ "and data loss. `ext4`/`xfs` are RWO and bind to exactly one *stateful* workload; `shared` is RWX, works " +
47
+ "with any type, and supports no snapshots. `high-throughput-ssd` has a 200 GB floor where the others have " +
48
+ "10 GB. Mount with `cpln://volumeset/NAME`.",
49
+ Secret: "An org-scoped secret, in one of 12 types.\n\n" +
50
+ "Reading one at runtime takes three steps, and **missing any one fails silently**: the workload has an " +
51
+ "identity, a policy grants that identity `reveal` on the secret, and the reference is field-qualified — " +
52
+ "`cpln://secret/NAME.payload`, not `cpln://secret/NAME` (CPL014).",
53
+ Policy: "Binds permissions to principals over a target.\n\n" +
54
+ "`targetKind` is singular and lowercase. Pick exactly one scope: `target: \"all\"`, `targetLinks`, or " +
55
+ "`targetQuery`. `ipset` and `mk8s` are **not** valid targets — they are governed through their parent. " +
56
+ "Permissions in a binding must be sorted and unique. Never set `origin` by hand.",
57
+ Domain: "A custom domain with its TLS, CORS and routing.\n\n" +
58
+ "**Apex domains must use `dnsMode: cname`** — NS does not support apex. NS mode requires " +
59
+ "`certChallengeType: dns01`. `gvcLink`, `workloadLink` and `ports[].routes` are mutually exclusive, and " +
60
+ "`workloadLink` (replica-direct) is stateful-only. Every route must target workloads in one GVC.",
61
+ IpSet: "Dedicated IP addresses reserved in named locations and bound to a workload.\n\n" +
62
+ "Not a valid policy target — access is governed through the workload it fronts.",
63
+ };
64
+ /** Enum-valued props are worth spelling out inline — they are the typo surface. */
65
+ function enumLines(entry) {
66
+ const constraints = entry.propertyConstraints;
67
+ if (!constraints)
68
+ return [];
69
+ const lines = [];
70
+ for (const [prop, constraint] of Object.entries(constraints)) {
71
+ if (!Array.isArray(constraint?.enum) || constraint.enum.length === 0)
72
+ continue;
73
+ lines.push(`- \`${prop}\`: ${constraint.enum.map((v) => `\`${String(v)}\``).join(" | ")}`);
74
+ }
75
+ return lines;
76
+ }
77
+ function cplnHover(className, entry) {
78
+ const lines = [`**${className}**`, "", `Control Plane type: \`${entry.resourceType}\``];
79
+ const kind = kindByClassName(className);
80
+ if (!kind) {
81
+ // A property type — a nested shape rather than a standalone declarable.
82
+ // Naming its owner is the useful part, since the names are path-derived.
83
+ const owner = KINDS.find((k) => className.startsWith(k.className));
84
+ lines.push("", owner
85
+ ? `*Property type — a nested shape within \`${owner.className}\`, not a standalone declarable.*`
86
+ : "*Property type — a nested shape, not a standalone declarable.*");
87
+ const propertyEnums = enumLines(entry);
88
+ if (propertyEnums.length > 0)
89
+ lines.push("", "Enumerated properties:", ...propertyEnums);
90
+ return { contents: lines.join("\n") };
91
+ }
92
+ lines.push("", `Manifest kind: \`${kind.kind}\``);
93
+ if (kind.gvcScoped)
94
+ lines.push("", "GVC-scoped — requires a `gvc`.");
95
+ const note = KIND_NOTES[className];
96
+ if (note)
97
+ lines.push("", note);
98
+ const enums = enumLines(entry);
99
+ if (enums.length > 0)
100
+ lines.push("", "Enumerated properties:", ...enums);
101
+ return { contents: lines.join("\n") };
102
+ }
103
+ /** Provide LSP hover information for cpln resources. */
104
+ export function hover(ctx) {
105
+ return lexiconHover(ctx, getIndex(), cplnHover);
106
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cpln",
3
- "version": "0.124.31",
3
+ "version": "0.124.32",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Cpln"
6
6
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The cpln lexicon's ownership convention.
3
+ *
4
+ * Declared here rather than in core — the same seam aws (`chant:managed-by`),
5
+ * azure and fly sit on. Core owns the generic stamp/detect logic; each lexicon
6
+ * owns key names its provider will actually accept.
7
+ *
8
+ * Control Plane gives every kind a free-form `tags` map, which makes this the
9
+ * cleanest ownership channel of any target chant supports: one uniform place on
10
+ * all eight kinds, readable straight off a plain `GET`, with no per-kind
11
+ * special case of the sort fly needs for volumes and certificates.
12
+ *
13
+ * Key naming. Tag keys accept a `/`, which Control Plane uses itself for its
14
+ * reserved `cpln/…` keys (`cpln/relaxMemoryToCpuRatio`). A prefixed key is
15
+ * therefore both idiomatic and safely namespaced against a future reserved key,
16
+ * so these follow the same `chant.intentius.io/…` convention as the shared
17
+ * label channel rather than inventing a flat spelling. The `cpln/` prefix is
18
+ * Control Plane's own and is never written here.
19
+ */
20
+ /** Tag keys carrying chant's ownership marker on every cpln resource. */
21
+ export const CPLN_TAG_OWNERSHIP_KEYS = {
22
+ managedBy: "chant.intentius.io/managed-by",
23
+ stack: "chant.intentius.io/stack",
24
+ env: "chant.intentius.io/env",
25
+ };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Entry point for `npm run bundle` — generates `src/generated/` and writes the
4
+ * `dist/` bundle.
5
+ *
6
+ * Top-level await rather than an async wrapper, matching the other lexicons:
7
+ * an async main() leaves event-loop references that keep the process alive
8
+ * after the bundle is written.
9
+ */
10
+ import { dirname, join } from "path";
11
+ import { fileURLToPath } from "url";
12
+ import { writeBundleSpec } from "@intentius/chant/codegen/package";
13
+ import { packageLexicon } from "./codegen/package.js";
14
+ const pkgDir = dirname(fileURLToPath(import.meta.url));
15
+ const distDir = join(pkgDir, "..", "dist");
16
+ const verbose = process.argv.includes("--verbose") || !process.argv.includes("--quiet");
17
+ const force = process.argv.includes("--force");
18
+ const { spec, stats } = await packageLexicon({ verbose, force });
19
+ writeBundleSpec(spec, distDir);
20
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
21
+ console.error(`dist/ written to ${distDir}`);