Implement Generate
Show the whole prompt
Read https://intentius.io/chant/lexicon-authoring/generate/.
Wire the generate step of the lexicon I name with `generatePipeline` and the callbacks the page lists. Fetch only through `fetchWithCache`, and write into `src/generated/` with `writeGeneratedArtifacts`.
Declare `upstreamPin` in `src/plugin.ts` for the pinned spec version, run `npm run generate` and `npm run bundle` in the lexicon and `chant dev pinned-upgrade lexicons/<name>`, and open a pull request with the result.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Optional: hand this page to your coding agent
Read https://intentius.io/chant/lexicon-authoring/generate/.Wire the generate step of the lexicon I name with `generatePipeline` and the callbacks the page lists. Fetch only through `fetchWithCache`, and write into `src/generated/` with `writeGeneratedArtifacts`.Declare `upstreamPin` in `src/plugin.ts` for the pinned spec version, run `npm run generate` and `npm run bundle` in the lexicon and `chant dev pinned-upgrade lexicons/<name>`, and open a pull request with the result.Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.The generate lifecycle method fetches the upstream spec, parses it, and produces TypeScript types, a runtime index, and a registry. Core’s generatePipeline runs these steps; you supply provider-specific callbacks.
Generation Pipeline
Section titled “Generation Pipeline”import { generatePipeline, type GeneratePipelineConfig } from "@intentius/chant/codegen/generate";
const result = await generatePipeline({ fetchSchemas: async (opts) => fetchMySchemas(opts.force), parseSchema: (name, data) => parseMySchema(data), createNaming: (results) => new NamingStrategy(results, myConfig), generateRegistry: (results, naming) => buildLexiconJSON(results, naming), generateTypes: (results, naming) => buildTypesDTS(results, naming), generateRuntimeIndex: (results, naming) => buildRuntimeIndex(results, naming), augmentSchemas: async (schemas, opts, log) => { /* patches, overlays */ }, augmentResults: (results, opts, log) => { /* fallbacks, synthetic resources */ }, generateExtraArtifacts: (results, naming) => ({ "extra.json": buildExtra(results) }),});See
lexicons/aws/src/codegen/generate.tsfor the complete AWS generation pipeline.
Pipeline data flow
Section titled “Pipeline data flow”The pipeline executes these steps in order:
fetchSchemas(opts)->Map<typeName, Buffer>— raw schema files keyed by type nameaugmentSchemas(schemas, opts, log)(optional) — patch or overlay schemas before parsing. Skipped whenopts.schemaSourceis provided (e.g. during testing). Returns{ schemas, extraResults?, warnings? }parseSchema(typeName, data)->T | T[] | null— parse each schema buffer. Returnnullto skip a file, or an array when one file yields several results (the k8s OpenAPI spec and the GitLab CI schema both do)augmentResults(results, opts, log)(optional) — add synthetic resources or fallbacks after parsing. Returns{ results, warnings? }createNaming(results)->NamingStrategy— build the naming strategy from all parsed resultsgenerateRegistry(results, naming)->string— JSON content for the lexicon registrygenerateTypes(results, naming)->string— TypeScript declaration content (.d.ts)generateRuntimeIndex(results, naming)->string— runtime index with factory exports (index.ts)generateExtraArtifacts(results, naming)(optional) ->Record<filename, string>— further derived tables from the same pass, returned onGenerateResult.extraArtifactsso they cannot skew against the registry and the types
ParsedResult contract
Section titled “ParsedResult contract”Your parser’s return type must extend ParsedResult:
interface ParsedResult { propertyTypes: Array<{ name: string }>; // property type definitions enums: Array<unknown>; // enum definitions}The pipeline uses .propertyTypes.length and .enums.length for stats only. The arrays are passed through untouched to your generate callbacks. Extend ParsedResult with any additional fields your callbacks need:
interface MyParsedResult extends ParsedResult { typeName: string; description: string; properties: Map<string, PropertyDef>; attributes: string[];}See
lexicons/aws/src/spec/parse.tsfor the AWS parser that produces aParsedResult.
Naming Strategy
Section titled “Naming Strategy”The NamingStrategy class implements a 5-phase collision-free naming algorithm for TypeScript class names. Supply your provider’s data tables via NamingConfig:
import { NamingStrategy, type NamingConfig, type NamingInput } from "@intentius/chant/codegen/naming";
const config: NamingConfig = { priorityNames: { "Provider::S3::Bucket": "Bucket" }, priorityAliases: {}, priorityPropertyAliases: {}, serviceAbbreviations: {}, shortName: (t) => t.split("::").pop()!, serviceName: (t) => t.split("::")[1], // Once the lexicon has published a surface, feed it the names it already // shipped so an unrelated upstream change cannot rename them (chant #1459): // reservedNames: reservedNamesFromSnapshot(snapshot),};
const naming = new NamingStrategy(inputs, config);See
lexicons/aws/src/codegen/naming.tsfor the AWS naming configuration with real data tables.
Fetch Utilities
Section titled “Fetch Utilities”fetchWithCache and extractFromZip handle HTTP download + caching + zip extraction:
import { fetchWithCache, extractFromZip } from "@intentius/chant/codegen/fetch";
const zipData = await fetchWithCache({ url: SCHEMA_URL, cacheFile: CACHE_PATH });const schemas = await extractFromZip(zipData, (name) => name.endsWith(".json"));See
lexicons/aws/src/spec/fetch.tsfor the AWS schema fetcher.
Reach for fetchWithCache from src/spec/fetch.ts and nowhere else. Generation is the one phase allowed to fetch: a serializer, a plugin hook or a lint rule that reached upstream would put a network call on chant build, which Network Egress guarantees is offline and a guard test fails on. fetchWithCache also caches to disk and falls back to the committed snapshot when the endpoint is unreachable, so generation still works offline.
When there is no upstream JSON schema
Section titled “When there is no upstream JSON schema”generatePipeline only requires fetchSchemas to hand back a
Map<string, Buffer>. It never inspects the bytes, so fetching can just as
well be a local file read or a transform over something that was never a
schema. Three shipped lexicons take three different routes:
| Lexicon | What its fetchSchemas returns | Source |
|---|---|---|
| k3s | Five pinned Go source files from pkg/cli/cmds/, packed into one JSON envelope keyed K3s::Config. k3s publishes no schema; the config keys are the CLI flag names, and the flags are urfave/cli struct literals in that source (chant #1599) | lexicons/k3s/src/spec/fetch.ts |
| k3d | The v1alpha5/schema.json from a tagged source tree rather than a moving branch, so the pin is the git tag | lexicons/k3d/src/spec/fetch.ts |
| cedar | A file read with a three-step resolution order: the project’s cedar.schema config key, then schema.cedarschema in the project root, then a real schema bundled in the package. No network at all | lexicons/cedar/src/spec/fetch.ts |
Cedar’s third step is the one to copy if your spec is local. Without a bundled
fallback, generate() has nothing to read in a fresh clone. Nothing gates it,
and CI never exercises the pipeline. fountain does the same thing on
the other axis: it fetches its pinned OpenAPI release and falls back to a
committed fountain-openapi.snapshot.json when the network is absent
(lexicons/fountain/src/spec/fetch.ts).
A hand-authored spec with no upstream at all belongs in src/spec/, read by
fetchSchemas. Skip upstreamPin in that case. The rest of the pipeline is
unchanged, and parseSchema still owns the shape.
Runtime Factories
Section titled “Runtime Factories”Use createResource and createProperty to generate Declarable-marked constructors:
import { createResource, createProperty } from "@intentius/chant/runtime";
const MyResource = createResource("Provider::Service::Type", "my-lexicon", { arn: "Arn" });const MyProperty = createProperty("Provider::Service::Type.PropType", "my-lexicon");Writing Generated Artifacts
Section titled “Writing Generated Artifacts”After generatePipeline returns a GenerateResult, write the files using writeGeneratedArtifacts:
import { writeGeneratedArtifacts } from "@intentius/chant/codegen/generate";
writeGeneratedArtifacts({ baseDir: pkgDir, // root of your lexicon package generatedSubdir: "src/generated", // default; can be customized files: { "lexicon.json": result.lexiconJSON, "index.d.ts": result.typesDTS, "index.ts": result.indexTS, },});See
lexicons/aws/src/codegen/generate.tsfor the complete generate + write flow.
Helper Utilities
Section titled “Helper Utilities”Runtime Index Generator
Section titled “Runtime Index Generator”import { generateRuntimeIndex, type RuntimeIndexConfig } from "@intentius/chant/codegen/generate-runtime-index";
const indexTS = generateRuntimeIndex(resources, properties, { lexiconName: "my-lexicon", intrinsicReExports: [], pseudoReExports: [],});Registry Builder
Section titled “Registry Builder”import { buildRegistry, serializeRegistry } from "@intentius/chant/codegen/generate-registry";
const registry = buildRegistry(results, naming, { shortName: (t) => naming.shortName(t), buildEntry: (r, shortName) => ({ resourceType: r.typeName, kind: "resource" }), buildPropertyEntry: (r, shortName) => ({ resourceType: r.typeName, kind: "property" }),});const lexiconJSON = serializeRegistry(registry);JSON Schema Utilities
Section titled “JSON Schema Utilities”For lexicons with JSON Schema-based specs:
import { resolvePropertyType, extractConstraints, isEnumDefinition } from "@intentius/chant/codegen/json-schema";resolvePropertyType(prop, schema, resolveDefName)— resolve a schema property to a TypeScript type string. Handles$ref,oneOf/anyOf, arrays, objects, and primitives. Inlineenumarrays produce sorted string-literal union types (e.g."Allow" | "Deny"). When a$refpoints to an enum definition, it callsresolveDefNameto produce a named enum type (e.g.Bucket_Status); passnullto fall back to"string"extractConstraints(prop)— extract validation constraints (min/max, pattern, allowed values, enum arrays)isEnumDefinition(def)— detect if a schema definition is a pure string enum (hasenumarray, noproperties)
PseudoParameter
Section titled “PseudoParameter”import { PseudoParameter, createPseudoParameters } from "@intentius/chant/pseudo-parameter";
const pseudos = createPseudoParameters({ "My::Region": "The deployment region", "My::AccountId": "The account identifier",});Import Utilities
Section titled “Import Utilities”For template import (converting existing templates to chant TypeScript):
import { BaseValueParser } from "@intentius/chant/import/base-parser";import { hasIntrinsicInValue, irUsesIntrinsic, collectDependencies } from "@intentius/chant/import/ir-utils";BaseValueParser— abstract base class for parsing template values into IR nodeshasIntrinsicInValue(value, name)— check whether a raw value contains a call to the named intrinsicirUsesIntrinsic(ir, name)— check whether an IR template uses a specific intrinsiccollectDependencies(value, isDependency)— collect resource dependencies from a value. The predicate is yours: given an object, return its logical id ornull
Intrinsic Interpolation
Section titled “Intrinsic Interpolation”For lexicons with string interpolation intrinsics (like CloudFormation’s Fn::Sub):
import { buildInterpolatedString, defaultInterpolationSerializer } from "@intentius/chant/intrinsic-interpolation";
// parts/values come straight from a tagged template literal's arguments.const result = buildInterpolatedString(parts, values, defaultInterpolationSerializer);buildInterpolatedString walks the template parts and the interpolated values
in lockstep, rendering each value through the serializer you pass.
Pinning the upstream schema — upstreamPin
Section titled “Pinning the upstream schema — upstreamPin”A lexicon generated from an upstream schema should pin the version it generated
from, so a regeneration is reproducible and a version bump is a reviewable diff
rather than whatever upstream published that morning. aws is the cautionary
case: its spec URL is unpinned, so any prepack folds in whatever CloudFormation
shipped today, and its resource count moves without a commit saying so.
upstreamPin tells the self-upgrade tooling where the pin lives and where to
look for a newer one:
upstreamPin: { file: "src/spec/fetch.ts", pattern: /export const K8S_SCHEMA_VERSION\s*=\s*"([^"]+)"/, replace: (v, line) => line.replace(/= "[^"]+"/, `= "${v}"`), upstream: { owner: "kubernetes", repo: "kubernetes", kind: "releases" },},| Field | What it is |
|---|---|
file | Source file holding the pinned version constant, relative to the package root |
pattern | Regex whose first capture group is the current version |
label | Optional on a lone upstreamPin, required on every upstreamPins entry. The short name (clickhouse, postgres-17) you pass to pinned-upgrade to check one pin |
replace(v, line) | Rebuild a matched line with the new version |
upstream.kind | releases for published releases, tags for every git tag |
upstream.tagSuffix | Only consider tags with this suffix — gitlab uses -ee, sql uses -lts |
upstream.tagVersion | Optional (tag) => string | null. Maps a raw tag to the version the pin stores, or returns null to skip the tag. For upstreams whose tags are not versions |
upstream.trackMajor | Optional, default false. Only consider versions whose first number equals the pinned version’s, for a lexicon that pins one snapshot per major |
alsoMoves | Optional. Set when other constants move with the version and replace cannot compute them. The tooling reports the new version and this text, and edits nothing |
k8s, k3s, gcp, gitlab, docker, cedar and sql declare one. checkPinnedUpgrade
(packages/core/src/codegen/pinned-upgrade.ts) uses it to report an available
upgrade and apply it. It then regenerates and reverts cleanly if the
regeneration fails, which is why replace exists in place of a blind string
substitution.
chant dev pinned-upgrade <dir> loads the plugin at <dir> and uses whatever
upstreamPin it declares; there is no list of supported lexicons in core. A
lexicon with no upstreamPin gets a message saying so. The sql pin sets
alsoMoves, because its server image digest sits beside the version in
src/spec/pin.ts and a new tag has a new digest: pinned-upgrade reports the
newer -lts release and how to move both constants, and does not edit the file
or regenerate.
LexiconUpgradeOp finds pinned lexicons the same way. Any lexicon that is not
rolling (aws, azure, github and fly are) is checked through its plugin’s
upstreamPin, so a lexicon that adds a pin needs no change in core. In
pull-request mode the Op opens a version-bump PR for a pin replace can
rewrite. A pin with alsoMoves stays report-only whatever the mode: the report
names the newer version and the manual step, and the Op makes no branch and
opens no PR.
Several pins: upstreamPins
Section titled “Several pins: upstreamPins”A lexicon that pins more than one upstream keeps upstreamPin as it is and
lists the rest in upstreamPins, an array of the same descriptor with a
required, unique label. Every lexicon with one pin is unchanged. The sql
lexicon has six. upstreamPin is ClickHouse’s (label clickhouse), and
upstreamPins holds postgres-14 to postgres-18, one per Postgres major,
built by postgresUpstreamPin(major) in src/spec/postgres-pin.ts. Each has
alsoMoves (the image digest sits beside the version) and trackMajor, so it
reports only its own major.
upstreamPin: { label: "clickhouse", /* ... */ },upstreamPins: POSTGRES_MAJORS.map(postgresUpstreamPin),chant dev pinned-upgrade <dir> checks upstreamPin first and then each
upstreamPins entry in order, one report per pin (a JSON array with -f json),
labelled sql/postgres-17. A failing pin is reported and the others still run.
Pass a label to check one pin and query only its upstream; a label the plugin
does not declare gets the list of labels it does.
LexiconUpgradeOp checks every pin too, and takes an optional pin label.
Its summary has one section per pin, and a pin with alsoMoves is report-only.
When a pin that replace can rewrite has an upgrade, the Op works on the first
such pin: the branch is lexicon-upgrade/<lexicon>-<label>, the bump goes to
that pin’s line, and the summary names the other pins to re-run with pin.
Tags that are not versions
Section titled “Tags that are not versions”postgres/postgres publishes no GitHub releases, only tags, such as REL_18_6,
REL_17_11, REL_18_BETA1 and REL_18_RC1, plus older spellings like
REL9_6_24 and Release_2_0. With kind: "tags" alone none of them parses as
a version, so nothing is ever found. tagVersion supplies the mapping, as in this sketch.
upstream: { owner: "postgres", repo: "postgres", kind: "tags", tagVersion: (tag) => { const m = /^REL_(\d+)_(\d+)$/.exec(tag); return m ? `${m[1]}.${m[2]}` : null; }, trackMajor: true,},The tooling then behaves as follows.
- A tag the function maps to
nullis skipped, so the pattern above drops the_BETA/_RCtags,REL_18_STABLEand the old spellings. Tags withrc,alpha,beta,preview,nightly,devorcanaryin them are dropped before the function runs, whatever it would return. - Versions are compared numerically segment by segment, so
17.11is newer than17.9. The mapped value (18.6) is what the report shows astoand whatreplace(v, line)receives. The pinned constant may hold the mapped version or the raw tag (REL_18_6). Both read as the same version. - With
tagVersionset the tool reads every page of the tag list (100 per page, at most 20 pages), because the tags API is not ordered by version and the newest minor is not on the first page. Without it, only the first 50 items are read, as before. trackMajor: truekeeps the answer in the pinned version’s major line. A pin on17.9reports17.11and never18.6. This is for a lexicon that commits one snapshot per supported major: each pin is checked against its own line. Without it the highest version of any major wins.
When there is no version to pin to
Section titled “When there is no version to pin to”upstreamPin resolves a version constant against GitHub releases or tags. Some
upstreams offer neither. The CloudFormation Registry schema is a single “latest”
artifact on an S3-backed host, republished constantly, with no version anywhere
in the path. aws generated against whatever AWS shipped that morning, and a
docs-only branch twice picked up a resource-count move with nothing in the
commit explaining it (chant #1390).
Where there is no version, pin the content: lexicons/aws/src/spec/pin.ts
records a sha256 over the extracted schemas — sorted typeName ->
sha256(schema) — plus the type names it covered, and generation refuses when
upstream no longer matches:
The upstream CloudFormation schema has moved since the pinned one.
pinned sha256:a2d99e08… (1650 resources, accepted 2026-08-03) upstream sha256:1357a446… (1651 resource types, +1 against the pin) added AWS::Fake::NewThingCopy two details if you need the same thing:
- Digest the extracted content, not the archive. A repackaged zip has a new
ETagand new bytes while the schemas inside are identical; a pin that fires on that is noise nobody will keep. - Commit the type names beside the digest. Generated artifacts are not committed, so without the list an acceptance is one opaque hash replacing another. With it, the PR that moves the pin shows exactly what AWS added.
Unlike the emulator image pins, which only report (chant #808), this one refuses. An emulator that drifts fails a test you can see; a spec that drifts rewrites committed artifacts in a branch about something else.
Next Steps
Section titled “Next Steps”Next, create a serializer that converts evaluated resources to your target format.