Skip to content

Development Setup

llms.txt lists every page for an agent
Optional: hand this page to your coding agentThe steps work by hand too.
Show the whole prompt
Read https://intentius.io/chant/contributing/development/.
Set up the chant repository on this machine: run `npm install` and `just regen`, then `just check` and `npx tsx scripts/typecheck.ts`.
Show me the output of each, and the first failure if one fails. Change no file.
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`.
  • Node.js 24+ — runtime. The repo .nvmrc pins Node 24, which is what CI tests against.
  • tsx — TypeScript execution (npm install -g tsx)
  • Git — for version control
  • just — task runner (optional, but recommended)
Terminal window
git clone https://github.com/intentius/chant.git
cd chant
npm install

The project uses just for common tasks. Run just with no arguments to see all available commands:

Terminal window
just # list all commands
just install # install all dependencies (including docs)
just build # type-check packages/core on TypeScript 7
just test # run all tests
just test-e2e-affected # run the e2e tests your change reaches
just lint # run eslint on packages/
just check # run build + lint + test
just docs # start the docs dev server
just bench # run performance benchmarks
just smoke # build and run the Docker smoke test
just docs-build # build the unified documentation site
Terminal window
npx vitest run # all tests
npx vitest run packages/core # core package only
npx vitest run lexicons/aws # AWS lexicon only
npx vitest run lexicons/k8s # K8s lexicon only

The suite is two vitest projects. unit is every test file except *.e2e.test.ts, and each of its tests has a 5 second budget: a test that runs longer fails with a message naming the budget. e2e is every *.e2e.test.ts, the tests that spawn the CLI, run tsc over fixtures, build every example in the corpus, boot containers or release whole projects, all on the machine running the suite. They are not the cloud deployments on the E2E Testing page. Pull request CI runs only the unit tests. They run in four shards beside the typechecks and lint. The e2e tests run in release-suites.yml on main every three hours and when someone starts it, and a release only tags a commit they passed.

Terminal window
npx vitest run --project unit # unit tests only
npx vitest run --project e2e # end-to-end tests only

A unit test over the budget either gets faster or moves into a *.e2e.test.ts file, and so does a test that runs the CLI or tsc over whole projects, or builds the example corpus. CHANT_UNIT_TEST_BUDGET_MS sets a different budget for a local run, and 0 turns it off.

Pull requests don’t run the e2e tests, so run the ones your change reaches yourself:

Terminal window
just test-e2e-affected # against origin/main
just test-e2e-affected main --list # print the files without running them

It runs every changed *.e2e.test.ts, the e2e files in the same directory as a changed file, and the e2e files vitest’s import graph relates to the diff (--no-related leaves those out). When the change touches examples/, a lexicon’s examples/ or src/serializer*, or docs/, it also runs the three example gates: root-examples-gate, sandbox-execution-boundary and fold-differential. The last line is a one-line result to paste into the pull request. Unlike just test-e2e, it doesn’t wait for a person to type run, so coding agents run it too.

Lexicon tests import each lexicon’s generated barrel (src/generated/) and bundled dist/meta.json. Neither is committed. just test builds whatever is missing before running vitest (about a minute the first time, nothing afterwards). If you run npx vitest run directly on a fresh clone, the run stops before any test with a message naming the lexicons that need generating. Build them with either of these:

Terminal window
just regen # every lexicon
npm run generate -w lexicons/aws && npm run bundle -w lexicons/aws # one lexicon

Set CHANT_SKIP_ARTIFACT_CHECK=1 to bypass the check.

The K8s lexicon has additional integration tests that run against real manifests from kubernetes/examples:

Terminal window
cd lexicons/k8s
# Parse-only roundtrip (parse → generate)
just import-samples --skip-clone --verbose
# Full roundtrip (parse → generate → import → serialize → compare)
just full-roundtrip --skip-clone --verbose
# k3d cluster validation (requires k3d + kubectl + Docker)
just k3d-validate --verbose
  • Unit tests live next to source files or in __tests__/ directories
  • The packages/test-utils/ directory (@intentius/chant-test-utils) provides shared test fixtures, assertions, and the example test harness

Example tests are centralized — each lexicon has a single examples.test.ts that auto-discovers all its examples. Example directories contain only source code (no .test.ts files), so users who copy examples get clean projects.

Per-lexicon examples live at lexicons/<name>/examples/<example>/src/ and are tested by lexicons/<name>/examples/examples.test.ts.

Cross-lexicon examples (e.g. AWS + K8s, GitLab + AWS) live at examples/<name>/src/ and are tested by examples/examples.e2e.test.ts.

Both use the shared harness from @intentius/chant-test-utils/example-harness:

import { describeAllExamples } from "@intentius/chant-test-utils/example-harness";
import { mySerializer } from "@intentius/chant-lexicon-my-lexicon";
describeAllExamples({
lexicon: "my-lexicon",
serializer: mySerializer,
outputKey: "my-lexicon",
examplesDir: import.meta.dirname,
}, {
// Per-example overrides (optional)
"docs-snippets": { skipLint: true },
"my-example": {
checks: (output) => { expect(output).toContain("expected"); },
},
});

Adding a new example: create a subdirectory with src/ and a package.json — the centralized examples.test.ts auto-discovers it with no test file changes needed. Add an override entry only if you need custom assertions or to skip lint/build.

Terminal window
scripts/tsc7.sh --noEmit --declaration false --declarationMap false -p packages/core/tsconfig.json
npx tsx scripts/typecheck.ts # tests, scripts and examples, against a baseline

Two TypeScript versions are installed:

WhatVersionHow it is called
Typechecks, and every package and lexicon .d.ts build7.0scripts/tsc7.sh, which runs the root alias typescript-native by path
Code that imports typescript: core (fold, lint, codegen), lexicon lint rules, eslint’s parser5.9import ts from "typescript"

TypeScript 7’s typescript package has no JS compiler API, so typescript stays on 5.9. Both packages ship a tsc bin. node_modules/.bin/tsc is 5.9, so a plain npx tsc still runs 5.9. scripts/tsc7.test.ts fails if any of these changes.

On TypeScript 7, a --noEmit check with declaration: true is slower than on 5.9, because it still computes declaration diagnostics. Pass --declaration false --declarationMap false to noEmit checks. Declaration errors are still caught by each package’s build.

chant/
├── packages/
│ ├── core/ @intentius/chant — type system, pipeline, CLI, and codegen infrastructure
│ │ └── src/
│ │ ├── runtime.ts createResource/createProperty factories
│ │ ├── codegen/
│ │ │ ├── naming.ts NamingStrategy (collision-free class names)
│ │ │ ├── generate.ts generatePipeline (schema → artifacts)
│ │ │ ├── package.ts packagePipeline (artifacts → BundleSpec)
│ │ │ └── fetch.ts fetchWithCache, extractFromZip
│ │ └── lsp/
│ │ └── lexicon-providers.ts LexiconIndex, completions, hover
│ └── test-utils/ shared test fixtures and assertions
├── lexicons/
│ ├── aws/ @intentius/chant-lexicon-aws — AWS CloudFormation lexicon
│ ├── gitlab/ @intentius/chant-lexicon-gitlab — GitLab CI/CD lexicon
│ └── k8s/ @intentius/chant-lexicon-k8s — Kubernetes lexicon
├── docs/ Astro Starlight documentation site
└── package.json

The packages/core/src/codegen/ directory contains reusable infrastructure for lexicon code generation pipelines. Lexicons supply provider-specific callbacks and data tables; core handles orchestration, logging, warning collection, stats counting, and artifact writing. The AWS lexicon (lexicons/aws/) demonstrates this pattern — its codegen files are thin wrappers around core pipelines.

Every page under docs/src/content/docs/ carries a diataxis frontmatter field with one of four values, following Diátaxis:

ValueThe page is forSidebar group
tutoriallearning by doing, start to finishTutorials
how-togetting one task doneHow-to guides
referencelooking something upReference
explanationunderstanding whyExplanation

One mode per page. A page that needs two gets split. The sidebar in docs/astro.config.mjs is grouped by the same four values, and just docs-check fails when a page is missing the field, is in no sidebar group, or sits in a group that does not match its field. index and whats-new are the only exempt pages.

Lexicon docs sites use the same four values in lexicons/<name>/docs/pages/*.mdx; the docs pipeline builds their sidebar from the field.

chant