Lexicons
A lexicon is a collection of types and semantic lint rules for an operational area. The core system is lexicon-agnostic — it discovers files, collects exports, resolves dependencies, and runs semantic lint rules. Each lexicon plugs into this pipeline with its own types and rules.
Available Lexicons
Section titled “Available Lexicons”| Lexicon | Package | Resources | Reference |
|---|---|---|---|
| AWS CloudFormation | @intentius/chant-lexicon-aws | 1500+ | Reference |
| Azure ARM | @intentius/chant-lexicon-azure | 200+ | Reference |
| GCP Config Connector | @intentius/chant-lexicon-gcp | 150+ | Reference |
| Kubernetes | @intentius/chant-lexicon-k8s | 50+ resources, 35+ properties | Reference |
| Helm | @intentius/chant-lexicon-helm | 10+ resources | Reference |
| GitHub Actions | @intentius/chant-lexicon-github | 3 resources, 14 composites | Reference |
| GitLab CI/CD | @intentius/chant-lexicon-gitlab | 3 resources, 16 properties | Reference — also ships GitHub Actions migration |
| Forgejo Actions (Codeberg / Gitea) | @intentius/chant-lexicon-forgejo | thin github dialect (reuses github entities) | Reference — ships GitHub Actions migration + forgejo:compare |
| Docker | @intentius/chant-lexicon-docker | 6 resources (Compose + Dockerfile) | Reference |
| Fly Machines | @intentius/chant-lexicon-fly | 6 resources (Machines API) | Reference — ships the mudflaps emulator for offline round-trips |
| Fountain | @intentius/chant-lexicon-fountain | 6 resources (Environment, Vault, Agent, Teammate, Schedule, Webhook) | Reference — sandboxed agent workloads and the steward that operates them; conversations are ops, not resources |
| Render | @intentius/chant-lexicon-render | 14 resources (Public API) | Reference — services, datastores, env groups, projects; applied straight to the Public API |
| Control Plane | @intentius/chant-lexicon-cpln | 8 resources, 142 property types | Reference — GVCs, workloads, identities and secrets |
| Cedar | @intentius/chant-lexicon-cedar | Policy and Schema, plus entity and action types generated from your schema | Reference — emits .cedar and Cedar JSON; observes and imports against Amazon Verified Permissions |
| k3d | @intentius/chant-lexicon-k3d | 1 resource (Cluster) + config property types, pinned to k3d v5.9.0 | Reference — emits the k3d.io/v1alpha5 config k3d cluster create --config consumes; k3dUp/k3dDown Op activities |
| k3s | @intentius/chant-lexicon-k3s | 3 resources (Server, Agent, Registries) | Reference — emits config.yaml and registries.yaml for /etc/rancher/k3s/; k3sInstall/k3sUninstall Op activities |
| Terraform | @intentius/chant-lexicon-terraform | one entity per HCL block, no generated resource surface | Reference — reads an existing root module’s .tf files as-is; terraformInit/terraformPlan/terraformApply/terraformShow Op activities and the TerraformApplyOp composite |
Runtime observation coverage
Section titled “Runtime observation coverage”Lexicons opt into the chant lifecycle diff <env> --live pipeline by implementing one of two plugin methods: describeResources() (entity-keyed — for 1:1 cloud resources) or listArtifacts() (context-keyed — for runtime concepts created by tooling outside chant’s entity model). See Drift Detection — Resources and artifacts for the conceptual distinction, and Implementing Observation for the author-side walkthrough.
A third, optional method goes one level deeper. observeResourcesDeep() reads the full live property tree per declared entity, which is what turns drift detection from “this resource changed status” into “this property changed, from this to that” — including properties source never declared, the class CloudFormation’s own drift detection cannot see. See Deep observation.
| Lexicon | describeResources() | observeResourcesDeep() | listArtifacts() | Ownership | Notes |
|---|---|---|---|---|---|
| AWS | ✅ | ✅ | Tags | CloudFormation DescribeStackResources over the applier’s own read transport (chant #1206). Deep read: the Cloud Control resource model for S3 buckets and IAM roles/managed policies, plus a bulk aws ec2 describe-security-groups (still a CLI spawn) mapped onto the CloudFormation model for security groups — four kinds today, the remainder tracked in chant #1015/#1269. --owned available on the deep read (the payloads carry tags) and on live export, not on describe | |
| Azure | ✅ | ✅ | Tags | Thin and deep reads both GET the full ARM resource JSON over the applier’s ARM transport (chant #1212 — no az CLI); resource-group is the env name. The deep read flattens ARM’s properties.* onto the shape chant’s generated classes declare; top-level ARM types only — a nested compound type reports unsupported-kind. --owned available on live export | |
| GCP (Config Connector) | ✅ | ✅ | Labels | Direct REST GETs via the applier’s per-kind mappers (chant #1209 — no Config Connector cluster needed to observe). The deep read reshapes the REST body into the declared CNRM shape (metadata/spec) and prunes noise by a static per-kind table — a REST payload carries no field ownership to prune by. A kind with no mapper reports unsupported-kind | |
| Kubernetes | ✅ | ✅ | Labels | Typed API client (@intentius/chant-k8s-client) — concurrent reads, any kind the cluster’s discovery reports including CRDs, typed errors; cluster resolved from k8s.profiles.<env>.context when bound (chant #1100) — a mismatched ambient context refuses loudly rather than reading the wrong cluster. Deep pruning is derived from metadata.managedFields: a field a foreign manager owns and source never declared is controller noise; a foreign write to a field chant declares stays visible, and the drift report names the owning manager | |
| Fly Machines | ✅ | Metadata | Machines API (flaps) reads, or the mudflaps emulator offline. Two-tier ownership: machines carry managed-by: chant in config.metadata; volumes, IPs and certificates have no per-resource marker, so ownership is inferred at the app boundary and says so | ||
| Fountain | ✅ | ✅ | Metadata | REST reads against the fountain API, one list per declared kind, over all six: environments, vaults, agents, teammates, schedules and webhooks (chant #2128). Two-tier ownership: --owned reads the managed-by: chant metadata marker, which environments, vaults and agents carry; a teammate and a schedule have no metadata of their own and inherit the verdict from the agent behind them; a webhook inherits nothing and reports unknown, so it is never proposed for deletion. Identity follows the serializer: a teammate keys on its roster name, a schedule on its teammate and its name together, a webhook on its url. The list endpoints render the whole record, so the deep read (chant #1217) needs no second GET per resource, and fountain’s JSON views name their fields the way the request schema does — the payload passes through and noise is a static per-kind table, since a REST payload carries no field ownership to prune by. Every server-resolved id is reported as the name the vocabulary source writes the reference in — an agent’s environment_id as the environment’s name, a schedule’s agent_id as its teammate, a teammate’s launch bindings as its environment and vault. A teammate is the one kind projected rather than passed through, because GET /api/team renders a roster row (presence, unread state, the last turn) rather than a record. Secrets are listed keys-only and collapsed whole on both sides: presence classifies, a key or a value never reaches a diff row | |
| Render | ✅ | Env vars | Public API reads, one name-scoped list per declared entity plus a workspace scan for orphans. Two-tier ownership: services and env groups carry CHANT_MANAGED_BY=chant in their env vars (Render has no tags), and disks/custom domains inherit their service’s verdict; datastores, projects, environments, credentials and webhooks have no marker channel and report unknown, so they are never proposed for deletion | ||
| Control Plane | ✅ | Tags | REST reads against the Control Plane Core API. The GVC-scoped kinds have an org-wide rollup (/org/{org}/workload alongside the per-GVC form), so the read is one request per kind rather than one per resource. tags is the ownership channel and every read path returns it, so --owned resolves a real verdict on the thin read — which is why describeResources is declared on the channel | ||
| Cedar (AVP) | ✅ | AVP tags | Reads the declared policy set against a live Amazon Verified Permissions policy store. The tri-state carries more weight here than on most substrates — a declared policy reported absent becomes a create, and creating a permit that already exists duplicates a grant — so every path that did not look says so. Design record in lexicons/cedar/src/avp/OWNERSHIP.md | ||
| k3d | ✅ | Labels | Two commands (chant #1412): k3d cluster list -o json for presence and running state, and docker inspect on the cluster’s first server node for the ownership marker, since k3d’s list output carries runtimeLabels only on the node | ||
| k3s | ✅ | Labels | The #1412 k3d answer applied to the Server/Agent shape (chant #1603). Reuses the same k8s.profiles.<env>.context binding helm and the k8s lexicon read (chant #1488), so the kubectl context is resolved rather than ambient | ||
| Helm | ✅ | ✅ | ✅ | Release identity / Labels | Per-release reads over the helm CLI (chant #1246): each declared Helm::Chart resolves to its release, and helm get manifest + helm get hooks — both channels, since hooks are excluded from the manifest — become per-resource rows. Rows are owned via release identity and classify as runtime children of their chart, so an unpinned release is not drift. The deep read (chant #1247) delegates to the k8s lexicon’s typed client and reuses its deepNormalizationHooks, so managed-fields ownership and Secret masking apply unchanged. helm list -A -o json still reports every release as an artifact — declared or not |
| Terraform | ✅ | State file, or the marker tags on a choudoufu root | terraform show -json over each configured root’s current state, matched against the <root>/<address> keys buildRoots() produced (chant #2087). On a stock root ownership is state membership, not a marker: that root is on the trusted-state-file row of lifecycle models’s third axis, so an address the state carries is owned and everything else is unknown (a declared module block, which state has resources under but no row for, lands there). Only resource and data blocks have rows at all; a terraform/provider/variable/output block reports unsupported-kind. A root under binary: "choudoufu" that declares an estate is read by choudoufu live-plan -json instead and sits on the live-marker row like every other lexicon (chant #2104): its tofu-estate/tofu-address tags are the answer, a bound instance is owned, an unowned resource at a declared identity is foreign, an adoptable match is unknown carrying the two tag values that would claim it, and live-ls -estate -json enumerates the owned orphans for observeAmbient/teardownOwned. A root whose read fails reports every entity it declares read-failed, or no-credentials where the failure names credentials, naming the root | ||
| Docker | ✅ | — | docker ps, docker image ls, docker network ls (NDJSON) | ||
| GitHub Actions | — | N/A — workflow definitions are git-tracked; drift is git diff (rationale) | |||
| GitLab CI/CD | — | N/A — same rationale (README) | |||
| Forgejo Actions | — | N/A — workflow definitions are git-tracked; drift is git diff (same as github) |
Lexicons that implement neither are warn-skipped — --live doesn’t fail the whole command for them.
The Ownership column shows the marker channel a lexicon queries for the --owned filter (ownership marking). — means the --owned query isn’t available for that lexicon — it has no durable marker channel. Where a lexicon is asked for owned resources but can’t read a marker on a given path — e.g. AWS and Azure describeResources, which the filter degrades to detect-only — it stamps an explicit unknown verdict and returns everything rather than silently filtering. The --owned filter is honored on live export, where the full config (with tags) is available.
Every observing lexicon reports per declared entity in three states, not two (the observation contract): observed present, observed absent, or not observed with a reason. Coverage gaps in the table above — Azure’s nested ARM types, a read denied by RBAC — surface as unobserved entries in lifecycle diff --live and lifecycle plan, never as absences, so an uncovered resource is reported as unknown instead of being proposed for creation.
Cross-lexicon migration
Section titled “Cross-lexicon migration”The GitLab lexicon ships a chant migrate source for from: github — translates .github/workflows/*.yml into .gitlab-ci.yml (or typed chant TypeScript), with provenance recorded as SARIF and a curated mapping table for the top 33 marketplace actions. See GitLab -> Migration for the full surface, or the chant migrate CLI reference.
The Forgejo lexicon implements the same migrationSource("github") hook — a thin edge, since Forgejo runs GitHub-Actions-compatible YAML. Its value is the forgejo:compare safety view: a per-property security-fate report of what the move costs (permissions/continue-on-error -> lost, unresolved uses: -> needs-review).
Future lexicons can opt into the same machinery by implementing migrationSource(from: string) on their plugin.
What a Lexicon Provides
Section titled “What a Lexicon Provides”TypeScript definition files that describe available resource classes, their properties, and intrinsic functions. These types power editor autocompletion, inline documentation, and compile-time checks.
Lint rules specific to the target ecosystem. These integrate with chant lint to catch provider-specific mistakes like invalid resource configurations or naming convention violations.
Serialization
Section titled “Serialization”Each lexicon implements the Serializer interface to convert evaluated resources into the target platform’s output format.
interface Serializer { name: string; rulePrefix: string; serialize(entities: Map<string, Declarable>, outputs?: LexiconOutput[]): string | SerializerResult;}Bundles
Section titled “Bundles”A bundle is the distributable form of a lexicon — a dist/ directory containing everything chant needs at build time:
| Artifact | Description |
|---|---|
manifest.json | Lexicon metadata (name, version, intrinsics, pseudo-parameters) |
meta.json | Resource registry mapping type names to definitions |
types/index.d.ts | TypeScript declarations for all resource and property types |
integrity.json | Per-artifact xxhash64 checksums |
rules/ | Lint rule implementations |
skills/ | AI assistant skill definitions |
When you install a lexicon package (e.g. @intentius/chant-lexicon-aws), the bundle is what ships inside it. Lexicon authors produce bundles with chant dev publish.
Why Lexicons Exist
Section titled “Why Lexicons Exist”chant is provider-agnostic by design. The core handles parsing, module resolution, evaluation, and serialization — but knows nothing about specific infrastructure platforms.
Lexicons bridge this gap:
- The core stays lean — no bundled provider SDKs or type catalogs
- Providers update independently — new resource types ship as lexicon updates
- Custom targets are first-class — organizations can author internal lexicons
- Projects declare exactly what they need — explicit, reproducible dependencies
Next Steps
Section titled “Next Steps”- Managing Lexicons — add, remove, and update lexicons
- Lexicon Authoring — create and publish your own lexicons