@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,32 @@
1
+ /**
2
+ * The render lexicon's ownership convention, declared here so core carries no
3
+ * render-specific branch — the #686 seam: each lexicon owns its key names; core
4
+ * keeps only the generic stamp/detect logic (@intentius/chant/ownership).
5
+ *
6
+ * Render has no tags or labels on any resource. What it does have, on the two
7
+ * kinds chant creates most and prunes — services and env groups — is a
8
+ * key/value store the API writes and reads back verbatim: environment
9
+ * variables. chant stamps `CHANT_MANAGED_BY=chant` (+ `CHANT_STACK` /
10
+ * `CHANT_ENV`) into a service's or env group's env vars at serialize time and
11
+ * reads them back through `GET .../env-vars`; that is the primary marker and
12
+ * the basis for owned-only prune. A service without it is never modified or
13
+ * deleted by a prune. The keys are upper-snake because that is what an env var
14
+ * is; the values are exactly what every other lexicon stamps.
15
+ *
16
+ * Disks and custom domains carry no marker either, but they hang off a
17
+ * service, so they inherit the parent's verdict — the service boundary, one
18
+ * level down from fly's app boundary. An undeclared disk under a chant-owned
19
+ * service is chant's and is pruned; one under a foreign service is foreign.
20
+ *
21
+ * Datastores, projects, environments, registry credentials, and webhooks
22
+ * carry no channel and no boundary. Their verdict is `unknown` (never
23
+ * `foreign`, never `owned`): the change set never escalates `unknown` to a
24
+ * delete, so an undeclared Postgres is never pruned. Removing one is an
25
+ * explicit `renderDelete` of a plan that names it.
26
+ */
27
+ import type { ChannelKeys } from "@intentius/chant/ownership";
28
+ /** Env-var keys for chant's ownership markers on services and env groups. */
29
+ export declare const RENDER_ENV_OWNERSHIP_KEYS: ChannelKeys;
30
+ /** True when a key is one of chant's marker env vars. */
31
+ export declare function isOwnershipKey(key: string): boolean;
32
+ //# sourceMappingURL=ownership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../src/ownership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAE9D,6EAA6E;AAC7E,eAAO,MAAM,yBAAyB,EAAE,WAIvC,CAAC;AAEF,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMnD"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env tsx
2
+ export {};
3
+ //# sourceMappingURL=package-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-cli.d.ts","sourceRoot":"","sources":["../src/package-cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import type { LexiconPlugin } from "@intentius/chant/lexicon";
2
+ /**
3
+ * render lexicon plugin.
4
+ *
5
+ * Implements all required LexiconPlugin lifecycle methods.
6
+ */
7
+ export declare const renderPlugin: LexiconPlugin;
8
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,0BAA0B,CAAC;AAY/E;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,aAgP1B,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { PseudoParameter } from "@intentius/chant/pseudo-parameter";
2
+ export { PseudoParameter };
3
+ /**
4
+ * Render pseudo-parameters — environment-resolved values a project references
5
+ * without hard-coding. Each serializes to a `{ Ref: "Render::..." }` marker
6
+ * that the serializer resolves from an environment variable at build time (see
7
+ * PSEUDO_ENV_MAP in ./serializer.ts), mirroring fly's `Fly.OrgSlug`.
8
+ *
9
+ * - OwnerId → the workspace (team or personal) id (RENDER_OWNER_ID), e.g. `tea-…`.
10
+ * Every ownerId chant fills in defaults to this, so a stack rarely names it.
11
+ * - Region → the target region (RENDER_REGION), default `oregon`.
12
+ */
13
+ export declare const OwnerId: PseudoParameter, Region: PseudoParameter;
14
+ /**
15
+ * Render namespace containing all pseudo-parameters.
16
+ */
17
+ export declare const Render: {
18
+ readonly OwnerId: PseudoParameter;
19
+ readonly Region: PseudoParameter;
20
+ };
21
+ //# sourceMappingURL=pseudo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pseudo.d.ts","sourceRoot":"","sources":["../src/pseudo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAA0B,MAAM,mCAAmC,CAAC;AAE5F,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B;;;;;;;;;GASG;AACH,eAAO,MAAQ,OAAO,mBAAE,MAAM,iBAG5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,MAAM;;;CAGT,CAAC"}
@@ -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,47 @@
1
+ /**
2
+ * REN010: A native-runtime service must say how to build and start.
3
+ *
4
+ * Render's native runtimes (node, python, ruby, go, rust, elixir) need a
5
+ * `buildCommand` and a `startCommand` in `envSpecificDetails`; without them the
6
+ * API rejects the create (or, on some paths, accepts it and the first deploy
7
+ * fails). `docker` reads a Dockerfile and `image` pulls a prebuilt image, so
8
+ * neither needs commands. Static sites have no runtime.
9
+ */
10
+
11
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
12
+ import { ENTITY_TYPES } from "../../catalog";
13
+ import { readProps, entityTypeOf, isService, kindOf } from "./render-helpers";
14
+
15
+ const NATIVE_RUNTIMES = new Set(["node", "python", "ruby", "go", "rust", "elixir"]);
16
+
17
+ export const ren010: PostSynthCheck = {
18
+ id: "REN010",
19
+ description: "A native-runtime service must set envSpecificDetails.buildCommand and startCommand",
20
+
21
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
22
+ const diagnostics: PostSynthDiagnostic[] = [];
23
+
24
+ for (const [name, entity] of ctx.entities) {
25
+ if (!isService(entity) || entityTypeOf(entity) === ENTITY_TYPES.staticSite) continue;
26
+ const details = readProps(readProps(entity).serviceDetails);
27
+ const runtime = details.runtime;
28
+ if (typeof runtime !== "string" || !NATIVE_RUNTIMES.has(runtime)) continue;
29
+
30
+ const env = readProps(details.envSpecificDetails);
31
+ const missing: string[] = [];
32
+ if (typeof env.buildCommand !== "string" || env.buildCommand.length === 0) missing.push("buildCommand");
33
+ if (typeof env.startCommand !== "string" || env.startCommand.length === 0) missing.push("startCommand");
34
+ if (missing.length > 0) {
35
+ diagnostics.push({
36
+ checkId: "REN010",
37
+ severity: "error",
38
+ message: `${kindOf(entity)} "${name}" uses the ${runtime} runtime but sets no ${missing.join(" or ")} — add envSpecificDetails: new NativeEnvironmentDetails({ buildCommand, startCommand })`,
39
+ entity: name,
40
+ lexicon: "render",
41
+ });
42
+ }
43
+ }
44
+
45
+ return diagnostics;
46
+ },
47
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * REN011: A service needs a source — a repo or an image.
3
+ *
4
+ * Every Render service is either git-backed (`repo`, built on Render) or
5
+ * image-backed (`image.imagePath`, pulled from a registry). One that names
6
+ * neither has nothing to deploy and is rejected at create time; one that uses
7
+ * the `image` runtime without an `image` is the same mistake spelled
8
+ * differently. Static sites are always git-backed.
9
+ */
10
+
11
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
12
+ import { readProps, isService, kindOf } from "./render-helpers";
13
+
14
+ export const ren011: PostSynthCheck = {
15
+ id: "REN011",
16
+ description: "A service must have a repo (git-backed) or an image (image-backed)",
17
+
18
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
19
+ const diagnostics: PostSynthDiagnostic[] = [];
20
+
21
+ for (const [name, entity] of ctx.entities) {
22
+ if (!isService(entity)) continue;
23
+ const props = readProps(entity);
24
+ const repo = typeof props.repo === "string" && props.repo.length > 0;
25
+ const imagePath = readProps(props.image).imagePath;
26
+ const image = typeof imagePath === "string" && imagePath.length > 0;
27
+ const runtime = readProps(props.serviceDetails).runtime;
28
+
29
+ if (!repo && !image) {
30
+ diagnostics.push({
31
+ checkId: "REN011",
32
+ severity: "error",
33
+ message: `${kindOf(entity)} "${name}" has neither a repo nor an image — set repo: "https://github.com/…" or image: new Image({ imagePath: "docker.io/…" })`,
34
+ entity: name,
35
+ lexicon: "render",
36
+ });
37
+ } else if (runtime === "image" && !image) {
38
+ diagnostics.push({
39
+ checkId: "REN011",
40
+ severity: "error",
41
+ message: `${kindOf(entity)} "${name}" uses runtime "image" but sets no image.imagePath`,
42
+ entity: name,
43
+ lexicon: "render",
44
+ });
45
+ }
46
+ }
47
+
48
+ return diagnostics;
49
+ },
50
+ };
@@ -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,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,99 @@
1
+ /**
2
+ * Render serializer.
3
+ *
4
+ * Turns declared `WebService`, `Postgres`, `EnvGroup`, `Project`, ... resources
5
+ * into the JSON create bodies the Render Public API accepts, so the applier can
6
+ * POST them straight through.
7
+ *
8
+ * Output shape — a JSON object keyed by entity name. Each value is a single
9
+ * Render request plus the identity the applier reconciles by:
10
+ *
11
+ * {
12
+ * "<entityName>": {
13
+ * "kind": "WebService", // generated class name
14
+ * "entityType": "Render::Services::WebService",
15
+ * "endpoint": "/services", // create collection
16
+ * "method": "POST",
17
+ * "name": "my-web", // reconcile key (Render has no client-chosen ids)
18
+ * "body": { ... }, // the create body
19
+ * "pathParams": { "serviceId": { "$ref": "web" } } // for child collections
20
+ * }
21
+ * }
22
+ *
23
+ * Cross-resource references. Render identifies resources by server-assigned
24
+ * ids, so a Disk's `serviceId` or an Environment's `projectId` cannot be known
25
+ * at build time. A declared resource passed where an id is expected serializes
26
+ * to `{ "$ref": "<entityName>" }`; an attribute read (`db.internalConnectionString`)
27
+ * to `{ "$attr": { "entity": "<entityName>", "attribute": "..." } }`. The applier
28
+ * resolves both once the target exists.
29
+ *
30
+ * Ownership. Every serialized service and env group carries the
31
+ * `CHANT_MANAGED_BY=chant` marker (plus the stack/env identity from
32
+ * `context.ownership`) merged into `envVars`, even when the author supplied
33
+ * none. See ./ownership.ts for why env vars.
34
+ *
35
+ * `ownerId`. Every owner-scoped body gets `ownerId` filled from `Render.OwnerId`
36
+ * (`RENDER_OWNER_ID`) when the author omitted it. When that is unset too, an
37
+ * `{ "$owner": true }` marker is left for the applier, which resolves it from
38
+ * `GET /owners` when the token sees exactly one workspace.
39
+ */
40
+ import type { Declarable } from "@intentius/chant/declarable";
41
+ import type { Serializer, SerializeContext } from "@intentius/chant/serializer";
42
+ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
43
+ /** A cross-resource id reference the applier resolves after the target exists. */
44
+ export interface RefMarker {
45
+ $ref: string;
46
+ }
47
+ /** An attribute read the applier resolves from the live target. */
48
+ export interface AttrMarker {
49
+ $attr: {
50
+ entity: string;
51
+ attribute: string;
52
+ };
53
+ }
54
+ /** The workspace placeholder left when neither the author nor `RENDER_OWNER_ID` named one. */
55
+ export interface OwnerMarker {
56
+ $owner: true;
57
+ }
58
+ export declare function isRefMarker(v: unknown): v is RefMarker;
59
+ export declare function isAttrMarker(v: unknown): v is AttrMarker;
60
+ export declare function isOwnerMarker(v: unknown): v is OwnerMarker;
61
+ /** A single Render REST call the applier can issue, plus its reconcile identity. */
62
+ export interface RenderRequest {
63
+ kind: string;
64
+ entityType: string;
65
+ endpoint: string;
66
+ method: "POST";
67
+ /** The human name Render will show and chant reconciles by. */
68
+ name: string;
69
+ body: Record<string, unknown>;
70
+ /** Path placeholders in `endpoint` (child collections). */
71
+ pathParams?: Record<string, unknown>;
72
+ }
73
+ /** The serializer's whole output: entity name → request. */
74
+ export type RenderPlan = Record<string, RenderRequest>;
75
+ /**
76
+ * Recursively replace pseudo-parameters with their resolved environment value.
77
+ * Handles both a raw `PseudoParameter` instance and the already-walked
78
+ * `{ Ref: "Render::X" }` shape. Everything else passes through untouched.
79
+ */
80
+ export declare function resolvePseudoParameters(value: unknown): unknown;
81
+ /**
82
+ * Merge chant's ownership marker into an `envVars` list. The marker wins over
83
+ * any colliding user key so ownership can never be silently unset; the user's
84
+ * ordering is otherwise preserved.
85
+ */
86
+ export declare function stampOwnership(envVars: unknown, marker: Record<string, string>): unknown[];
87
+ /**
88
+ * Serialize a render partition into the plan document `renderApply` consumes.
89
+ *
90
+ * Split out of the `Serializer` object so callers that need the concrete
91
+ * document — the applier, its tests, the `describeResources` fixtures — get
92
+ * `string` rather than the interface's `string | SerializerResult` union.
93
+ */
94
+ export declare function serializeRender(entities: Map<string, Declarable>, _outputs?: LexiconOutput[], context?: SerializeContext): string;
95
+ /**
96
+ * Render serializer.
97
+ */
98
+ export declare const renderSerializer: Serializer;
99
+ //# sourceMappingURL=serializer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAOrE,kFAAkF;AAClF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AAED,8FAA8F;AAC9F,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,IAAI,CAAC;CACd;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,SAAS,CAEtD;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,UAAU,CAGxD;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAE1D;AAED,oFAAoF;AACpF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,4DAA4D;AAC5D,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAkDvD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAuB/D;AAOD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,EAAE,CAK1F;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,QAAQ,CAAC,EAAE,aAAa,EAAE,EAC1B,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,CA4ER;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,UAI9B,CAAC"}