Skip to content

Package & Publish

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/lexicon-authoring/package/.
Make the lexicon I name ready to publish: its `src/package-cli.ts` on `packagePipeline` behind `npm run bundle`, `files`, `exports` and the `prepack` chain shaped like `lexicons/k3s/package.json`, and `chantVersion` in the manifest.
Run `npm run prepack`, then `chant dev check-lexicon lexicons/<name>` until tier 1 is clean. Run `npm pack --dry-run`, show me the file list, and open a pull request.
Do not run `npm publish` or `chant dev publish`.
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 package lifecycle method bundles your lexicon’s generated artifacts for publishing. Core’s packagePipeline assembles the bundle and writeBundleSpec writes it to disk.

packagePipeline returns a PackageResult in memory. It writes nothing. writeBundleSpec(spec, distDir) creates dist/. Without it, chant dev check-lexicon reports dist/manifest.json exists as failing.

src/codegen/package.ts
import { dirname } from "path";
import { fileURLToPath } from "url";
import { packagePipeline, collectSkills } from "@intentius/chant/codegen/package";
import type { PackageOptions, PackageResult } from "@intentius/chant/codegen/package";
import { myPlugin } from "../plugin";
import { generate } from "./generate";
// Lexicon packages are ESM, so `__dirname` does not exist.
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
export async function packageLexicon(opts: PackageOptions = {}): Promise<PackageResult> {
return packagePipeline(
{
generate: (genOpts) => generate(genOpts),
buildManifest: (genResult) => ({
name: "my-lexicon",
version: "1.0.0",
chantVersion: ">=0.1.0",
}),
srcDir: pkgDir,
collectSkills: () => collectSkills(myPlugin.skills?.() ?? []),
},
opts,
);
}
// src/package-cli.ts, run by `npm run bundle`
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { writeBundleSpec } from "@intentius/chant/codegen/package";
import { packageLexicon } from "./codegen/package";
const pkgDir = dirname(fileURLToPath(import.meta.url));
const { spec, stats } = await packageLexicon({ verbose: true });
writeBundleSpec(spec, join(dirname(pkgDir), "dist"));
console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules`);

See lexicons/aws/src/codegen/package.ts and lexicons/k3s/src/package-cli.ts for the shipped pair.

FieldRequiredWhat it does
generateYesRuns your generation pipeline and returns the GenerateResult
buildManifestYesBuilds the LexiconManifest from that result
srcDirYesPackage root, scanned for rule files
collectSkillsYesReturns Map<filename, content>. collectSkills(defs) maps SkillDefinition[] to <name>.md
ruleDirsNoRule directories relative to srcDir. Defaults to ["lint/rules", "lint/post-synth"]
versionNoRecorded as metadata.generatorVersion. Defaults to "0.0.0"

writeBundleSpec creates the dist/ directory:

ArtifactDescription
manifest.jsonLexicon metadata (name, version, intrinsics, pseudo-parameters)
meta.jsonRecord<shortName, LexiconEntry> resource registry
integrity.jsonPer-artifact sha256 hashes plus a composite hash over the sorted path:hash pairs
types/index.d.tsTypeScript declarations for all resource and property types
rules/*.tsLint rule implementations
skills/*.mdAI assistant skill definitions
okf/**OKF knowledge bundle derived from the same registry and rules (chant #1060), replaced wholesale on each write

The integrity record covers manifest.json, meta.json, types/index.d.ts and every file under rules/ and skills/. okf/ is not hashed.

The buildManifest callback must return a LexiconManifest:

FieldTypeRequiredDescription
namestringYesLexicon identifier (e.g. "aws", "k8s")
versionstringYesLexicon version (e.g. "1.0.0")
chantVersionstringNoMinimum chant version required
namespacestringNoType namespace prefix (e.g. "AWS")
intrinsicsIntrinsicDef[]NoIntrinsic function definitions
pseudoParametersRecord<string, string>NoPseudo-parameter names and descriptions

chantVersion is optional to the type and required in practice: chant dev check-lexicon fails tier 1 when the packaged manifest omits it. It is checked for presence and shape only, never for compatibility with the running core.

Core’s docsPipeline and writeDocsSite generate a standalone Starlight docs site from your lexicon’s dist/ artifacts:

import { docsPipeline, writeDocsSite, type DocsConfig } from "@intentius/chant/codegen/docs";
const config: DocsConfig = {
name: "my-lexicon",
displayName: "My Lexicon",
description: "Description for page metadata",
distDir: "./dist",
outDir: "./docs",
serviceFromType: (type) => type.split("::")[1] ?? "Other",
};
const result = docsPipeline(config);
writeDocsSite(config, result);

This creates a self-contained Starlight site with pages for resources, intrinsics, pseudo-parameters, rules, and serialization. Build it with cd docs && npm install && npm run build.

Registering docs() on the plugin is a tier-1 requirement, so wire the method as well as the "docs" script in package.json. The full field list for DocsConfig, including the authored pagesDir and the Diátaxis quadrants the sidebar is grouped from, is in Docs Site.

Once the bundle is built:

  1. Run chant dev check-lexicon <dir> and confirm tier 1 is clean
  2. Set files to ["src/", "dist/"]. tsc compiles modules only, so a compiled module reads the generated files beside its source from src/ (moduleSourceDir in @intentius/chant/module-source). Fold reads a composite’s source there too. Shipping only dist/ breaks both
  3. Give every module export two conditions: development on ./src/*.ts and default on the compiled ./dist/*.js. The build script runs tsc on tsconfig.build.json and keeps the .js it emits. Both are tier-1 checks, and scripts/lexicon-exports.test.ts checks every export of every lexicon in this repository
  4. Chain the bundle step and the build into prepack, so dist/ is rebuilt from the pinned spec at publish time rather than from whatever is on disk
  5. Publish with npm pack (for testing) or npm publish

An installed chant runs plain node and loads a lexicon’s dist/*.js with no TypeScript loader (#3981). In this repository the tools resolve the development export instead. tsc reads it through customConditions and tsx through the root tsconfig paths. vitest adds the condition itself, and bin/chant passes --conditions=development. A lexicon runs from its source there whether or not dist/ has been built. A compiled lexicon that imports another lexicon needs that one compiled too, so every lexicon ships the same way.

lexicons/k3s/package.json is the shape to copy, including its "prepack": "npm run generate && npm run bundle && npm run validate && npm run build".

Users install your lexicon with:

Terminal window
npm install --save-dev @intentius/chant-lexicon-<name>

Once the bundle is ready, run chant dev onboard to wire the lexicon into CI, Docker smoke tests, and the npm publish workflow. See CI & Distribution for the full checklist.

chant