Generating record models

The pub.layers.* record models are generated from the vendored Layers lexicons and committed to the repository. No model is hand-written. A new Layers version is adopted by re-vendoring the lexicon tree, regenerating the models, and confirming the drift gate is clean. The generated modules carry descriptions, optionality, refined value types, integer ranges, knownValues, and union discriminators that a lossy schema round-trip would drop, so editing a generated file by hand is both forbidden and pointless: the next regeneration overwrites it.

This guide drives the two CLI commands that own the pipeline: lairs vendor and lairs gen. For the rationale (why generation rather than authoring) see Generated models.

Vendor a lexicon tree

lairs vendor copies a lexicons/pub/layers tree from a local Layers checkout into lairs/lexicons and rewrites MANIFEST.toml. Point --from at the tree to vendor:

lairs vendor --from /path/to/layers/lexicons/pub/layers \
    --layers-version 0.7.0 \
    --layers-git-sha 3f2a1c9 \
    --vendored-at 2026-06-16

The three provenance flags are optional. When omitted, the existing manifest values are kept. To vendor a specific upstream ref or tag, check it out first, then point --from at its lexicons/pub/layers. Fetching by ref is a thin wrapper over the same copy-and-manifest step, not a separate command.

Vendoring recomputes a content hash over the new tree. When the copied content is byte-identical to the tree already vendored, the existing hash and vendoring date are retained, so a no-op re-vendor does not invalidate the committed generated models. The command prints the file count and the resulting lexicon_tree_hash, then reminds you to run lairs gen.

Regenerate the models

lairs gen reads the vendored tree and writes one module per pub.layers.* namespace into lairs/records/_generated, plus the package __init__:

lairs gen

Each lexicon document is parsed through panproto, mapped to the codegen intermediate representation, and emitted as module text. Permission-set (OAuth scope) lexicons are skipped, and a namespace whose lexicons are all method-only (query, procedure, subscription) contributes no records and emits no module, so the vendored tree can mirror the full Layers tree without forcing a module for every namespace. Cross-namespace embeds (for example annotation embedding defs#anchor) are resolved to imports so every module type-checks in isolation. Emission is deterministic. Classes and fields are ordered stably, and the output is run through ruff format, ruff check --fix, and ruff format again so the committed modules converge to a lint-clean canonical form. That determinism is what makes the drift gate meaningful.

The generated modules are the import surface of lairs.records. A class imports as from lairs.records._generated.<namespace> import <Class> and is also re-exported through the namespace attribute, so lairs.records.expression.Expression and lairs.records.defs.Anchor resolve the same classes. A lexicon union becomes a dx.TaggedUnion whose discriminator is kind and whose variants pin a Literal discriminator value.

Run the drift gate

lairs gen --check regenerates into a temporary directory and compares the result byte-for-byte against the committed modules. It exits zero when they match and non-zero when they are stale:

lairs gen --check

This is the CI gate that keeps the committed models honest. It catches two failure modes: a vendored tree that was changed without regenerating, and a generated module that was hand-edited. When it reports staleness, run lairs gen and commit the result.

What the manifest records

lairs/lexicons/MANIFEST.toml is the provenance of the vendored tree. Its runtime form is the Manifest model, and the TOML file is its serialized, reviewable form, written by lairs vendor. It records, under [provenance], the upstream layers_git_sha, the layers_version, the vendored_at date, and the lexicon_tree_hash, and under [counts], the lexicon_files and record_types totals. The Manifest model is flat: the [provenance] and [counts] groupings are a property of the TOML serialization, not of the model fields.

The lexicon_tree_hash is the deterministic SHA-256 over the sorted per-file hashes of every lexicon JSON document, so it depends only on file paths and contents and is stable across machines. That hash is stamped into the header of every emitted module, so a generated file records the exact lexicon revision it came from:

# generated by lairs gen; do not edit
# lexicon tree hash cedfb9f34cc55fb5be1abb81dda63fd6a6671af2efd6a5e5923fca0c77ba0fd1

See also