@intentius/chant-lexicon-render 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 (168) hide show
  1. package/README.md +89 -0
  2. package/dist/catalog.d.ts +100 -0
  3. package/dist/catalog.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 +21 -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 +18 -0
  11. package/dist/codegen/generate-lexicon.d.ts.map +1 -0
  12. package/dist/codegen/generate-typescript.d.ts +12 -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 +14 -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/render-deploy.d.ts +88 -0
  21. package/dist/composites/render-deploy.d.ts.map +1 -0
  22. package/dist/coverage.d.ts +15 -0
  23. package/dist/coverage.d.ts.map +1 -0
  24. package/dist/describe-resources.d.ts +60 -0
  25. package/dist/describe-resources.d.ts.map +1 -0
  26. package/dist/detect.d.ts +12 -0
  27. package/dist/detect.d.ts.map +1 -0
  28. package/dist/generated/index.d.ts +38 -0
  29. package/dist/generated/index.d.ts.map +1 -0
  30. package/dist/generated/runtime.d.ts +5 -0
  31. package/dist/generated/runtime.d.ts.map +1 -0
  32. package/dist/index.d.ts +11 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/integrity.json +18 -0
  35. package/dist/lint/audit-catalog.d.ts +13 -0
  36. package/dist/lint/audit-catalog.d.ts.map +1 -0
  37. package/dist/lint/post-synth/index.d.ts +3 -0
  38. package/dist/lint/post-synth/index.d.ts.map +1 -0
  39. package/dist/lint/post-synth/ren010-native-runtime-requires-commands.d.ts +12 -0
  40. package/dist/lint/post-synth/ren010-native-runtime-requires-commands.d.ts.map +1 -0
  41. package/dist/lint/post-synth/ren011-service-requires-source.d.ts +12 -0
  42. package/dist/lint/post-synth/ren011-service-requires-source.d.ts.map +1 -0
  43. package/dist/lint/post-synth/ren012-free-plan-cannot-scale.d.ts +11 -0
  44. package/dist/lint/post-synth/ren012-free-plan-cannot-scale.d.ts.map +1 -0
  45. package/dist/lint/post-synth/render-helpers.d.ts +19 -0
  46. package/dist/lint/post-synth/render-helpers.d.ts.map +1 -0
  47. package/dist/lint/rules/index.d.ts +7 -0
  48. package/dist/lint/rules/index.d.ts.map +1 -0
  49. package/dist/lint/rules/no-secret-literals.d.ts +13 -0
  50. package/dist/lint/rules/no-secret-literals.d.ts.map +1 -0
  51. package/dist/lint/rules/valid-cron-schedule.d.ts +12 -0
  52. package/dist/lint/rules/valid-cron-schedule.d.ts.map +1 -0
  53. package/dist/lint/rules/valid-region.d.ts +16 -0
  54. package/dist/lint/rules/valid-region.d.ts.map +1 -0
  55. package/dist/lsp/completions.d.ts +8 -0
  56. package/dist/lsp/completions.d.ts.map +1 -0
  57. package/dist/lsp/hover.d.ts +6 -0
  58. package/dist/lsp/hover.d.ts.map +1 -0
  59. package/dist/manifest.json +6 -0
  60. package/dist/meta.json +304 -0
  61. package/dist/okf/index.md +29 -0
  62. package/dist/okf/rules/REN001.md +11 -0
  63. package/dist/okf/rules/REN002.md +15 -0
  64. package/dist/okf/rules/REN003.md +15 -0
  65. package/dist/okf/rules/REN010.md +11 -0
  66. package/dist/okf/rules/REN011.md +11 -0
  67. package/dist/okf/rules/REN012.md +11 -0
  68. package/dist/okf/types/BackgroundWorker.md +24 -0
  69. package/dist/okf/types/CronJob.md +28 -0
  70. package/dist/okf/types/CustomDomain.md +14 -0
  71. package/dist/okf/types/Disk.md +16 -0
  72. package/dist/okf/types/EnvGroup.md +22 -0
  73. package/dist/okf/types/Environment.md +17 -0
  74. package/dist/okf/types/KeyValue.md +20 -0
  75. package/dist/okf/types/Postgres.md +29 -0
  76. package/dist/okf/types/PrivateService.md +24 -0
  77. package/dist/okf/types/Project.md +15 -0
  78. package/dist/okf/types/RegistryCredential.md +17 -0
  79. package/dist/okf/types/StaticSite.md +24 -0
  80. package/dist/okf/types/WebService.md +24 -0
  81. package/dist/okf/types/Webhook.md +17 -0
  82. package/dist/op/activities/fake-render.d.ts +52 -0
  83. package/dist/op/activities/fake-render.d.ts.map +1 -0
  84. package/dist/op/activities/index.d.ts +10 -0
  85. package/dist/op/activities/index.d.ts.map +1 -0
  86. package/dist/op/activities/render-apply.d.ts +229 -0
  87. package/dist/op/activities/render-apply.d.ts.map +1 -0
  88. package/dist/ownership.d.ts +32 -0
  89. package/dist/ownership.d.ts.map +1 -0
  90. package/dist/package-cli.d.ts +3 -0
  91. package/dist/package-cli.d.ts.map +1 -0
  92. package/dist/plugin.d.ts +8 -0
  93. package/dist/plugin.d.ts.map +1 -0
  94. package/dist/pseudo.d.ts +21 -0
  95. package/dist/pseudo.d.ts.map +1 -0
  96. package/dist/rules/no-secret-literals.ts +73 -0
  97. package/dist/rules/ren010-native-runtime-requires-commands.ts +47 -0
  98. package/dist/rules/ren011-service-requires-source.ts +50 -0
  99. package/dist/rules/ren012-free-plan-cannot-scale.ts +43 -0
  100. package/dist/rules/render-helpers.ts +38 -0
  101. package/dist/rules/valid-cron-schedule.ts +51 -0
  102. package/dist/rules/valid-region.ts +49 -0
  103. package/dist/serializer.d.ts +99 -0
  104. package/dist/serializer.d.ts.map +1 -0
  105. package/dist/skills/chant-render-patterns.md +126 -0
  106. package/dist/skills/chant-render.md +112 -0
  107. package/dist/spec/fetch.d.ts +21 -0
  108. package/dist/spec/fetch.d.ts.map +1 -0
  109. package/dist/spec/parse.d.ts +120 -0
  110. package/dist/spec/parse.d.ts.map +1 -0
  111. package/dist/types/index.d.ts +573 -0
  112. package/dist/validate-cli.d.ts +3 -0
  113. package/dist/validate-cli.d.ts.map +1 -0
  114. package/dist/validate.d.ts +15 -0
  115. package/dist/validate.d.ts.map +1 -0
  116. package/package.json +73 -0
  117. package/src/catalog.ts +286 -0
  118. package/src/codegen/docs-cli.ts +7 -0
  119. package/src/codegen/docs.ts +120 -0
  120. package/src/codegen/generate-cli.ts +36 -0
  121. package/src/codegen/generate-lexicon.ts +52 -0
  122. package/src/codegen/generate-typescript.ts +70 -0
  123. package/src/codegen/generate.ts +106 -0
  124. package/src/codegen/naming.ts +55 -0
  125. package/src/codegen/package.ts +30 -0
  126. package/src/composites/render-deploy.ts +133 -0
  127. package/src/coverage.ts +49 -0
  128. package/src/describe-resources.test.ts +143 -0
  129. package/src/describe-resources.ts +366 -0
  130. package/src/detect.ts +29 -0
  131. package/src/generated/index.d.ts +573 -0
  132. package/src/generated/index.ts +43 -0
  133. package/src/generated/lexicon-render.json +304 -0
  134. package/src/generated/runtime.ts +4 -0
  135. package/src/index.ts +30 -0
  136. package/src/lint/audit-catalog.ts +47 -0
  137. package/src/lint/post-synth/index.ts +11 -0
  138. package/src/lint/post-synth/post-synth.test.ts +151 -0
  139. package/src/lint/post-synth/ren010-native-runtime-requires-commands.ts +47 -0
  140. package/src/lint/post-synth/ren011-service-requires-source.ts +50 -0
  141. package/src/lint/post-synth/ren012-free-plan-cannot-scale.ts +43 -0
  142. package/src/lint/post-synth/render-helpers.ts +38 -0
  143. package/src/lint/rules/index.ts +11 -0
  144. package/src/lint/rules/no-secret-literals.ts +73 -0
  145. package/src/lint/rules/rules.test.ts +73 -0
  146. package/src/lint/rules/valid-cron-schedule.ts +51 -0
  147. package/src/lint/rules/valid-region.ts +49 -0
  148. package/src/lsp/completions.test.ts +28 -0
  149. package/src/lsp/completions.ts +22 -0
  150. package/src/lsp/hover.test.ts +32 -0
  151. package/src/lsp/hover.ts +50 -0
  152. package/src/op/activities/fake-render.ts +275 -0
  153. package/src/op/activities/index.ts +51 -0
  154. package/src/op/activities/render-apply.test.ts +403 -0
  155. package/src/op/activities/render-apply.ts +1042 -0
  156. package/src/ownership.ts +44 -0
  157. package/src/package-cli.ts +23 -0
  158. package/src/plugin.test.ts +54 -0
  159. package/src/plugin.ts +258 -0
  160. package/src/pseudo.ts +26 -0
  161. package/src/serializer.test.ts +221 -0
  162. package/src/serializer.ts +286 -0
  163. package/src/skills/chant-render-patterns.md +126 -0
  164. package/src/skills/chant-render.md +112 -0
  165. package/src/spec/fetch.ts +33 -0
  166. package/src/spec/parse.ts +721 -0
  167. package/src/validate-cli.ts +5 -0
  168. package/src/validate.ts +47 -0
@@ -0,0 +1,43 @@
1
+ /**
2
+ * REN012: The free instance type cannot scale.
3
+ *
4
+ * Render's `free` plan runs a single instance and cannot autoscale; a
5
+ * `numInstances > 1` or an enabled `autoscaling` block on a free service is
6
+ * rejected at create/patch time. Persistent disks are also unavailable on
7
+ * free. Catch all three at synth time with a message that says which.
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { readProps, isService, kindOf } from "./render-helpers";
12
+
13
+ export const ren012: PostSynthCheck = {
14
+ id: "REN012",
15
+ description: "A free-plan service cannot set numInstances > 1, autoscaling, or a disk",
16
+
17
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [name, entity] of ctx.entities) {
21
+ if (!isService(entity)) continue;
22
+ const details = readProps(readProps(entity).serviceDetails);
23
+ if (details.plan !== "free") continue;
24
+
25
+ const problems: string[] = [];
26
+ if (typeof details.numInstances === "number" && details.numInstances > 1) problems.push(`numInstances: ${details.numInstances}`);
27
+ const autoscaling = readProps(details.autoscaling);
28
+ if (autoscaling.enabled === true) problems.push("autoscaling");
29
+ if (details.disk !== undefined) problems.push("a disk");
30
+ if (problems.length > 0) {
31
+ diagnostics.push({
32
+ checkId: "REN012",
33
+ severity: "error",
34
+ message: `${kindOf(entity)} "${name}" is on the free plan but sets ${problems.join(", ")} — the free instance type runs one instance, cannot autoscale, and has no persistent disk; pick a paid plan (starter or above)`,
35
+ entity: name,
36
+ lexicon: "render",
37
+ });
38
+ }
39
+ }
40
+
41
+ return diagnostics;
42
+ },
43
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared helpers for the render post-synth checks. Not a check itself, so the
3
+ * generated barrel skips it (the scanner only picks up exported PostSynthChecks).
4
+ */
5
+
6
+ import { isServiceEntityType } from "../../catalog";
7
+
8
+ /**
9
+ * Read an entity or property's constructor props. Nested Declarables
10
+ * (WebServiceDetails, Image, ServiceDisk) stash their args under a
11
+ * non-enumerable `props`; a plain inline object carries them directly. Handle
12
+ * both so a check behaves the same whether the user authored
13
+ * `serviceDetails: { runtime }` or `serviceDetails: new WebServiceDetails({ runtime })`.
14
+ */
15
+ export function readProps(value: unknown): Record<string, unknown> {
16
+ if (value && typeof value === "object") {
17
+ const nested = (value as { props?: unknown }).props;
18
+ if (nested && typeof nested === "object") return nested as Record<string, unknown>;
19
+ return value as Record<string, unknown>;
20
+ }
21
+ return {};
22
+ }
23
+
24
+ /** The entityType of a declarable, or undefined. */
25
+ export function entityTypeOf(value: unknown): string | undefined {
26
+ return (value as { entityType?: string } | undefined)?.entityType;
27
+ }
28
+
29
+ /** True when the entity is one of the five service kinds. */
30
+ export function isService(value: unknown): boolean {
31
+ return isServiceEntityType(entityTypeOf(value));
32
+ }
33
+
34
+ /** The short kind (`WebService`) from an entity type, for messages. */
35
+ export function kindOf(value: unknown): string {
36
+ const t = entityTypeOf(value) ?? "";
37
+ return t.slice(t.lastIndexOf("::") + 2);
38
+ }
@@ -0,0 +1,11 @@
1
+ import type { LintRule } from "@intentius/chant/lint/rule";
2
+ import { validRegionRule } from "./valid-region";
3
+ import { noSecretLiteralsRule } from "./no-secret-literals";
4
+ import { validCronScheduleRule } from "./valid-cron-schedule";
5
+
6
+ export { validRegionRule } from "./valid-region";
7
+ export { noSecretLiteralsRule } from "./no-secret-literals";
8
+ export { validCronScheduleRule, isValidCronSchedule } from "./valid-cron-schedule";
9
+
10
+ /** All lint rules provided by the render lexicon. */
11
+ export const rules: LintRule[] = [validRegionRule, noSecretLiteralsRule, validCronScheduleRule];
@@ -0,0 +1,73 @@
1
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
2
+ import * as ts from "typescript";
3
+
4
+ /**
5
+ * Env-var keys that carry a credential. Kept conservative on purpose — an
6
+ * inline value under one of these keys is very likely a real secret.
7
+ */
8
+ const SECRET_KEY_PATTERN = /(?:^|[_-])(?:password|passwd|secret|token|api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key|client[_-]?secret|credential|auth[_-]?token)(?:$|[_-])/i;
9
+
10
+ /**
11
+ * Values that are references or placeholders, not literal secrets: shell/
12
+ * interpolation references ($FOO, ${FOO}) and secret-manager style references.
13
+ */
14
+ const REFERENCE_VALUE_PATTERN = /^(?:\$\{?[A-Za-z0-9_]+\}?|(?:secret|ref|env|vault):\S+)$/;
15
+
16
+ /**
17
+ * REN002: No secret literals in env vars
18
+ *
19
+ * Flags an `envVars` (or `secretFiles`) entry whose `key` looks like a
20
+ * credential and whose `value` is a plain string literal. Render can generate
21
+ * the value (`generateValue: true`), it can come from an EnvGroup or from
22
+ * another resource's attribute (`db.internalConnectionString`), or from the
23
+ * process environment at build time — anything but a literal in source that
24
+ * ends up in git and in the build output.
25
+ */
26
+ export const noSecretLiteralsRule: LintRule = {
27
+ id: "REN002",
28
+ severity: "warning",
29
+ category: "security",
30
+ description: "Secret env-var values must not be written inline — use generateValue, an EnvGroup, or a reference",
31
+
32
+ check(context: LintContext): LintDiagnostic[] {
33
+ const { sourceFile } = context;
34
+ const diagnostics: LintDiagnostic[] = [];
35
+
36
+ function literalOf(init: ts.Expression): string | undefined {
37
+ return ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init) ? init.text : undefined;
38
+ }
39
+
40
+ function visit(node: ts.Node): void {
41
+ // `{ key: "DB_PASSWORD", value: "hunter2" }` — an object literal with a
42
+ // secret-looking `key` and a literal `value`.
43
+ if (ts.isObjectLiteralExpression(node)) {
44
+ let key: string | undefined;
45
+ let valueNode: ts.Expression | undefined;
46
+ for (const prop of node.properties) {
47
+ if (!ts.isPropertyAssignment(prop)) continue;
48
+ const name = prop.name.getText(sourceFile).replace(/^["']|["']$/g, "");
49
+ if (name === "key") key = literalOf(prop.initializer);
50
+ if (name === "value") valueNode = prop.initializer;
51
+ }
52
+ if (key && valueNode) {
53
+ const value = literalOf(valueNode);
54
+ if (value !== undefined && value.length >= 4 && SECRET_KEY_PATTERN.test(key) && !REFERENCE_VALUE_PATTERN.test(value.trim())) {
55
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(valueNode.getStart(sourceFile));
56
+ diagnostics.push({
57
+ file: sourceFile.fileName,
58
+ line: line + 1,
59
+ column: character + 1,
60
+ ruleId: "REN002",
61
+ severity: "warning",
62
+ message: `Possible inline secret for env var "${key}". Use \`generateValue: true\`, an EnvGroup, a resource attribute (e.g. db.internalConnectionString), or process.env — not a literal.`,
63
+ });
64
+ }
65
+ }
66
+ }
67
+ ts.forEachChild(node, visit);
68
+ }
69
+
70
+ visit(sourceFile);
71
+ return diagnostics;
72
+ },
73
+ };
@@ -0,0 +1,73 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import * as ts from "typescript";
3
+ import { validRegionRule } from "./valid-region";
4
+ import { noSecretLiteralsRule } from "./no-secret-literals";
5
+ import { validCronScheduleRule, isValidCronSchedule } from "./valid-cron-schedule";
6
+ import type { LintContext } from "@intentius/chant/lint/rule";
7
+
8
+ function createContext(code: string, fileName = "infra.ts"): LintContext {
9
+ const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
10
+ return { sourceFile, entities: [], filePath: fileName };
11
+ }
12
+
13
+ describe("Render lint rules", () => {
14
+ describe("REN001: valid region", () => {
15
+ test("flags an unknown region", () => {
16
+ const diags = validRegionRule.check(createContext(`new WebServiceDetails({ runtime: "node", region: "narnia" });`));
17
+ expect(diags).toHaveLength(1);
18
+ expect(diags[0].ruleId).toBe("REN001");
19
+ expect(diags[0].severity).toBe("error");
20
+ expect(diags[0].message).toContain("narnia");
21
+ });
22
+
23
+ test("passes every known region", () => {
24
+ for (const r of ["frankfurt", "oregon", "ohio", "singapore", "virginia"]) {
25
+ expect(validRegionRule.check(createContext(`new Postgres({ name: "db", region: "${r}" });`))).toHaveLength(0);
26
+ }
27
+ });
28
+
29
+ test("ignores a non-literal region", () => {
30
+ expect(validRegionRule.check(createContext(`new KeyValue({ region: Render.Region });`))).toHaveLength(0);
31
+ });
32
+ });
33
+
34
+ describe("REN002: no secret literals", () => {
35
+ test("flags a literal value under a secret-looking key", () => {
36
+ const diags = noSecretLiteralsRule.check(createContext(`envVars: [{ key: "DATABASE_PASSWORD", value: "hunter22" }]`));
37
+ expect(diags).toHaveLength(1);
38
+ expect(diags[0].ruleId).toBe("REN002");
39
+ expect(diags[0].severity).toBe("warning");
40
+ expect(diags[0].message).toContain("DATABASE_PASSWORD");
41
+ });
42
+
43
+ test("passes generateValue, references, placeholders, and non-secret keys", () => {
44
+ expect(noSecretLiteralsRule.check(createContext(`[{ key: "API_KEY", generateValue: true }]`))).toHaveLength(0);
45
+ expect(noSecretLiteralsRule.check(createContext(`[{ key: "API_KEY", value: db.internalConnectionString }]`))).toHaveLength(0);
46
+ expect(noSecretLiteralsRule.check(createContext(`[{ key: "API_KEY", value: process.env.API_KEY! }]`))).toHaveLength(0);
47
+ expect(noSecretLiteralsRule.check(createContext(`[{ key: "API_KEY", value: "$API_KEY" }]`))).toHaveLength(0);
48
+ expect(noSecretLiteralsRule.check(createContext(`[{ key: "LOG_LEVEL", value: "debug" }]`))).toHaveLength(0);
49
+ });
50
+ });
51
+
52
+ describe("REN003: valid cron schedule", () => {
53
+ test("isValidCronSchedule accepts standard five-field expressions", () => {
54
+ for (const ok of ["0 * * * *", "*/15 * * * *", "0 0 1 * *", "30 2 * * 1-5", "0,30 * * * *", "0 9-17/2 * * 1,3,5"]) {
55
+ expect(isValidCronSchedule(ok), ok).toBe(true);
56
+ }
57
+ for (const bad of ["@hourly", "* * * * * *", "0 * * *", "every hour", ""]) {
58
+ expect(isValidCronSchedule(bad), bad).toBe(false);
59
+ }
60
+ });
61
+
62
+ test("flags an invalid schedule literal", () => {
63
+ const diags = validCronScheduleRule.check(createContext(`new CronJobDetails({ runtime: "docker", schedule: "@hourly" });`));
64
+ expect(diags).toHaveLength(1);
65
+ expect(diags[0].ruleId).toBe("REN003");
66
+ expect(diags[0].message).toContain("@hourly");
67
+ });
68
+
69
+ test("passes a valid schedule", () => {
70
+ expect(validCronScheduleRule.check(createContext(`new CronJobDetails({ runtime: "docker", schedule: "0 * * * *" });`))).toHaveLength(0);
71
+ });
72
+ });
73
+ });
@@ -0,0 +1,51 @@
1
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
2
+ import * as ts from "typescript";
3
+
4
+ /** One cron field: `*`, a number, a range, a list, or any of those with a `/step`. */
5
+ const FIELD = String.raw`(?:\*|\d+(?:-\d+)?)(?:,(?:\*|\d+(?:-\d+)?))*(?:\/\d+)?`;
6
+ const CRON_5 = new RegExp(`^${FIELD}(?:\\s+${FIELD}){4}$`);
7
+
8
+ /** True for a standard five-field cron expression (Render accepts no seconds field or macros). */
9
+ export function isValidCronSchedule(s: string): boolean {
10
+ return CRON_5.test(s.trim());
11
+ }
12
+
13
+ /**
14
+ * REN003: Valid cron schedule
15
+ *
16
+ * A CronJob's `schedule` must be a standard five-field cron expression
17
+ * (`minute hour day-of-month month day-of-week`). Render rejects `@hourly`-style
18
+ * macros and six-field (seconds) expressions at create time; catch them at build.
19
+ */
20
+ export const validCronScheduleRule: LintRule = {
21
+ id: "REN003",
22
+ severity: "error",
23
+ category: "correctness",
24
+ description: "A cron job schedule must be a five-field cron expression",
25
+
26
+ check(context: LintContext): LintDiagnostic[] {
27
+ const { sourceFile } = context;
28
+ const diagnostics: LintDiagnostic[] = [];
29
+
30
+ function visit(node: ts.Node): void {
31
+ if (ts.isPropertyAssignment(node) && node.name.getText(sourceFile) === "schedule") {
32
+ const init = node.initializer;
33
+ if (ts.isStringLiteral(init) && !isValidCronSchedule(init.text)) {
34
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(init.getStart(sourceFile));
35
+ diagnostics.push({
36
+ file: sourceFile.fileName,
37
+ line: line + 1,
38
+ column: character + 1,
39
+ ruleId: "REN003",
40
+ severity: "error",
41
+ message: `Invalid cron schedule "${init.text}". Use five fields, e.g. "0 * * * *" (hourly) or "*/15 * * * *" (every 15 minutes); Render accepts no @macros or seconds field.`,
42
+ });
43
+ }
44
+ }
45
+ ts.forEachChild(node, visit);
46
+ }
47
+
48
+ visit(sourceFile);
49
+ return diagnostics;
50
+ },
51
+ };
@@ -0,0 +1,49 @@
1
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
2
+ import * as ts from "typescript";
3
+
4
+ /**
5
+ * Render's regions, per the `region` enum in the Public API spec. The generated
6
+ * types already narrow `region` to this union, so a typo is a compile error in
7
+ * a typed project; this rule gives the same answer with a clearer message, and
8
+ * fires in contexts the type checker does not reach (a `.js` stack, a value
9
+ * that has been widened to `string`).
10
+ */
11
+ export const KNOWN_REGIONS = new Set<string>(["frankfurt", "oregon", "ohio", "singapore", "virginia"]);
12
+
13
+ /**
14
+ * REN001: Valid Render region
15
+ *
16
+ * A service, datastore, or Key Value `region` must be a Render region.
17
+ */
18
+ export const validRegionRule: LintRule = {
19
+ id: "REN001",
20
+ severity: "error",
21
+ category: "correctness",
22
+ description: "A region must be a Render region (frankfurt, oregon, ohio, singapore, virginia)",
23
+
24
+ check(context: LintContext): LintDiagnostic[] {
25
+ const { sourceFile } = context;
26
+ const diagnostics: LintDiagnostic[] = [];
27
+
28
+ function visit(node: ts.Node): void {
29
+ if (ts.isPropertyAssignment(node) && node.name.getText(sourceFile) === "region") {
30
+ const init = node.initializer;
31
+ if (ts.isStringLiteral(init) && init.text.length > 0 && !KNOWN_REGIONS.has(init.text)) {
32
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(init.getStart(sourceFile));
33
+ diagnostics.push({
34
+ file: sourceFile.fileName,
35
+ line: line + 1,
36
+ column: character + 1,
37
+ ruleId: "REN001",
38
+ severity: "error",
39
+ message: `Unknown Render region "${init.text}". Use one of: ${[...KNOWN_REGIONS].join(", ")}.`,
40
+ });
41
+ }
42
+ }
43
+ ts.forEachChild(node, visit);
44
+ }
45
+
46
+ visit(sourceFile);
47
+ return diagnostics;
48
+ },
49
+ };
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { completions } from "./completions";
3
+
4
+ describe("render LSP completions", () => {
5
+ it("returns nothing for a non-constructor context", () => {
6
+ const items = completions({
7
+ uri: "file:///a.ts",
8
+ content: "const x = 42",
9
+ position: { line: 0, character: 12 },
10
+ wordAtCursor: "42",
11
+ linePrefix: "const x = 42",
12
+ });
13
+ expect(items).toHaveLength(0);
14
+ });
15
+
16
+ it("completes resource classes after `new`", () => {
17
+ const items = completions({
18
+ uri: "file:///infra.ts",
19
+ content: "const web = new Web",
20
+ position: { line: 0, character: 19 },
21
+ wordAtCursor: "Web",
22
+ linePrefix: "const web = new Web",
23
+ });
24
+ const labels = items.map((c) => c.label);
25
+ expect(labels).toContain("WebService");
26
+ expect(labels).toContain("Webhook");
27
+ });
28
+ });
@@ -0,0 +1,22 @@
1
+ import { createRequire } from "module";
2
+ import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
3
+ import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
+ const require = createRequire(import.meta.url);
5
+
6
+ let cachedIndex: LexiconIndex | null = null;
7
+
8
+ function getIndex(): LexiconIndex {
9
+ if (cachedIndex) return cachedIndex;
10
+ const data = require("../generated/lexicon-render.json") as Record<string, LexiconEntry>;
11
+ cachedIndex = new LexiconIndex(data);
12
+ return cachedIndex;
13
+ }
14
+
15
+ /**
16
+ * Provide render completions based on context — the resource classes
17
+ * (WebService, Postgres, EnvGroup, ...) and their property entities, from the
18
+ * generated registry.
19
+ */
20
+ export function completions(ctx: CompletionContext): CompletionItem[] {
21
+ return lexiconCompletions(ctx, getIndex(), "Render resource");
22
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { hover } from "./hover";
3
+
4
+ describe("render LSP hover", () => {
5
+ it("returns undefined for an unknown word", () => {
6
+ expect(
7
+ hover({ uri: "file:///a.ts", content: "const x = 42", position: { line: 0, character: 8 }, word: "x", lineText: "const x = 42" }),
8
+ ).toBeUndefined();
9
+ });
10
+
11
+ it("describes a resource with its collection and marker, and a property as nested", () => {
12
+ const web = hover({
13
+ uri: "file:///infra.ts",
14
+ content: "new WebService({})",
15
+ position: { line: 0, character: 6 },
16
+ word: "WebService",
17
+ lineText: "new WebService({})",
18
+ });
19
+ expect(web?.contents).toContain("Render::Services::WebService");
20
+ expect(web?.contents).toContain("POST /services");
21
+ expect(web?.contents).toContain("CHANT_MANAGED_BY");
22
+
23
+ const details = hover({
24
+ uri: "file:///infra.ts",
25
+ content: "new WebServiceDetails({})",
26
+ position: { line: 0, character: 6 },
27
+ word: "WebServiceDetails",
28
+ lineText: "new WebServiceDetails({})",
29
+ });
30
+ expect(details?.contents).toContain("Property");
31
+ });
32
+ });
@@ -0,0 +1,50 @@
1
+ import { createRequire } from "module";
2
+ import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
3
+ import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
+ import { CATALOG } from "../catalog";
5
+ const require = createRequire(import.meta.url);
6
+
7
+ let cachedIndex: LexiconIndex | null = null;
8
+
9
+ function getIndex(): LexiconIndex {
10
+ if (cachedIndex) return cachedIndex;
11
+ const data = require("../generated/lexicon-render.json") as Record<string, LexiconEntry>;
12
+ cachedIndex = new LexiconIndex(data);
13
+ return cachedIndex;
14
+ }
15
+
16
+ /**
17
+ * Provide hover information for render entity types.
18
+ */
19
+ export function hover(ctx: HoverContext): HoverInfo | undefined {
20
+ return lexiconHover(ctx, getIndex(), resourceHover);
21
+ }
22
+
23
+ function resourceHover(className: string, entry: LexiconEntry): HoverInfo | undefined {
24
+ const lines: string[] = [];
25
+
26
+ lines.push(`**${className}**`);
27
+ lines.push("");
28
+ lines.push(`Render type: \`${entry.resourceType}\``);
29
+ lines.push("");
30
+ const cat = CATALOG[entry.resourceType];
31
+ if (entry.kind === "resource" && cat) {
32
+ lines.push(`*Resource — created with \`POST ${cat.collection}\` on the Render Public API and reconciled by name*`);
33
+ if (cat.marked) {
34
+ lines.push("");
35
+ lines.push("Carries chant's `CHANT_MANAGED_BY` env-var ownership marker; eligible for owned-only prune.");
36
+ } else if (cat.boundary === "service") {
37
+ lines.push("");
38
+ lines.push("Inherits its parent service's ownership verdict (service boundary); pruned when the service is chant-owned and this is undeclared.");
39
+ } else {
40
+ lines.push("");
41
+ lines.push("No ownership marker channel: verdict is `unknown`; never auto-pruned.");
42
+ }
43
+ } else if (entry.kind === "resource") {
44
+ lines.push("*Resource — a Render Public API object*");
45
+ } else {
46
+ lines.push("*Property — a nested value in a Render resource declaration*");
47
+ }
48
+
49
+ return { contents: lines.join("\n") };
50
+ }