@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,286 @@
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
+
41
+ import type { Declarable } from "@intentius/chant/declarable";
42
+ import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
43
+ import type { Serializer, SerializeContext } from "@intentius/chant/serializer";
44
+ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
45
+ import { ownershipEntries, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
46
+ import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
47
+ import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
48
+ import { RENDER_ENV_OWNERSHIP_KEYS } from "./ownership";
49
+ import { CATALOG, SERVICE_TYPE_OF, catalogEntry, isServiceEntityType } from "./catalog";
50
+
51
+ /** A cross-resource id reference the applier resolves after the target exists. */
52
+ export interface RefMarker {
53
+ $ref: string;
54
+ }
55
+
56
+ /** An attribute read the applier resolves from the live target. */
57
+ export interface AttrMarker {
58
+ $attr: { entity: string; attribute: string };
59
+ }
60
+
61
+ /** The workspace placeholder left when neither the author nor `RENDER_OWNER_ID` named one. */
62
+ export interface OwnerMarker {
63
+ $owner: true;
64
+ }
65
+
66
+ export function isRefMarker(v: unknown): v is RefMarker {
67
+ return !!v && typeof v === "object" && typeof (v as RefMarker).$ref === "string";
68
+ }
69
+
70
+ export function isAttrMarker(v: unknown): v is AttrMarker {
71
+ const a = v as AttrMarker;
72
+ return !!a && typeof a === "object" && !!a.$attr && typeof a.$attr.entity === "string";
73
+ }
74
+
75
+ export function isOwnerMarker(v: unknown): v is OwnerMarker {
76
+ return !!v && typeof v === "object" && (v as OwnerMarker).$owner === true;
77
+ }
78
+
79
+ /** A single Render REST call the applier can issue, plus its reconcile identity. */
80
+ export interface RenderRequest {
81
+ kind: string;
82
+ entityType: string;
83
+ endpoint: string;
84
+ method: "POST";
85
+ /** The human name Render will show and chant reconciles by. */
86
+ name: string;
87
+ body: Record<string, unknown>;
88
+ /** Path placeholders in `endpoint` (child collections). */
89
+ pathParams?: Record<string, unknown>;
90
+ }
91
+
92
+ /** The serializer's whole output: entity name → request. */
93
+ export type RenderPlan = Record<string, RenderRequest>;
94
+
95
+ /**
96
+ * Visitor for the generic serializer walker. Property declarables
97
+ * (WebServiceDetails, Image, ServiceDisk, ...) are unwrapped to plain objects;
98
+ * resource references and attribute reads become the applier's markers.
99
+ */
100
+ function renderVisitor(): SerializerVisitor {
101
+ return {
102
+ attrRef: (entity, attribute) => ({ $attr: { entity, attribute } }),
103
+ resourceRef: (name) => ({ $ref: name }),
104
+ propertyDeclarable: (entity, walk) => {
105
+ const props = isResourceDeclarable(entity) ? entity.props : undefined;
106
+ if (!props || typeof props !== "object") return undefined;
107
+ const result: Record<string, unknown> = {};
108
+ for (const [key, value] of Object.entries(props)) {
109
+ if (value !== undefined) result[key] = walk(value);
110
+ }
111
+ return result;
112
+ },
113
+ };
114
+ }
115
+
116
+ function readProps(entity: Declarable): Record<string, unknown> {
117
+ const props = isResourceDeclarable(entity) ? entity.props : undefined;
118
+ return props && typeof props === "object" ? (props as Record<string, unknown>) : {};
119
+ }
120
+
121
+ /**
122
+ * Pseudo-parameter → environment variable mapping. Render bodies are plain
123
+ * JSON, so `Render.OwnerId` / `Render.Region` (which the walker lowers to a
124
+ * `{ Ref: "Render::..." }` marker) are resolved from the environment at build
125
+ * time. Region has a fallback (Render's own default); the owner does not — an
126
+ * unset owner becomes the `$owner` marker for the applier.
127
+ */
128
+ const PSEUDO_ENV_MAP: Record<string, { envVars: string[]; fallback?: unknown }> = {
129
+ "Render::OwnerId": { envVars: ["RENDER_OWNER_ID"], fallback: { $owner: true } },
130
+ "Render::Region": { envVars: ["RENDER_REGION"], fallback: "oregon" },
131
+ };
132
+
133
+ function resolvePseudoRef(ref: string): unknown {
134
+ const mapping = PSEUDO_ENV_MAP[ref];
135
+ if (!mapping) return undefined;
136
+ for (const envVar of mapping.envVars) {
137
+ const value = process.env[envVar];
138
+ if (value) return value;
139
+ }
140
+ return mapping.fallback;
141
+ }
142
+
143
+ /**
144
+ * Recursively replace pseudo-parameters with their resolved environment value.
145
+ * Handles both a raw `PseudoParameter` instance and the already-walked
146
+ * `{ Ref: "Render::X" }` shape. Everything else passes through untouched.
147
+ */
148
+ export function resolvePseudoParameters(value: unknown): unknown {
149
+ if (Array.isArray(value)) {
150
+ return value.map(resolvePseudoParameters);
151
+ }
152
+ if (value && typeof value === "object") {
153
+ const record = value as Record<string, unknown>;
154
+
155
+ if (INTRINSIC_MARKER in record && typeof record.toJSON === "function") {
156
+ return resolvePseudoParameters(record.toJSON());
157
+ }
158
+
159
+ const keys = Object.keys(record);
160
+ if (keys.length === 1 && keys[0] === "Ref" && typeof record.Ref === "string") {
161
+ const resolved = resolvePseudoRef(record.Ref);
162
+ if (resolved !== undefined) return resolved;
163
+ }
164
+ const result: Record<string, unknown> = {};
165
+ for (const [key, val] of Object.entries(record)) {
166
+ result[key] = resolvePseudoParameters(val);
167
+ }
168
+ return result;
169
+ }
170
+ return value;
171
+ }
172
+
173
+ /** The Render name a resource gets: an explicit `name` prop, else the entity name. */
174
+ function resourceName(props: Record<string, unknown>, entityName: string): string {
175
+ return typeof props.name === "string" && props.name ? props.name : entityName;
176
+ }
177
+
178
+ /**
179
+ * Merge chant's ownership marker into an `envVars` list. The marker wins over
180
+ * any colliding user key so ownership can never be silently unset; the user's
181
+ * ordering is otherwise preserved.
182
+ */
183
+ export function stampOwnership(envVars: unknown, marker: Record<string, string>): unknown[] {
184
+ const list = Array.isArray(envVars) ? [...envVars] : [];
185
+ const out = list.filter((e) => !(e && typeof e === "object" && (e as { key?: unknown }).key !== undefined && (e as { key: string }).key in marker));
186
+ for (const [key, value] of Object.entries(marker)) out.push({ key, value });
187
+ return out;
188
+ }
189
+
190
+ /**
191
+ * Serialize a render partition into the plan document `renderApply` consumes.
192
+ *
193
+ * Split out of the `Serializer` object so callers that need the concrete
194
+ * document — the applier, its tests, the `describeResources` fixtures — get
195
+ * `string` rather than the interface's `string | SerializerResult` union.
196
+ */
197
+ export function serializeRender(
198
+ entities: Map<string, Declarable>,
199
+ _outputs?: LexiconOutput[],
200
+ context?: SerializeContext,
201
+ ): string {
202
+ const entityNames = new Map<Declarable, string>();
203
+ for (const [name, entity] of entities) entityNames.set(entity, name);
204
+ const visitor = renderVisitor();
205
+
206
+ // Ownership marker. `CHANT_MANAGED_BY=chant` is always stamped; stack/env
207
+ // are added when the build threads an ownership marker through the context.
208
+ const ownershipEnv: Record<string, string> = context?.ownership
209
+ ? ownershipEntries(RENDER_ENV_OWNERSHIP_KEYS, context.ownership)
210
+ : { [RENDER_ENV_OWNERSHIP_KEYS.managedBy]: OWNERSHIP_MANAGED_BY_VALUE };
211
+
212
+ const requests: RenderPlan = {};
213
+
214
+ for (const [entityName, entity] of entities) {
215
+ if (isPropertyDeclarable(entity)) continue;
216
+ const entityType = (entity as unknown as { entityType?: string }).entityType;
217
+ if (!entityType || !(entityType in CATALOG)) continue;
218
+ const entry = catalogEntry(entityType);
219
+ const props = readProps(entity);
220
+
221
+ // Body: every prop except association hints, walked so nested property
222
+ // declarables flatten and references become markers.
223
+ const body: Record<string, unknown> = {};
224
+ const pathParams: Record<string, unknown> = {};
225
+ for (const [key, value] of Object.entries(props)) {
226
+ if (value === undefined) continue;
227
+ const walked = walkValue(value, entityNames, visitor);
228
+ if (entry.nonBodyProps.includes(key)) {
229
+ pathParams[key] = walked;
230
+ } else {
231
+ body[key] = walked;
232
+ }
233
+ }
234
+
235
+ // Services: re-inject the fixed `type` discriminator the authoring
236
+ // surface hides, and stamp the marker.
237
+ if (isServiceEntityType(entityType)) {
238
+ body.type = SERVICE_TYPE_OF[entityType];
239
+ }
240
+ if (entry.marked) {
241
+ body.envVars = stampOwnership(body.envVars, ownershipEnv);
242
+ }
243
+
244
+ // Owner: fill from the pseudo-parameter when omitted; the walked value
245
+ // of `Render.OwnerId` is a `{ Ref }` resolved below with the rest.
246
+ if (entry.ownerScoped && body.ownerId === undefined) {
247
+ body.ownerId = { Ref: "Render::OwnerId" };
248
+ }
249
+ // `image.ownerId` must match the service's; default it the same way.
250
+ if (isServiceEntityType(entityType) && body.image && typeof body.image === "object") {
251
+ const image = body.image as Record<string, unknown>;
252
+ if (image.ownerId === undefined) image.ownerId = body.ownerId;
253
+ }
254
+
255
+ // Child collections: fill the endpoint placeholder from pathParams when
256
+ // the value is a literal id; a `{ $ref }` stays for the applier.
257
+ let endpoint = entry.collection;
258
+ for (const [key, value] of Object.entries(pathParams)) {
259
+ if (typeof value === "string") endpoint = endpoint.replace(`{${key}}`, encodeURIComponent(value));
260
+ }
261
+
262
+ const request: RenderRequest = {
263
+ kind: entry.kind,
264
+ entityType,
265
+ endpoint,
266
+ method: "POST",
267
+ name: resourceName(props, entityName),
268
+ body,
269
+ };
270
+ if (Object.keys(pathParams).length > 0) request.pathParams = pathParams;
271
+ requests[entityName] = request;
272
+ }
273
+
274
+ // Resolve pseudo-parameter markers (Render.OwnerId, Render.Region) to their
275
+ // environment value so bodies carry plain strings, not `{ Ref }`.
276
+ return JSON.stringify(resolvePseudoParameters(requests), null, 2);
277
+ }
278
+
279
+ /**
280
+ * Render serializer.
281
+ */
282
+ export const renderSerializer: Serializer = {
283
+ name: "render",
284
+ rulePrefix: "REN",
285
+ serialize: serializeRender,
286
+ };
@@ -0,0 +1,126 @@
1
+ ---
2
+ skill: chant-render-patterns
3
+ description: Render patterns in chant — env groups, projects and environments, disks, custom domains, image-backed and cron services, and the ownership model
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Render Patterns
8
+
9
+ Companion to `chant-render`. Each pattern is a fragment you can drop into `src/`.
10
+
11
+ ## Env groups shared across services
12
+
13
+ An `EnvGroup` holds env vars (and secret files) many services share. Link services with `serviceIds`, passing the declared resources:
14
+
15
+ ```ts
16
+ import { EnvGroup, EnvVar, GeneratedEnvVar, WebService, BackgroundWorker, WebServiceDetails, BackgroundWorkerDetails } from "@intentius/chant-lexicon-render";
17
+
18
+ export const web = new WebService({ name: "web", repo: REPO, serviceDetails: new WebServiceDetails({ runtime: "docker" }) });
19
+ export const worker = new BackgroundWorker({ name: "worker", repo: REPO, serviceDetails: new BackgroundWorkerDetails({ runtime: "docker" }) });
20
+
21
+ export const shared = new EnvGroup({
22
+ name: "shared",
23
+ envVars: [
24
+ new EnvVar({ key: "LOG_LEVEL", value: "info" }),
25
+ new GeneratedEnvVar({ key: "SIGNING_KEY", generateValue: true }),
26
+ ],
27
+ serviceIds: [web, worker],
28
+ });
29
+ ```
30
+
31
+ The applier links each service after both exist (`POST /env-groups/{id}/services/{serviceId}`) and reconciles group vars per key on later runs. Env groups carry the ownership marker, so they are prunable.
32
+
33
+ ## Projects and environments
34
+
35
+ A `Project` groups `Environment`s; a service, datastore, or env group joins one through `environmentId`. Reference the declared environment and the applier fills the id:
36
+
37
+ ```ts
38
+ import { Project, ProjectEnvironment, Environment, Postgres } from "@intentius/chant-lexicon-render";
39
+
40
+ export const project = new Project({ name: "shop", environments: [new ProjectEnvironment({ name: "production" })] });
41
+ export const prod = new Environment({ name: "production", projectId: project, protectedStatus: "protected" });
42
+ export const db = new Postgres({ name: "shop-db", plan: "basic_1gb", version: "16", environmentId: prod });
43
+ ```
44
+
45
+ `Project.environments` creates environments with the project (Render requires at least one); a separate `Environment` resource is how you reference one from other resources and PATCH it later. Reconciling by name means the environment created inline and the one declared separately are the same live object.
46
+
47
+ ## Persistent disks
48
+
49
+ Two ways. Inline, as part of the service (created with it):
50
+
51
+ ```ts
52
+ serviceDetails: new WebServiceDetails({ runtime: "docker", plan: "starter", disk: new ServiceDisk({ name: "data", mountPath: "/data", sizeGB: 10 }) })
53
+ ```
54
+
55
+ Or as a standalone `Disk` attached to a declared service, which can be resized independently:
56
+
57
+ ```ts
58
+ export const data = new Disk({ name: "data", sizeGB: 10, mountPath: "/data", serviceId: web });
59
+ ```
60
+
61
+ Disks are only valid on web services, private services, and background workers (the `serviceId` type says so), and never on the free plan (REN012).
62
+
63
+ ## Custom domains
64
+
65
+ ```ts
66
+ export const apex = new CustomDomain({ name: "example.com", serviceId: web });
67
+ export const www = new CustomDomain({ name: "www.example.com", serviceId: web });
68
+ ```
69
+
70
+ The domain is created under the service (`POST /services/{id}/custom-domains`); DNS verification stays a manual step in Render's dashboard or via the `/verify` endpoint. `describeResources` reports the domain's `verificationStatus` as its status.
71
+
72
+ ## Image-backed services
73
+
74
+ ```ts
75
+ import { WebService, WebServiceDetails, Image, RegistryCredential } from "@intentius/chant-lexicon-render";
76
+
77
+ export const cred = new RegistryCredential({ name: "ghcr", registry: "GITHUB", username: "me", authToken: process.env.GHCR_TOKEN! });
78
+ export const api = new WebService({
79
+ name: "api",
80
+ image: new Image({ imagePath: "ghcr.io/acme/api:1.4.2", registryCredentialId: cred }),
81
+ serviceDetails: new WebServiceDetails({ runtime: "image", plan: "starter" }),
82
+ });
83
+ ```
84
+
85
+ `Image.ownerId` is filled from the service's owner. `registryCredentialId` accepts the declared credential. Deploying a new tag is a change to `imagePath` → a PATCH → Render redeploys.
86
+
87
+ ## Cron jobs
88
+
89
+ ```ts
90
+ export const nightly = new CronJob({
91
+ name: "nightly-report",
92
+ repo: REPO,
93
+ serviceDetails: new CronJobDetails({
94
+ runtime: "python",
95
+ schedule: "0 3 * * *",
96
+ envSpecificDetails: new NativeEnvironmentDetails({ buildCommand: "pip install -r requirements.txt", startCommand: "python report.py" }),
97
+ }),
98
+ });
99
+ ```
100
+
101
+ `schedule` is a five-field cron expression (REN003); Render accepts no `@daily`-style macros.
102
+
103
+ ## Static sites
104
+
105
+ ```ts
106
+ export const site = new StaticSite({
107
+ name: "docs",
108
+ repo: REPO,
109
+ serviceDetails: new StaticSiteDetails({
110
+ buildCommand: "npm ci && npm run build",
111
+ publishPath: "dist",
112
+ routes: [new Route({ type: "rewrite", source: "/*", destination: "/index.html" })],
113
+ headers: [new Header({ path: "/*", name: "X-Frame-Options", value: "DENY" })],
114
+ }),
115
+ });
116
+ ```
117
+
118
+ ## The ownership model
119
+
120
+ Render has no tags or labels. chant's marker is an env var:
121
+
122
+ - **Services and env groups** carry `CHANT_MANAGED_BY=chant`, `CHANT_STACK=<stack>`, `CHANT_ENV=<env>` in their env vars. That is the primary marker: `describeResources` answers `owned` / `foreign` from it, and `prune: true` deletes only marked resources of the current stack that the plan no longer declares. A service someone created in the dashboard is `foreign` and is never modified or deleted by prune — but it *is* adopted by name if you declare it (a PATCH brings it to the declared shape and stamps the marker).
123
+ - **Disks and custom domains** have no marker but hang off a service and inherit its verdict — the service boundary. An undeclared disk or domain under a chant-owned service is pruned with `prune: true`; one under a foreign service is foreign and untouched.
124
+ - **Everything else** (Postgres, KeyValue, Project, Environment, RegistryCredential, Webhook) has no marker channel and no boundary; its verdict is `unknown` and it is never pruned. Remove one by dropping it from the plan and running `renderDelete` on the old plan, or by hand.
125
+
126
+ The marker keys are visible in the service's environment. That is deliberate — it is the same information a Kubernetes label or an AWS tag carries, in the only durable key/value store Render exposes.
@@ -0,0 +1,112 @@
1
+ ---
2
+ skill: chant-render
3
+ description: Author, lint, and deploy Render services, datastores, and env groups from a chant project, applied straight to the Render Public API
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Deploy to Render Operational Playbook
8
+
9
+ ## How chant and Render relate
10
+
11
+ chant is a synthesis compiler: it compiles TypeScript in `src/` into a plan of Render Public API create requests, then reconciles that plan against a Render workspace. There is no external CLI to hand off to and no Blueprint (`render.yaml`) to keep in sync. `renderApply` speaks the Public API directly (`https://api.render.com/v1`), so the same code that builds the plan also applies it, and there is no state file to store, lock, or back up.
12
+
13
+ The source of truth is the TypeScript in `src/`. The serialized plan (a JSON object keyed by entity name, each value a `{ kind, entityType, endpoint, method, name, body }` request) is an intermediate artifact.
14
+
15
+ Your job as an agent:
16
+
17
+ - Use `chant build` for synthesis and lint (region, secret literals, cron schedule, runtime commands, source, free-plan scaling).
18
+ - Use `renderApply` (via the deploy Op, `chant run`) to reconcile the plan against the workspace: create or update each resource by name, wait each created service's deploy to `live`, and optionally prune what chant owns.
19
+
20
+ ## Credentials and the workspace
21
+
22
+ Three environment variables:
23
+
24
+ - `RENDER_API_KEY` — a Render API key (Account Settings → API Keys). Required for every apply and read. Sent as `Authorization: Bearer`.
25
+ - `RENDER_OWNER_ID` — the workspace (team or personal) id, `tea-…` or `usr-…`. Every `ownerId` chant fills in defaults to it (`Render.OwnerId`). If unset, the applier reads `GET /owners` and uses the sole workspace the key can see; several visible workspaces is an error, never a guess.
26
+ - `RENDER_API_BASE_URL` — optional endpoint override (a local stand-in). Default `https://api.render.com/v1`.
27
+
28
+ Resolution order for the endpoint is: an explicit `endpoint` arg, then `RENDER_API_BASE_URL`, then the default.
29
+
30
+ Start from the runnable [`examples/getting-started`](../../lexicons/render/examples/getting-started) stack:
31
+
32
+ ```bash
33
+ cd lexicons/render/examples/getting-started
34
+ export RENDER_API_KEY=rnd_… RENDER_OWNER_ID=tea-…
35
+ npm run build # chant build src --lexicon render -o dist/render.json
36
+ chant run render # build → renderApply (creates the web service, waits for its deploy)
37
+ ```
38
+
39
+ ## Author a service and a database
40
+
41
+ Import resource types from `@intentius/chant-lexicon-render`. They are generated from Render's Public API OpenAPI spec, so `WebServiceDetails`, `Image`, `ServiceDisk`, and friends are typed all the way down, and every enum (`plan`, `region`, `runtime`) is a string-literal union.
42
+
43
+ ```ts
44
+ import {
45
+ WebService, WebServiceDetails, NativeEnvironmentDetails, EnvVar, GeneratedEnvVar, Postgres, Render,
46
+ } from "@intentius/chant-lexicon-render";
47
+
48
+ export const db = new Postgres({ name: "app-db", plan: "free", version: "16", region: Render.Region });
49
+
50
+ export const web = new WebService({
51
+ name: "app-web",
52
+ repo: "https://github.com/render-examples/express-hello-world",
53
+ branch: "main",
54
+ serviceDetails: new WebServiceDetails({
55
+ runtime: "node",
56
+ plan: "starter",
57
+ region: Render.Region,
58
+ envSpecificDetails: new NativeEnvironmentDetails({ buildCommand: "npm ci", startCommand: "npm start" }),
59
+ }),
60
+ envVars: [
61
+ new EnvVar({ key: "DATABASE_URL", value: db.internalConnectionString }),
62
+ new GeneratedEnvVar({ key: "SESSION_SECRET", generateValue: true }),
63
+ ],
64
+ });
65
+ ```
66
+
67
+ Each service type is its own class — `WebService`, `StaticSite`, `PrivateService`, `BackgroundWorker`, `CronJob` — with `serviceDetails` narrowed to that type's shape (`CronJobDetails` requires `schedule`; `StaticSiteDetails` has `publishPath` and `routes`). The other resources are `Postgres`, `KeyValue`, `EnvGroup`, `Project`, `Environment`, `Disk`, `CustomDomain`, `RegistryCredential`, and `Webhook`.
68
+
69
+ You do not stamp the ownership marker yourself: the serializer writes `CHANT_MANAGED_BY=chant` (plus `CHANT_STACK` / `CHANT_ENV`) into each service's and env group's `envVars`, and the owned-only prune reads it back.
70
+
71
+ ### References between resources
72
+
73
+ Render assigns ids on create, so a Disk's `serviceId`, an Environment's `projectId`, or a service's `environmentId` cannot be a literal at build time. Pass the declared resource instead — `serviceId: web`, `projectId: project`, `environmentId: env` — and the applier substitutes the live id after the target exists. Attribute reads work the same way: `db.internalConnectionString`, `db.externalConnectionString`, `kv.internalConnectionString`, `web.id`, `web.dashboardUrl` resolve from the live resource (connection strings from the `/connection-info` endpoint).
74
+
75
+ ## Build and lint
76
+
77
+ ```bash
78
+ chant build src/
79
+ ```
80
+
81
+ Build synthesizes the plan and runs the lint rules before anything reaches the API:
82
+
83
+ | Rule | Catches |
84
+ |------|---------|
85
+ | REN001 | `region` is not a Render region (frankfurt, oregon, ohio, singapore, virginia) |
86
+ | REN002 | A secret-looking env var (`*_PASSWORD`, `*_TOKEN`, `API_KEY`, …) with a literal value |
87
+ | REN003 | A `schedule` that is not a five-field cron expression |
88
+ | REN010 | A native-runtime service (node, python, …) with no `buildCommand`/`startCommand` |
89
+ | REN011 | A service with neither `repo` nor `image`, or `runtime: "image"` without an image |
90
+ | REN012 | A free-plan service with `numInstances > 1`, `autoscaling`, or a `disk` |
91
+
92
+ Fix every reported violation before applying. Secret values belong in `generateValue: true`, an `EnvGroup`, a resource attribute, or `process.env` — never a literal (REN002).
93
+
94
+ ## Apply with renderApply
95
+
96
+ `renderApply` reads the plan and, in dependency order (projects → environments → env groups/datastores → services → disks/domains → webhooks):
97
+
98
+ 1. Resolves `{ $ref }`, `{ $attr }`, and `{ $owner }` markers from what is already live.
99
+ 2. Finds the existing resource by name (services also by type; environments by project; disks by service).
100
+ 3. `POST`s a create when absent, or `PATCH`es the differing patchable fields when present. Service env vars are replaced through `PUT /services/{id}/env-vars`; a `generateValue` var keeps its live value rather than being regenerated. Env-group vars are reconciled per key.
101
+ 4. Waits each **created** service's first deploy to `live` (`wait.deploys: false` to skip; `wait.deadlineMs` default 15 minutes) and throws on `build_failed`/`update_failed`/`canceled`.
102
+ 5. With `prune: true`, deletes services and env groups that carry the marker for this stack but are no longer in the plan, and the disks and custom domains under an owned declared service that the plan no longer declares. Foreign resources (no marker) and other stacks' resources are never touched. Datastores, projects, environments, registry credentials, and webhooks are never pruned — they have no marker; remove them explicitly with `renderDelete`.
103
+
104
+ Returns the versioned apply envelope: `applied` (`created` / `updated` / `unchanged`, with the Render id as `physicalId`) and `pruned`.
105
+
106
+ ## Read back with plan
107
+
108
+ `chant lifecycle plan` (via `describeResources`) lists what is live for every declared entity, with an ownership verdict: services and env groups are `owned` or `foreign` by the marker; disks and custom domains inherit their service's verdict; everything else is `unknown`, which the change set never escalates to a delete. Undeclared chant-owned services, env groups, disks, and domains surface as owned orphans, so a removed declaration shows up as a delete candidate before you prune.
109
+
110
+ ## Teardown
111
+
112
+ `renderDelete` (or `renderDeploy({ teardown: true })`) deletes what the plan names in reverse order, looking each up by name; already-gone resources are reported `deleted: false`. Only what the plan declares is touched.
@@ -0,0 +1,33 @@
1
+ import { join } from "path";
2
+ import { homedir } from "os";
3
+ import { fetchWithCache } from "@intentius/chant/codegen/fetch";
4
+
5
+ /**
6
+ * Render's published Public API OpenAPI 3.0 spec.
7
+ *
8
+ * Render serves the current spec at a fixed URL with no version or release tag
9
+ * to pin (the document's own `info.version` has sat at "1.0.0" across many
10
+ * additive changes), so — like fly — the plugin declares no `upstreamPin`.
11
+ * Re-running `generate` picks up whatever the URL currently serves.
12
+ */
13
+ export const SCHEMA_URL = "https://api-docs.render.com/v1.0/openapi/render-public-api-1.json";
14
+
15
+ /**
16
+ * Cache under `~/.chant` so CI's `~/.chant` schema cache covers it — an
17
+ * api-docs.render.com hiccup then falls back to the cache instead of failing
18
+ * the generate step.
19
+ */
20
+ const CACHE_FILE = join(homedir(), ".chant", "render-public-api-openapi.json");
21
+
22
+ /**
23
+ * Fetch the Render OpenAPI spec and return it as a Map<typeName, Buffer>
24
+ * compatible with the generatePipeline fetchSchemas callback.
25
+ *
26
+ * The spec is a single document, so we return a single entry keyed by
27
+ * "Render::OpenAPI" — the parse step splits it into the curated resources and
28
+ * their reachable property types.
29
+ */
30
+ export async function fetchSchemas(options?: { force?: boolean }): Promise<Map<string, Buffer>> {
31
+ const raw = await fetchWithCache({ url: SCHEMA_URL, cacheFile: CACHE_FILE }, options?.force);
32
+ return new Map([["Render::OpenAPI", raw]]);
33
+ }