This is the abridged developer documentation for codegen # codegen > A scriptable meta-programming engine for modern C++. **codegen** is a production-grade C++ code generation engine. You annotate C++ declarations directly in source — structs, classes, enums, free functions, aliases — and each rule declares in its YAML which AST node kinds it consumes. You write a LuaU script that receives a structured AST node as JSON and emits whatever you want — C++, TypeScript, Markdown, SQL. The engine handles the rest: parsing, scheduling, deduplication, inline injection, and file routing. It ships as a single binary. It calls nothing home. It runs the same on every machine. ## How codegen is built [Section titled “How codegen is built”](#how-codegen-is-built) codegen separates the problem into two clean layers: 1. **Parsing** — handled by the engine. The full C++ AST is resolved and serialised to JSON, including namespaces, template arguments, member variables, enumerators, and annotations. 2. **Transformation** — handled by you, in LuaU. Your rule receives one AST node as JSON and returns output text. The rule has no compiler coupling, no filesystem access, and no network access. This split is what makes everything below possible. *** ## Core capabilities [Section titled “Core capabilities”](#core-capabilities) Deterministic output Same source ⇒ same output. Always. The engine is a pure function over your AST. Generated files are stable across machines and CI runs, making diffs meaningful and code review tractable. Non-1:1 generation Most generators emit one output file per input file. codegen does not require this. A `grouping.luau` script routes each matched entity to any output path — fan-in dozens of structs into a single `api-reference.md`, a `registry.cpp`, or a sorted `bindings.ts`. The topology is yours to define. Cross-language generation Rules emit text. The engine does not care whether that text is C++, TypeScript, Markdown, SQL, or a JSON manifest. One annotated C++ type can drive a serialiser, an FFI binding, a documentation page, and a database migration — all from the same source of truth. LLM-ready AST schema The C++ AST is fully serialised as JSON before any rule sees it. Rules are testable without a compiler, inspectable with `jq`, and the full docs are also published as a single plain-text bundle at [`/llms-full.txt`](/llms-full.txt) — drop it into any LLM agent for codegen-aware code intelligence. Rules testable in isolation Feed a JSON blob to a rule, assert on the output. No compiler, no project, no fixtures. The same property that makes rules LLM-friendly makes them unit-testable in seconds. Isolated execution LuaU runs in a sandboxed VM. Rules have no filesystem access, no network access, no process spawning — unless you explicitly grant permissions in the config. A malicious rule in a third-party library cannot exfiltrate your source code. Inline injection Rules can emit a `source` (to a `.g.cpp` file) and an `inline` list — declarations injected back into the original header at a designated anchor comment. Bare anchors are rewritten into idempotent `:begin]] / :end]]` blocks on first run. Self-hosted codegen generates parts of its own infrastructure. We use it ourselves to build it, so every feature you rely on has already been exercised against a 100k+ LoC real-world codebase. *** ## Licensing [Section titled “Licensing”](#licensing) Community **Free forever** For open-source projects and personal exploration. * CLI-only interface * All built-in rule examples included * Community support (GitHub Issues) *No commercial use.* Professional **€12 / month · €99 / year · Individual** For solo developers building commercial software. * Everything in Community * **TUI diff viewer:** review generated changes before applying * **VS Code DAP debugger:** step through LuaU rules in the editor *14-day free trial. Annual saves \~31% (€8.25 / month effective).* Team **from €24 / seat / month · €230 / seat / year · Organization** For engineering teams that ship at scale. * Everything in Professional * **CMake integration:** rules run as part of the build graph * **Shared `.codegen/shared/` libraries:** standardise grouping and preambles * **Priority support:** direct response SLA *30-day free trial. Volume discounts: 15% at 5+ seats, 25% at 20+ seats.* [Compare tiers in detail ⇒](/licensing/tiers/) # The AST Schema > The JSON structure handler scripts receive, fields, node kinds, and how to navigate namespace and type information. Every handler invocation receives one entity AST node, JSON-encoded. Call `json.decode(input)` to get a Lua table. This page describes the fields available on the most common node kinds. The full per-kind schema is auto-generated and available under [AST Schemas](/reference/schemas/). ## What the engine adds before the handler sees the JSON [Section titled “What the engine adds before the handler sees the JSON”](#what-the-engine-adds-before-the-handler-sees-the-json) The engine takes the codex `toJSON` output and: 1. Removes “heavy” fields the handler rarely needs: `memberFunctions`, `staticMemberFunctions`, `constructors`, `destructors`, `operators`, `nestedTypes`, `statements`. Use the [native introspection API](/reference/luau-globals/) (`get_node`, `get_base_classes`, etc.) to retrieve them on demand. 2. Adds `_namespaces` (array of namespace strings, outermost first). 3. Adds `_registryId` (an opaque integer; pass it to `get_node`, `get_base_classes`, etc.). 4. Adds `params` (a copy of `rule.params` from the config). ## Struct / class node [Section titled “Struct / class node”](#struct--class-node) ```json { "kind": "Struct", "identifier": { "name": "ConnectionOptions", "templateArguments": [] }, "_namespaces": ["network", "internal"], "memberVariables": [ /* see below */ ], "attributes": [ { "ns": "codegen", "name": "MarkdownDocs", "arguments": [] } ] } ``` | Field | Type | Description | | ------------------------------ | --------- | ---------------------------------------------------------------------------- | | `kind` | string | `"Struct"`, `"Class"`, or `"Union"` | | `identifier.name` | string | Unqualified type name | | `identifier.templateArguments` | array | Template parameter / argument nodes (if templated) | | `_namespaces` | string\[] | Enclosing namespace stack, outermost first | | `memberVariables` | array | Member variable nodes (see below) | | `attributes` | array | C++ attributes on this declaration. Each entry has `ns`, `name`, `arguments` | ### Building the qualified name [Section titled “Building the qualified name”](#building-the-qualified-name) ```lua local ns = table.concat(node._namespaces, "::") local qualified = ns ~= "" and (ns .. "::" .. node.identifier.name) or node.identifier.name -- result: "network::internal::ConnectionOptions" ``` ## Enum node [Section titled “Enum node”](#enum-node) `enum` declarations with a body have `kind = "EnumeratorSpecifier"` (the codex `NodeKind::EnumSpecifier` stringified): ```json { "kind": "EnumeratorSpecifier", "identifier": { "name": "Color" }, "_namespaces": [], "isScoped": true, "isForwardDeclaration": false, "enumerators": [ { "name": "Red", "value": null }, { "name": "Green", "value": null }, { "name": "Blue", "value": "2" } ] } ``` | Field | Type | Description | | ---------------------- | -------------- | ---------------------------------------------------- | | `isScoped` | bool | `true` for `enum class`, `false` for unscoped `enum` | | `isForwardDeclaration` | bool | `true` for declarations without a body | | `enumerators` | array | Enumerator nodes | | `enumerators[].name` | string | Enumerator identifier | | `enumerators[].value` | string \| null | Explicit value as source text, or `null` | Forward declarations have `kind = "Enumerator"` (codex `NodeKind::Enum`). Anchor-triggered handlers also receive the appropriate kind via the same payload. ## Member variable node [Section titled “Member variable node”](#member-variable-node) ```json { "kind": "Variable", "identifier": { "name": "port" }, "typeSignature": { "identifier": { "name": "uint16_t", "templateArguments": [] }, "isConst": false, "isPointer": false, "isReference": false } } ``` For groups of variables sharing a type (`int x, y, z;`), the kind is `"VariableGroup"` with a `variables` array. Always handle both: ```lua for _, var in ipairs(node.memberVariables or {}) do if var.kind == "Variable" then process(var) elseif var.kind == "VariableGroup" then for _, v in ipairs(var.variables or {}) do process(v) end end end ``` ## Type signatures [Section titled “Type signatures”](#type-signatures) ```lua local function typeName(tSig) if not tSig or not tSig.identifier then return "?" end local name = tSig.identifier.name local args = tSig.identifier.templateArguments or {} -- e.g. name = "vector", args = [{ identifier = { name = "string" }, ... }] return name end ``` Template arguments are themselves type-signature-shaped — navigate them recursively. ## Inspecting a header before writing a rule [Section titled “Inspecting a header before writing a rule”](#inspecting-a-header-before-writing-a-rule) Use the `ast-dump` tool to dump any header in the codex JSON form: ```sh ast-dump -m codex -i include/color.hpp ``` The shape printed is the same the handler decodes, minus the `_namespaces`, `_registryId`, and `params` synthetic keys, and minus the heavy fields the engine strips. Key Takeaways * Structs/classes/unions carry `_namespaces` (outermost-first), `memberVariables`, and `attributes` (note: `attributes`, the C++ term, not `annotations`). * Enum declarations with a body have `kind = "EnumeratorSpecifier"`; forward declarations have `kind = "Enumerator"`. * Member variables have a `typeSignature`; templates recurse via `identifier.templateArguments`. * Heavy fields are stripped before the handler sees the JSON; recover them with the native introspection API. * Use `ast-dump -m codex -i ` to preview the exact JSON your handler will receive. # Grouping & Fan-in > How grouping.luau scripts control output file routing, the feature that enables non-1:1 generation. The `grouping.luau` script is the most powerful part of the codegen rule system. It controls the mapping from matched C++ entities to output files. Understanding it is the key to unlocking codegen’s full capability. ## The default: 1:1 routing [Section titled “The default: 1:1 routing”](#the-default-11-routing) Without a grouping script, each entity produces output written to a file derived from its input header’s path. One header → one output file. For simple cases (enum-to-string functions, for example) this is the right behaviour: the `ToString` rule produces `color.h` → `color.g.cpp` next to the input. ## The problem with 1:1 [Section titled “The problem with 1:1”](#the-problem-with-11) Consider generating API documentation. You have 40 structs spread across 15 headers. The 1:1 policy produces 15 separate Markdown files. But you want one `api-reference.md`. Or a serialization registry: one `registry.cpp` calling `register()` for every annotated type. Or a TypeScript bindings manifest: one `bindings.ts`, one `export interface` per C++ struct, sorted alphabetically. These are **fan-in** patterns. Many entities, one output file. The inverse — **fan-out**, one entity into multiple files — is also possible by combining grouping with multi-anchor `inline` patches. ## How grouping.luau fits the lifecycle [Section titled “How grouping.luau fits the lifecycle”](#how-groupingluau-fits-the-lifecycle) Grouping runs **before** handlers, as the second phase of entity routing. It takes all collected candidate entities and returns a map overriding their default output paths: .codegen/rules/MyRule/MyRule.grouping.luau ```lua return function(input) local data = json.decode(input) local result = {} for _, entity in ipairs(data.entities) do result[tostring(entity.registryId)] = "generated/docs/api-reference.md" end return json.encode(result) end ``` Each entity exposes only: `registryId`, `qualifiedName`, `structName`, `namespaces`, `inputFile`, `defaultPath` (the 1:1 output path derived from the input header), plus the rule’s `params` at the top level. There is **no** full AST node or attribute list at this stage — if you need that for routing, do the work in the handler and stuff the result into the `includes` or use the `params` channel. The engine then: 1. Looks up each entity’s new path in the returned map. 2. Resolves it against the project root (CWD); rejects escapes with `E005`. 3. Groups entities by output path for later assembly. 4. Calls the handler with the post-grouping output path (available as `_outputPath` in the handler). 5. Runs the preamble once per unique (rule, outputFile) pair, passing the entities bound to that file. 6. Concatenates each entity’s `source` into the assembled file. ## Fan-in: many structs, one file [Section titled “Fan-in: many structs, one file”](#fan-in-many-structs-one-file) The `MarkdownDocs` example demonstrates this pattern. Every struct annotated `[[codegen::MarkdownDocs]]`, regardless of which header it lives in, is routed to a single `api-reference.md`: grouping.luau ```lua return function(input) local data = json.decode(input) if not data then error("failed to decode input") end local OUTPUT = "generated/docs/api-reference.md" local result = {} for _, ent in ipairs(data.entities) do result[tostring(ent.registryId)] = OUTPUT end return json.encode(result) end ``` The preamble runs once for the rule; its result lands at the top of `api-reference.md` exactly once thanks to per-output-path content deduplication. Each entity’s handler-rendered `source` follows in match order. ## Fan-out: one entity, multiple files [Section titled “Fan-out: one entity, multiple files”](#fan-out-one-entity-multiple-files) Today’s grouping API maps each entity to **one** primary path. To split content across multiple files for a single entity, the cleanest pattern is: * Use `source` for the primary file (e.g. the implementation `.cpp`). * Use `inline` to inject declarations back into the original header at an anchor. Routing the same entity to two distinct generated files isn’t directly expressible in one `grouping.luau` invocation; the typical workaround is to define two rules and annotate the entity with both attributes. ## Conditional routing [Section titled “Conditional routing”](#conditional-routing) Grouping only sees summary metadata. Routing decisions you can express directly: ```lua for _, entity in ipairs(data.entities) do local isInternal = entity.inputFile:find("internal/") ~= nil local output = isInternal and "generated/internal-api.md" or "generated/public-api.md" result[tostring(entity.registryId)] = output end ``` Routing decisions that need full AST data (attribute arguments, base classes, member shape) need to happen in the **handler**, with the conclusion baked into a `params` value or the rendered `source`, then read out in grouping. ## Ordering guarantees [Section titled “Ordering guarantees”](#ordering-guarantees) Within an output file, entities appear in match order: the order their input headers were enumerated, then declaration order within each header. To impose a different order (alphabetical, for example), sort the entity list inside grouping before assigning paths — assembly visits entities in the post-grouping order. Key Takeaways * Without `grouping.luau`, routing is 1:1 (one entity → one file derived from the input path). * Grouping runs **before** handlers, with summary metadata only (no full AST node, no handler output). * Output is a ` → path` map; missing keys skip the entity (`E004`). * Paths must stay under the project root (CWD); escapes are rejected with `E005`. * The preamble dedup happens at assembly time per output path, so fan-in produces a single header. # How It Works > A top-down walkthrough of the codegen pipeline, from annotated C++ source to generated output files. ## The pipeline [Section titled “The pipeline”](#the-pipeline) ``` graph TD Start([Annotated C++ headers]) --> RL RL[Rule Loader] -- "Load config + scripts + shared/" --> PR PR[Preamble Run] -- "Run each rule's preamble once, store result" --> AS AS[Anchor Scan] -- "Index '// [[codegen::generated::...]]' in inputs" --> P P[Codex Pipeline] -- "FilesCollector → Preprocess → Parse → Analyze" --> EM EM[Entity Match] -- "Walk AST, match attribute by node_kinds + collect anchor entities" --> H H[Handler] -- "Run RuleName.luau per matched entity → source / inline / includes" --> G G[Grouping] -- "If RuleName.grouping.luau: rewrite each entity's output path" --> CD CD[Collision Check] -- "Non-cpp paths claimed by 2+ rules → drop" --> A A[Assemble & Write] -- "banner + preamble (deduped) + self-include + project includes + sources" --> IE IE[Inline Edits] -- "Replace anchor blocks in source headers" --> End([Output Files]) style Start fill:#f9f,stroke:#333,stroke-width:2px style End fill:#f9f,stroke:#333,stroke-width:2px ``` ## Key design decisions [Section titled “Key design decisions”](#key-design-decisions) **The AST is serialised to JSON before scripts see it.** Handlers receive a JSON-encoded entity payload (the codex AST node, with heavy fields stripped, plus synthetic `_namespaces`, `_registryId`, `_outputPath`, plus a `params` key copied from the rule config). This means: * Scripts are pure functions: JSON in, JSON out. * Scripts can be tested with a static fixture, no compiler required. * The full docs are also published as a single plain-text bundle at [`/llms-full.txt`](/llms-full.txt) for LLM agents and offline reference. **Each rule gets its own LuaU execution context.** Each call (preamble, handler, grouping) executes in a fresh execution; nothing carries between invocations. Rules cannot reach each other, the filesystem, or the network unless explicitly granted via `permissions:`. **Grouping runs before handlers.** Grouping rewrites each candidate’s output path using summary metadata (`registryId`, `qualifiedName`, `namespaces`, `inputFile`, `defaultPath`) — the handler hasn’t run yet, so no `source` text is available. The handler then runs with the post-grouping route exposed as `_outputPath`, so it can specialise on the file it’s about to land in. **Preamble runs once per `(rule, outputFile)` pair.** It receives the entities bound to that file (with their handler-emitted `includes`) so the preamble can specialise on what’s actually needed. The result is deduplicated by content when the engine assembles each output file: if two rules contribute identical preambles to the same path, the text appears once. **Inline injection rewrites anchor blocks in place.** Bare anchors written by hand are expanded on first run into one paired `:begin]]/:end]]` block per `inline` item (N items ⇒ N consecutive blocks). Later runs grow or shrink the run in place to match the current item count. ## What the engine does not do [Section titled “What the engine does not do”](#what-the-engine-does-not-do) * It does not instantiate templates. It sees the template declaration as the codex parser produced it. * It does not evaluate `constexpr`. Values are represented as their source text. * It does not perform overload resolution. The analyzer does best-effort symbol indexing but no semantic typing. * It does not cache between runs. Every invocation re-parses every input under `--input` (incremental persistence is not implemented today). Key Takeaways * Pipeline: load → anchor-scan → parse + analyze → collect candidates → **grouping → handler → preamble** → assemble → inline-edits. * Handlers are pure functions over a JSON AST node. No side effects, no shared state. Receive `_outputPath` (post-grouping route). * Grouping is path-only and runs **before** handlers; it sees summary metadata + `defaultPath`, not handler output. * Preamble runs once per `(rule, outputFile)` pair with the bound entities; the engine dedupes by content per output file at assembly time. # Preamble System > How preamble.luau scripts emit file headers, includes, license banners, and namespace wrappers — and how they're deduplicated when multiple rules share an output file. The preamble script (`preamble.luau`) is required for every rule. It runs **once per (rule, outputFile) pair** — that is, once per unique output file a rule contributes to. The engine deduplicates its result by content per output file at assembly time. It is the right place for: * `#include` directives common to all of the rule’s outputs to this file * License or auto-generated banners specific to this rule (the engine already emits its own banner separately) * Namespace opening brackets * Any text that must appear at the top of the file regardless of which entities land there ## Signature [Section titled “Signature”](#signature) ```lua return function(input) -- input: JSON-encoded context object, decode if you need it return "text to prepend to the file" end ``` The input context is: ```json { "language": "cpp", "outputFile": "path/to/output.cpp", "ruleName": "", "params": { /* rule.params from RuleName.config.yaml */ }, "entities": [ { "registryId": 42, "qualifiedName": "myns::MyStruct", "structName": "MyStruct", "namespaces": ["myns"], "inputFile": "include/myns/my_struct.hpp", "includes": [""] } ] } ``` `outputFile` is the post-grouping output path for this (rule, file) pair. The `entities` array contains every entity bound to this output file, with each entity’s handler-emitted `includes` exposed for conditional preamble logic. This lets you, for example, include `` in the preamble only if any entity in the output file needed a vector. The return value is a raw string — not JSON. Whatever you return is buffered on the rule. ## Example: C++ includes [Section titled “Example: C++ includes”](#example-c-includes) MyRule.preamble.luau ```lua return function(input) return "#include \n#include \n\n" end ``` ## Example: Markdown document header [Section titled “Example: Markdown document header”](#example-markdown-document-header) preamble.luau ```lua return function(input) return "# API Reference\n\n" .. "> This file is auto-generated. Do not edit manually.\n\n" .. "---\n\n" end ``` ## Deduplication at assembly [Section titled “Deduplication at assembly”](#deduplication-at-assembly) The engine collects all preamble results from the rules contributing entities to a single output file, deduplicates by **content** (string equality), and emits each unique result once. Two rules that produce identical preamble strings collapse to a single emission; two rules that produce different strings each contribute one block. This is what makes fan-in clean: dozens of entities from one rule routed into `api-reference.md` produce exactly one `# API Reference` header. It also means: preambles don’t have visibility into which entities will land in their output file. They run before grouping. If you find yourself wanting to conditionalise preamble content on entity data (“only include this header if the entity has template parameters”), that’s a signal to emit the include from the handler’s `includes` field instead — the engine will deduplicate those across the file too. ## Preamble vs handler [Section titled “Preamble vs handler”](#preamble-vs-handler) | Belongs in the preamble | Belongs in the handler | | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | Banner / file-level comment | Per-entity content | | Always-on `#include` directives, or conditional on the `entities` list | `#include` directives specific to a single entity (use `includes` in the return) | | Opening namespace bracket | The actual code/declaration text (`source`, `inline`) | ## Errors [Section titled “Errors”](#errors) A preamble that calls `error()` (or hits a runtime error) aborts the **whole** run with `E007` (exit 1). Unlike handler errors (which skip a single entity), there’s no recovery — if a preamble fails for one output file, the entire codegen run is unsafe. Key Takeaways * The preamble is required (`E003` if missing); use `return function() return "" end` if there’s nothing to emit. * Runs once per (rule, outputFile) pair; results are deduplicated by content per output file at assembly time. * The input context is `{language, outputFile, ruleName, params, entities}`; entities lists all matches bound to this output file. * Use `entities` to conditionally emit includes (e.g., include `` only if any entity has a vector member). * Use `includes` from the handler for entity-conditional includes; the engine deduplicates those too. * Preamble errors abort the whole run. # Rule Lifecycle > The sequence of script invocations for a single rule run, in the order the engine actually executes them. ## Phase 1: Rule load [Section titled “Phase 1: Rule load”](#phase-1-rule-load) The engine reads `//` for each `-a ` on the CLI: handler, preamble, optional grouping, optional `.env`, and `config.yaml` (with `extends:` chasing as needed). It also loads `/../shared/*.luau` (alphabetical by filename, concatenated) as the shared prelude and `/../shared/.env` as the baseline dotenv. CLI overrides (`-P`, `--env`, `--allow-http`, `--allow-env`) are folded in. ## Phase 2: Anchor scan + AST build [Section titled “Phase 2: Anchor scan + AST build”](#phase-2-anchor-scan--ast-build) The engine scans every input file (under `-i`) for `// [[codegen::generated::::]]` markers (bare or `:begin]]`/`:end]]` form) and indexes them by `(rule, qualifiedName)`. In parallel it runs the codex pipeline (collect → preprocess → parse → analyze) to produce `SourceNode` trees and an analysis result. ## Phase 3: Entity match [Section titled “Phase 3: Entity match”](#phase-3-entity-match) The engine walks each parsed source. For every node whose kind is in the rule’s `node_kinds` (default: `Struct`), it checks for matching `[[codegen::]]` attributes and queues a match. It then merges in entities referenced only by anchor (no attribute) so they can also fire the handler. ## Phase 4: Collect candidates [Section titled “Phase 4: Collect candidates”](#phase-4-collect-candidates) For each rule, the engine collects all matching entities (from Phase 3). The entities have default output paths derived from their input headers (1:1 routing). ## Phase 5: Grouping [Section titled “Phase 5: Grouping”](#phase-5-grouping) For each rule that ships a `grouping.luau`, the engine calls it **once per invocation, with all candidate entities**: ```plaintext input: JSON { params: , entities: [{ registryId, qualifiedName, structName, namespaces, inputFile, defaultPath }, ...] } output: JSON { "": "path/to/output.cpp", ... } ``` For each returned key, the engine resolves the path against the project root (CWD) and updates that entity’s output path. Paths escaping the project root emit `E005` and skip the entity. Entities missing from the map emit `E004` and skip. A grouping error emits `E008` and the rule’s entities keep their default 1:1 paths. ## Phase 6: Handler [Section titled “Phase 6: Handler”](#phase-6-handler) For each matched entity (with updated output path if grouping fired), the engine calls `transform.luau` once: ```plaintext input: JSON the entity payload (codex AST node with heavy fields stripped, plus _namespaces and _registryId and _outputPath, plus params) output: JSON { source = "...", inline = [{source = "..."}, ...], includes = ["..."] } ``` The `inline` and `includes` keys are optional. A handler `error()` skips this entity and emits `E006`; the run continues. A handler internal/VM error is fatal (exit 2). ## Phase 7: Preamble [Section titled “Phase 7: Preamble”](#phase-7-preamble) For each rule, the engine runs `preamble.luau` **once per unique output file** (the pair of rule + outputPath): ```plaintext input: JSON { language: "cpp", outputFile: "path/to/output.cpp", ruleName: "", params: , entities: [{registryId, qualifiedName, structName, namespaces, inputFile, includes}, ...] } output: string (raw text, stored for assembly) ``` Each entity in the `entities` array is a matching entity bound to this output file, with its handler-emitted `includes` available for conditional preamble logic. The result is held until output assembly. A preamble error aborts the whole run with `E007`. ## Phase 9: Collision check (non-cpp only) [Section titled “Phase 9: Collision check (non-cpp only)”](#phase-9-collision-check-non-cpp-only) The engine groups entities by output path. For non-cpp languages, a path claimed by two or more rules is rejected — the conflicting entities are dropped and the engine prints `error: output path '

' claimed by rules ...`. C++ outputs are exempt by design: stitching multiple rules’ contributions into one `.g.cpp` is intended. ## Phase 10: Assemble & write [Section titled “Phase 10: Assemble & write”](#phase-10-assemble--write) For each output path: 1. Banner: `// Generated with Codegen - DO NOT EDIT!!!` 2. For C++ outputs whose extension is a header (`.hpp` / `.h` / `.hxx` / `.hh`): `#pragma once`. 3. Each contributing rule’s preamble result (one per rule), deduplicated by content. 4. For C++ outputs: a self-include of the input header (rebased when the output is relocated), followed by the union of `includes` from all contributing entities. When `autoDeduceIncludes: true`, project includes already covered by the input header’s transitive include graph are filtered out. 5. Each entity’s `source`, in match order. Files are written via a temp-file-and-rename pattern. Under `--dry-run`, contents are collected and emitted as a unified diff (zero context) to stdout — works on every tier, suitable for CI and LLM pipelines. With `--dry-run --show-tui`, the FTXUI viewer opens instead (Professional+; the gate emits `E102` on Community). ## Phase 11: Inline edits [Section titled “Phase 11: Inline edits”](#phase-11-inline-edits) For each entity that returned `inline` items, the engine rewrites the source header at the matching anchor (bare or paired form). Bare anchors are expanded to the paired form on first run. See [Inline Injection](/rules/inline-injection/). Under `--dry-run`, edits are collected (not applied) and shown alongside the assembly previews. ## Error handling summary [Section titled “Error handling summary”](#error-handling-summary) | Failure | Effect | | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | Bad CLI / missing input / rule load failure | Exit 1 (with the matching `E0xx`). | | Grouping error | Skip grouping for that rule, fall back to 1:1 paths, emit `E008`. | | Handler `error()` | Skip the entity, emit `E006`, continue. | | Handler internal/VM error | Exit 2 (`E006`, fatal). | | Preamble error | Exit 1 (`E007`). The whole run aborts. | | Inline anchor missing | Plain stderr warning, continue. | | License `E102` (`--show-tui` on Community) | Exit 0 with the count of would-be files; falls back to a filename list. The default `--dry-run` plain output is unaffected. | Key Takeaways * Order: rule load → anchor scan + parse → match → **collect candidates → grouping → handler → preamble** (per output file) → assemble → inline edits. * Candidates are collected before grouping; grouping overwrites default output paths. Handler fires with post-grouping paths and emits `_outputPath`. * Preamble runs once per (rule, outputFile) pair and sees entities bound to that file. Handler runs once per matched entity. * Grouping errors fall back to default routing. Handler errors skip the entity. Preamble errors abort the whole run. * C++ outputs from multiple rules merge into one file; non-cpp collisions are rejected. # Markdown Docs: N structs ⇒ 1 file > The MarkdownDocs example demonstrates fan-in grouping, consolidating annotated structs from any number of headers into a single API reference document. The `MarkdownDocs` example is the clearest demonstration of codegen’s grouping capability. It consolidates every annotated C++ struct, regardless of which header it lives in, into a single `api-reference.md`. No post-processing. No shell script. The engine handles the fan-in. ## What it produces [Section titled “What it produces”](#what-it-produces) Given a codebase with annotated structs across multiple headers, the rule produces one file: generated/docs/api-reference.md ```markdown // Generated with Codegen - DO NOT EDIT!!! # API Reference > This file is auto-generated. Do not edit manually. --- ## `network::ConnectionOptions` | Field | Type | |---|---| | `host` | `string` | | `port` | `uint16_t` | | `timeout_ms` | `uint32_t` | --- ## `storage::CacheConfig` | Field | Type | |---|---| | `maxEntries` | `size_t` | | `evictionPolicy` | `EvictionPolicy` | --- ``` The `// Generated with Codegen - DO NOT EDIT!!!` banner is emitted by the engine. The `# API Reference` header and the auto-generated notice come from the rule’s preamble, which runs once per `(rule, outputFile)` pair and is deduplicated by content at output assembly. Each struct contributes one section, in match order. ## Annotating your structs [Section titled “Annotating your structs”](#annotating-your-structs) include/network/connection.hpp ```cpp struct [[codegen::MarkdownDocs]] ConnectionOptions { std::string host; uint16_t port; uint32_t timeout_ms; }; ``` include/storage/cache.hpp ```cpp struct [[codegen::MarkdownDocs]] CacheConfig { size_t maxEntries; EvictionPolicy evictionPolicy; }; ``` Both headers, different directories. The engine finds both; the grouping script routes both to the same output file. ## The rule files [Section titled “The rule files”](#the-rule-files) ### `config.yaml` [Section titled “config.yaml”](#configyaml) config.yaml ```yaml version: 1 output: language: markdown # No outputDirectory or outputNameTemplate - grouping.luau controls all paths. ``` Because grouping handles all routing, the config does not set `outputDirectory` or `outputNameTemplate`. ### `grouping.luau` [Section titled “grouping.luau”](#groupingluau) grouping.luau ```lua -- Routes ALL matched structs into a single consolidated output file. -- Input file topology is irrelevant; structs from any header end up here. return function(input) local data = json.decode(input) if not data then error("failed to decode input") end local OUTPUT = "generated/docs/api-reference.md" local result = {} for _, ent in ipairs(data.entities) do result[tostring(ent.registryId)] = OUTPUT end return json.encode(result) end ``` Every entity, one output path. The path is resolved against the project root (CWD) — escapes are rejected with `E005`. Each entity passed to grouping has only the summary fields `registryId`, `qualifiedName`, `structName`, `namespaces`, `inputFile`, `defaultPath`. ### `preamble.luau` [Section titled “preamble.luau”](#preambleluau) preamble.luau ```lua -- Emitted once per (rule, outputFile) pair. Deduplicated by content at output assembly. return function(input) return "# API Reference\n\n" .. "> This file is auto-generated. Do not edit manually.\n\n" .. "---\n\n" end ``` ### `transform.luau` [Section titled “transform.luau”](#transformluau) transform.luau ```lua -- Generates a Markdown section for each annotated struct. -- grouping.luau consolidates all sections into a single api-reference.md. return function(input) local node = json.decode(input) if not node then error("failed to decode input") end local structName = node.identifier and node.identifier.name if not structName or structName == "" then error("struct has no name") end -- Build the fully-qualified name using namespace context local namespaces = node._namespaces or {} local nsPrefix = table.concat(namespaces, "::") local qualifiedName = nsPrefix ~= "" and (nsPrefix .. "::" .. structName) or structName -- Build field table rows local rows = {} local function typeName(tSig) if not tSig or not tSig.identifier then return "?" end return tSig.identifier.name or "?" end local function processVar(varNode) local fname = varNode.identifier and varNode.identifier.name or varNode.name if not fname then return end local t = typeName(varNode.typeSignature) fname = fname:gsub("|", "\\|") t = t:gsub("|", "\\|") table.insert(rows, "| `" .. fname .. "` | `" .. t .. "` |") end for _, varNode in ipairs(node.memberVariables or {}) do if varNode.kind == "Variable" then processVar(varNode) elseif varNode.kind == "VariableGroup" then for _, v in ipairs(varNode.variables or {}) do processVar(v) end end end -- Assemble the section local lines = { "## `" .. qualifiedName .. "`", "" } if #rows > 0 then table.insert(lines, "| Field | Type |") table.insert(lines, "|---|---|") for _, row in ipairs(rows) do table.insert(lines, row) end else table.insert(lines, "_No public member variables._") end table.insert(lines, "") table.insert(lines, "---") return json.encode({ source = table.concat(lines, "\n") }) end ``` ## Running the rule [Section titled “Running the rule”](#running-the-rule) ```sh codegen -i ./include -r .codegen/rules -a MarkdownDocs ``` 1. The engine walks `./include`, finds all headers. 2. The codex pipeline parses + analyzes each header. 3. Each struct annotated `[[codegen::MarkdownDocs]]` matches; the handler runs and emits a `## ...` section. 4. `grouping.luau` rewrites every entity’s output path to `generated/docs/api-reference.md`. 5. The engine assembles the file: banner + preamble (deduplicated) + each entity’s section in match order. 6. The file is written via temp-and-rename. ## Extending this example [Section titled “Extending this example”](#extending-this-example) **Add descriptions.** Accept a string attribute argument: `[[codegen::MarkdownDocs("Optional per-entity description")]]`. The handler reads it via `node.attributes[i]` (look for `attr.ns == "codegen"` and `attr.name == "MarkdownDocs"`, then `attr.arguments[1]`). **Sort alphabetically.** Inside `grouping.luau`, sort `data.entities` by `entity.qualifiedName` before assigning paths. Output assembly visits entities in the post-grouping order. **Multi-section output.** Route internal entities to `internal-api.md` and public entities to `public-api.md`: ```lua local isInternal = ent.inputFile:find("internal/") ~= nil result[tostring(ent.registryId)] = isInternal and "generated/internal-api.md" or "generated/public-api.md" ``` Key Takeaways * This example demonstrates **fan-in**: N annotated structs from any number of headers ⇒ 1 output file. * The `grouping.luau` script is the only change needed to switch from 1:1 to fan-in routing. * The preamble runs once per (rule, outputFile) pair; the engine deduplicates the result by content per output file. * Grouping sees only summary fields (`registryId`, `qualifiedName`, `structName`, `namespaces`, `inputFile`, `defaultPath`); grouping runs before handlers, so handler output is unavailable. For AST-level routing decisions, look entities up via `get_node()` in the handler. * Paths in grouping must stay under the project root (CWD). # ToString: enum ⇒ switch > The built-in ToString rule generates a std::string_view toString() switch statement for every annotated C++ enum. The `ToString` example is the canonical demonstration of anchor-based triggering and inline injection. It generates a `std::string_view toString(EnumType e)` function for any enum you annotate, placing the implementation in a `.g.cpp` next to the input header and injecting the declaration back into the header. ## What it produces [Section titled “What it produces”](#what-it-produces) Given (write-by-hand form, before the first run): include/color.hpp (before generation) ```cpp #pragma once namespace app { enum class Color { Red, Green, Blue }; // [[codegen::generated::ToString::app::Color]] } // namespace app ``` The third anchor segment is the **qualified name** of the target entity — namespace path included. Above, `app::Color` because the enum lives in `namespace app`. There is no `qualified` modifier; the path is just the qualified name. After running: ```sh codegen -i include/color.hpp -r .codegen/rules -a ToString ``` The header becomes: include/color.hpp (after generation) ```cpp #pragma once namespace app { enum class Color { Red, Green, Blue }; // [[codegen::generated::ToString::app::Color:begin]] std::string_view toString(app::Color e); // [[codegen::generated::ToString::app::Color:end]] } // namespace app ``` The bare anchor was expanded into the paired `:begin]] / :end]]` form. Subsequent runs replace only the content between the markers. A sibling `.g.cpp` is produced next to the input header: include/color.g.cpp ```cpp // Generated with Codegen - DO NOT EDIT!!! #include #include "color.hpp" std::string_view toString(app::Color e) { switch (e) { case app::Color::Red: return "Red"; case app::Color::Green: return "Green"; case app::Color::Blue: return "Blue"; default: return ""; } } ``` The `// Generated with Codegen - DO NOT EDIT!!!` banner, the `#include "color.hpp"` self-include, and any project includes are emitted by the engine; the rule’s preamble (`#include `) is added on top. ## The rule scripts [Section titled “The rule scripts”](#the-rule-scripts) The shipping example lives at `clis/codegen/examples/ToString/`. Key behaviours: * **Preamble** emits `#include `. * **Handler** decodes the entity, reads `node.identifier.name`, `node._namespaces`, `node.isScoped`, and `node.enumerators[]`. It builds one `case` per enumerator, wraps it in a switch, and returns `{ source = impl, inline = { { source = decl } } }`. * **`isScoped`** controls whether case values use the `EnumName::` prefix. Scoped enums (`enum class`) need it; unscoped enums don’t. * **No grouping script.** Each enum’s output goes to a `.g.cpp` next to its input header. The example’s enum is anchor-triggered (no `[[codegen::ToString]]` attribute on the enum itself), because C++ does not allow attributes on enum declarations the same way as on structs. The anchor doubles as the inline-injection site for the declaration. **Note:** No grouping script. Grouping is optional and omitting it is safe; each entity routes to its default 1:1 path derived from the input header. ## Extending this rule [Section titled “Extending this rule”](#extending-this-rule) **Add a reverse `fromString`.** Return a second item in the `inline` list **at a separate anchor** (one anchor per `::`), or extend the handler so its `source` emits both `toString` and `fromString` functions. **Handle explicit values.** `node.enumerators[].value` carries explicit values as source-text strings (`null` when unset). Use it to detect sparse enums. **Namespace-bucketed output.** Add a grouping script that routes by `entity.namespaces[1]` to e.g. `generated//strings.g.cpp`. Key Takeaways * Anchor comments (`// [[codegen::generated::Rule::Qualified]]`) trigger the rule and mark the inline-injection site. * The third anchor segment is the qualified name; `app::Color`, `Outer::Inner::E`, etc. * `node.isScoped` distinguishes `enum class` from plain `enum` for the case-value prefix. * No grouping script ⇒ output goes next to the input header (`color.hpp` → `color.g.cpp`). * The first run rewrites bare anchors into paired `:begin]] / :end]]` blocks; later runs are idempotent. # TypeScript Types: struct ⇒ interface > The built-in TypeScriptTypes rule generates TypeScript interface declarations from annotated C++ structs, with recursive type mapping for generics. The `TypeScriptTypes` rule demonstrates attribute-based triggering and recursive type mapping. It converts C++ structs to TypeScript `export interface` declarations, handling primitives, `std::vector`, `std::optional`, `std::shared_ptr`, and `std::map` recursively. ## What it produces [Section titled “What it produces”](#what-it-produces) include/api\_types.hpp ```cpp struct [[codegen::TypeScriptTypes]] UserProfile { std::string username; uint32_t age; std::vector roles; std::optional bio; }; ``` By default the output lands at `api_types.TypeScriptTypes.ts` next to the input header (the non-cpp default filename pattern is `.`). To centralise the output, set `outputDirectory` and/or `outputNameTemplate` in the rule config. api\_types.TypeScriptTypes.ts ```ts // Generated with Codegen - DO NOT EDIT!!! // Auto-generated TypeScript types for rule: TypeScriptTypes // Source: export interface UserProfile { username: string; age: number; roles: Array; bio: string | undefined; } ``` The `// Sources:` lines in the example’s preamble iterate `ctx.entities` to list the contributing input files. Each entity in `entities` has its `inputFile` available, so the preamble can list the sources that contributed to this output file. Run: ```sh codegen -i include/api_types.hpp -r .codegen/rules -a TypeScriptTypes ``` ## Type mapping [Section titled “Type mapping”](#type-mapping) | C++ type | TypeScript type | | ---------------------------------------------------------------------------- | ------------------------------------------------------ | | `bool` | `boolean` | | `int`, `int8_t`…`int64_t`, `uint8_t`…`uint64_t`, `float`, `double`, `size_t` | `number` | | `std::string`, `string`, `path` | `string` | | `std::vector` | `Array` | | `std::optional` | `T \| undefined` | | `std::shared_ptr` | `T \| null` | | `std::map`, `std::unordered_map` | `Record` | | Anything else | Used as-is (assumed to be a TypeScript type reference) | The mapping is implemented as a recursive `cppToTs(tSig)` function in `transform.luau`. Adding new mappings means editing only that function. ## Extending the type map [Section titled “Extending the type map”](#extending-the-type-map) Open `transform.luau` and add entries to the `primitives` table or new `if name == "..."` branches for generics: ```lua -- Add std::set -> Set if name == "set" then local args = tSig.identifier.templateArguments or {} if args[1] then return "Set<" .. cppToTs(args[1]) .. ">" end return "Set" end ``` ## Fan-in variant [Section titled “Fan-in variant”](#fan-in-variant) To consolidate every TypeScript interface into a single `types.ts`, add a grouping script and (optionally) override the output path: grouping.luau ```lua return function(input) local data = json.decode(input) local result = {} for _, ent in ipairs(data.entities) do result[tostring(ent.registryId)] = "generated/types.ts" end return json.encode(result) end ``` The output path is resolved against the project root (the working directory). Key Takeaways * Attribute annotation (`[[codegen::TypeScriptTypes]]`) triggers struct-level rules; no anchor needed. * Default non-cpp output: `.` next to the input. Override with `outputDirectory` / `outputNameTemplate`. * Type mapping is a recursive LuaU function; extend it by adding branches. * Adding `grouping.luau` switches from 1:1 to fan-in routing. * Preamble runs once per (rule, outputFile) pair and receives the `entities` bound to that file, so it can conditionally emit content based on what entities land there. # Your First Rule > Write a rule from scratch — a struct-to-JSON-serializer generator. This guide writes a new rule, `JsonSerializer`, that generates a `toJson()` function for each annotated struct. It covers the three script files every rule needs and the config that ties them together. ## Goal [Section titled “Goal”](#goal) Given: include/config.hpp ```cpp struct [[codegen::JsonSerializer]] AppConfig { std::string host; uint16_t port; bool enableTls; }; ``` Produce, alongside the header, `include/config.g.cpp`: include/config.g.cpp ```cpp // Generated with Codegen - DO NOT EDIT!!! #include #include "config.hpp" nlohmann::json toJson(const AppConfig& v) { return { {"host", v.host}, {"port", v.port}, {"enableTls", v.enableTls}, }; } ``` The `// Generated with Codegen - DO NOT EDIT!!!` banner and the `#include "config.hpp"` self-include are emitted by the engine. The rule supplies the `` include (via the preamble) and the function body. ## Rule file layout [Section titled “Rule file layout”](#rule-file-layout) No `grouping.luau` — this rule uses 1:1 routing (one struct ⇒ one `.g.cpp` next to the input). ## Step 1: config [Section titled “Step 1: config”](#step-1-config) config.yaml ```yaml version: 1 output: language: cpp autoDeduceIncludes: false ``` `autoDeduceIncludes: false` means the engine will emit every `includes` entry the rule returns verbatim, without filtering against the input header’s transitive include graph. We rely on the preamble for the JSON include, so there’s nothing to filter anyway. ## Step 2: preamble [Section titled “Step 2: preamble”](#step-2-preamble) preamble.luau ```lua return function(input) return "#include \n\n" end ``` The preamble runs **once per (rule, outputFile) pair**. Its result is deduplicated by content per output file at assembly time. With 1:1 routing, each output file ends up with one preamble emission of `#include `. ## Step 3: handler [Section titled “Step 3: handler”](#step-3-handler) transform.luau ```lua return function(input) local node = json.decode(input) if not node then error("failed to decode input") end local structName = node.identifier and node.identifier.name if not structName then error("struct has no name") end -- Collect field initializer-list entries local entries = {} for _, varNode in ipairs(node.memberVariables or {}) do if varNode.kind == "Variable" then local fname = varNode.identifier and varNode.identifier.name if fname then table.insert(entries, ' {"' .. fname .. '", v.' .. fname .. '},') end elseif varNode.kind == "VariableGroup" then for _, v in ipairs(varNode.variables or {}) do local fname = v.identifier and v.identifier.name if fname then table.insert(entries, ' {"' .. fname .. '", v.' .. fname .. '},') end end end end local body = table.concat(entries, "\n") local impl = "nlohmann::json toJson(const " .. structName .. "& v) {\n" .. " return {\n" .. body .. "\n" .. " };\n" .. "}" local decl = "nlohmann::json toJson(const " .. structName .. "& v);" return json.encode({ source = impl, inline = { { source = decl } } }) end ``` The `inline` list injects each item as its own `:begin]]/:end]]` block at an anchor site in the original header (if you’ve placed one). The qualified-name segment of the anchor is built from `node._namespaces` plus the struct name; an anchor like `// [[codegen::generated::JsonSerializer::AppConfig]]` will match this struct in the global namespace. With no anchor present in the header, the engine emits diagnostic `W003` and skips the inline injection — the `.g.cpp` is still written. The `VariableGroup` branch covers `int x, y, z;` declarations. ## Step 4: run [Section titled “Step 4: run”](#step-4-run) ```sh codegen -i ./include -r .codegen/rules -a JsonSerializer ``` The output lands at `include/config.g.cpp`. Key Takeaways * A rule needs three files: `config.yaml`, `preamble.luau` (required, can return `""`), and the handler `transform.luau`. * The handler receives one entity as JSON and returns `{ source, inline?, includes? }`. * `source` lands in the output file (default: next to the input header). `inline` items are injected at matching anchor comments. * No `grouping.luau` ⇒ 1:1 routing; one annotated entity ⇒ one output file derived from the input. * The LuaU sandbox provides `json`, the [native introspection API](/reference/luau-globals/), and a stdlib subset; opt-in to HTTP/env via `permissions:`. # Installation > Install the CodeXX DTDK Manager — the single entry point that installs, updates, licenses, and verifies codegen and every other DTDK component. ## How CodeXX DTDK is installed [Section titled “How CodeXX DTDK is installed”](#how-codexx-dtdk-is-installed) codegen ships as one component of the **CodeXX Dev-Tool Development Kit (DTDK)**. You don’t download `codegen` directly. You install the **DTDK Manager** once — a small terminal app — and use it to install, update, license-activate, and verify codegen and every other component. The manager keeps each component in a versioned, side-by-side layout under `~/.codexx` and exposes one binary per tool on your `PATH`. No `sudo`, no system package manager, no admin rights. ## Install [Section titled “Install”](#install) One command downloads the latest stable manager, verifies its checksum, extracts it under `~/.codexx`, and adds `~/.codexx/bin` to your shell profile. No GitHub account or token required. Linux Windows macOS ```sh curl -fsSL https://www.codexx-dtdk.com/install.sh | bash ``` Open a new shell afterwards so the `PATH` change takes effect. To also verify the release signature, append `-s -- --verify` (requires `cosign` on `PATH`). ```powershell iex ((New-Object System.Net.WebClient).DownloadString('https://www.codexx-dtdk.com/install.ps1')) ``` Installs under `%USERPROFILE%\.codexx` and prepends `%USERPROFILE%\.codexx\bin` to your user `PATH`. Open a new shell afterwards. macOS builds are temporarily paused while the release pipeline is validated on Apple hardware. Until macOS archives ship, build from source on macOS — see the project `README` — or run the Linux build in an x86-64 environment. The installer fetches the manager through a download proxy that holds a read-only token server-side — the releases live in a private repository, but you never need credentials. To pin a specific version or install a pre-release channel, download `install.sh` / `install.ps1` and pass `--tag` or `--channel`; those paths read the private repo directly and require a GitHub token. ## Inspect the installer before running it [Section titled “Inspect the installer before running it”](#inspect-the-installer-before-running-it) Don’t want to pipe a remote script straight into a shell? Download it, read it, then run it. Linux Windows ```sh curl -fSL https://www.codexx-dtdk.com/install.sh -o install.sh less install.sh # read it chmod +x install.sh ./install.sh --help # list every flag ./install.sh # default install (stable, ~/.codexx, PATH) ./install.sh --verify # additionally cosign-verify the archive ``` ```powershell Invoke-WebRequest https://www.codexx-dtdk.com/install.ps1 -OutFile install.ps1 Get-Content .\install.ps1 # read it Get-Help .\install.ps1 -Detailed # list every flag .\install.ps1 # default install .\install.ps1 -Verify # additionally cosign-verify the archive ``` This is the same artifact the proxy serves to `curl | bash` / `iex` — saving it to disk just lets you audit it first. ## Manual archive download [Section titled “Manual archive download”](#manual-archive-download) Prefer to inspect the archive before running anything? Download it directly and set things up yourself. Linux (x64) Windows (x64) [Download DTDK Manager — Linux x64](/api/download?platform=linux\&arch=x64) Extract it into your install root and add its `bin/` to your `PATH`: ```sh mkdir -p ~/.codexx tar -xzf codexx_dtdk_manager-*-linux-*.tar.gz -C ~/.codexx echo 'export PATH="$HOME/.codexx/bin:$PATH"' >> ~/.bashrc export PATH="$HOME/.codexx/bin:$PATH" ``` [Download DTDK Manager — Windows x64](/api/download?platform=windows\&arch=x64) Extract the archive into `%USERPROFILE%\.codexx` and add `%USERPROFILE%\.codexx\bin` to your user `PATH`: ```powershell $root = "$env:USERPROFILE\.codexx" Expand-Archive codexx_dtdk_manager-*-windows-*.zip -DestinationPath $root -Force $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') [Environment]::SetEnvironmentVariable('Path', "$root\bin;$userPath", 'User') ``` Each archive carries a `.sha256` checksum and a Sigstore signature bundle — fetch them with `&asset=sha256` / `&asset=sigstore` on the same URL. See [Supply Chain](/trust/supply-chain/) for how releases are attested and verified. ## Run the manager [Section titled “Run the manager”](#run-the-manager) Open a new shell so the `PATH` change takes effect, then launch: ```sh codexx_dtdk_manager ``` The manager is a terminal UI. Move with the arrow keys; press `Enter` on the **codegen** row to install and activate it. `q` quits. The manager handles the download, checksum verification, and side-by-side version management for you. ## Activate your license [Section titled “Activate your license”](#activate-your-license) The Community tier requires no activation. For Professional or Team, activation happens **inside the manager** — not through a `codegen` CLI command. Select the codegen row, and the manager shows an activation prompt in place of the version list. Paste your license key there; the manager validates it, stores a signed offline token, and unlocks the component. See [License Activation](/licensing/activation/) for the cached-token model and the air-gapped path. ## Verify the installation [Section titled “Verify the installation”](#verify-the-installation) ```sh codegen --help ``` Should print the codegen option list. If the command isn’t found, confirm `~/.codexx/bin` (or `%USERPROFILE%\.codexx\bin`) is on your `PATH` and that you opened a new shell. ## Uninstall [Section titled “Uninstall”](#uninstall) One command removes the install tree, the `PATH` entry, and — unless you ask otherwise — the per-user config (license tokens) and cache (shared active map). Linux Windows ```sh curl -fsSL https://www.codexx-dtdk.com/uninstall.sh | bash ``` The script prints what it will remove and asks for confirmation (reading your answer from the terminal even when piped), then deletes `~/.codexx`, strips the marker block from `~/.bashrc` / `~/.zshrc` / `~/.profile`, and removes `~/.config/codexx` and `~/.cache/codexx`. For unattended use append `| bash -s -- --yes` to skip the prompt. Add `--keep-config` to retain license tokens (so a reinstall picks them up) or `--keep-cache` to retain the shared active map. `--dry-run` previews without touching anything; `--root

` targets a non-default install location. ```powershell irm https://www.codexx-dtdk.com/uninstall.ps1 | iex ``` The script prints what it will remove, asks for confirmation, then deletes `%USERPROFILE%\.codexx`, removes `%USERPROFILE%\.codexx\bin` from your user `PATH`, and removes `%APPDATA%\CodeXX` and `%LOCALAPPDATA%\CodeXX\Cache`. For unattended use, download it and run with `-Yes` (`irm …/uninstall.ps1 -OutFile uninstall.ps1; .\uninstall.ps1 -Yes`). Add `-KeepConfig` to retain license tokens or `-KeepCache` to retain the shared active map. `-DryRun` previews without touching anything; `-Root ` targets a non-default install location. ### Manual uninstall [Section titled “Manual uninstall”](#manual-uninstall) If you’d rather remove things by hand: Linux Windows ```sh # 1. Install tree (binaries, side-by-side component versions, shim). rm -rf ~/.codexx # 2. License tokens (skip to keep your activation for a future reinstall). rm -rf ~/.config/codexx # 3. Shared active map + per-tool cache. rm -rf ~/.cache/codexx # 4. PATH entry. install.sh appended a marker block to your shell rc files — # open each rc that exists and delete the block bounded by: # # >>> codexx-dtdk install begin >>> # … # # <<< codexx-dtdk install end <<< sed -i '/# >>> codexx-dtdk install begin >>>/,/# <<< codexx-dtdk install end <</dev/null || true ``` ```powershell # 1. Install tree. Remove-Item -Recurse -Force "$env:USERPROFILE\.codexx" # 2. License tokens (skip to keep activation for a future reinstall). Remove-Item -Recurse -Force "$env:APPDATA\CodeXX" # 3. Shared active map + per-tool cache. Remove-Item -Recurse -Force "$env:LOCALAPPDATA\CodeXX\Cache" # 4. PATH entry — strip \bin from the User-scope Path. $bin = "$env:USERPROFILE\.codexx\bin" $parts = ([Environment]::GetEnvironmentVariable('Path','User') -split ';') | Where-Object { $_ -and $_ -ne $bin } [Environment]::SetEnvironmentVariable('Path', ($parts -join ';'), 'User') ``` Open a new shell afterwards so the `PATH` change takes effect. Key Takeaways * codegen is installed through the DTDK Manager, not downloaded directly. * One PAT-free command installs the manager — download, checksum, extract, and `PATH` are all automatic. * The manager installs, updates, verifies, and license-activates every component into `~/.codexx`. * License activation is done in the manager TUI; Community needs none. Professional and Team paste a key into the activation prompt. # Quick Start > Generate your first C++ enum-to-string function in under five minutes. This guide walks through the `ToString` example: annotating a C++ enum and generating a `std::string_view toString()` function from it. The shipping example is at `clis/codegen/examples/ToString/` in the source tree. 1. **Set up the rule directory** ```sh mkdir -p .codegen/rules/ToString # Copy config.yaml, transform.luau, and preamble.luau # from clis/codegen/examples/ToString/ into .codegen/rules/ToString/ ``` The four files in a rule directory are documented in [Rule Anatomy](/rules/anatomy/). 2. **Annotate your enum** Add an anchor comment near the enum. The third anchor segment is the entity’s qualified name (with namespaces): include/color.hpp ```cpp #pragma once namespace app { enum class Color { Red, Green, Blue }; // [[codegen::generated::ToString::app::Color]] } // namespace app ``` `ToString` uses anchor-based triggering because C++ doesn’t allow attributes on enum declarations. The anchor doubles as the inline-injection site for the generated declaration. 3. **Run the engine** ```sh codegen -i ./include -r .codegen/rules -a ToString ``` * `-i` is the input directory (or a single header). * `-r` is the rules root (defaults to `.codegen/rules`; you can omit it). * `-a` selects which rule to run; repeatable. 4. **Inspect the output** The engine writes a sibling `.g.cpp` next to the input header by default: include/color.g.cpp ```cpp // Generated with Codegen - DO NOT EDIT!!! #include #include "color.hpp" std::string_view toString(app::Color e) { switch (e) { case app::Color::Red: return "Red"; case app::Color::Green: return "Green"; case app::Color::Blue: return "Blue"; default: return ""; } } ``` The header itself is patched: the bare anchor expands into a paired `:begin]] / :end]]` block that holds the forward declaration. Subsequent runs replace only the content between the markers. 5. **Add the generated file to your build** CMakeLists.txt ```cmake target_sources(mylib PRIVATE include/color.g.cpp) ``` To re-trigger codegen automatically when the header changes, drive the binary from `add_custom_command` — see [CMake Integration](/integrations/cmake/) for the current pattern (the packaged `find_package(codegen)` flow is on the roadmap). ## What just happened [Section titled “What just happened”](#what-just-happened) The engine collected `.h`/`.hpp` files under `./include`, parsed them via codex, and matched the anchor comment against the `ToString` rule. It called `transform.luau` once with the enum’s AST node serialised as JSON. The script returned a `source` (the switch implementation) and a single-item `inline` list (the forward declaration). The engine wrote `color.g.cpp` and rewrote the anchor in `color.hpp` into the paired-marker block. ## Next step [Section titled “Next step”](#next-step) Read [Your First Rule](/getting-started/first-rule/) to write a rule from scratch. Working with an LLM agent? The full docs are bundled as a single plain-text file at [`/llms-full.txt`](/llms-full.txt). Paste it into your assistant’s context (Claude Code, Cursor, Copilot Chat, etc.) and it will have everything it needs to write rules, debug configs, and explain the AST schema — no crawling required. A condensed index is also available at [`/llms-small.txt`](/llms-small.txt). Key Takeaways * `codegen` selects rules by name with `-a`; the input is a path with `-i`. * Default outputs land **next to the input header** unless the rule config sets `outputDirectory`. * Anchor comments (`// [[codegen::generated::Rule::QualifiedName]]`) trigger anchor-based rules and mark the inline-injection site. * Attribute annotations (`[[codegen::Rule]]`) are the alternative trigger for declaration-level rules — any node kind listed in the rule’s `node_kinds`. * The first run rewrites bare anchors into idempotent `:begin]] / :end]]` blocks. # CLI Reference > Complete reference for the codegen command-line interface. ## Synopsis [Section titled “Synopsis”](#synopsis) ```sh codegen -i [-r ] -a [-a ...] [OPTIONS] codegen license [args] ``` Rules are loaded from `//` for each `-a` on the command line. The rules directory defaults to `.codegen/rules`. The input is a single header or a directory (recursively scanned for `.h` / `.hpp`). ## Required flag [Section titled “Required flag”](#required-flag) | Flag | Description | | -------------------- | ----------------------- | | `-i, --input ` | Input file or directory | `-a` is required in practice — without it the engine has no rules to run. ## Rule selection [Section titled “Rule selection”](#rule-selection) | Flag | Default | Description | | ------------------------ | ---------------- | --------------------------------------------- | | `-a, --attribute ` | (none) | Rule (attribute) name to process. Repeatable. | | `-r, --rules-dir ` | `.codegen/rules` | Root directory containing rule subfolders. | ## Preprocessor / parser [Section titled “Preprocessor / parser”](#preprocessor--parser) | Flag | Description | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-D, --define NAME[=VALUE]` | Preprocessor define. Repeatable. | | `-I, --include-path ` | Include search path. Repeatable. | | `--use-workspace` | Auto-load includes and defines from a build-system config (`codexx.workspace.yaml` or CMake file-api). Default workspace root is the CWD. **Requires Professional or Team licence** — emits `E102` and exits otherwise. | | `--workspace-dir ` | Override workspace root for `--use-workspace`. | When `--use-workspace` is active, includes and defines from the build-system config are merged with any explicit `-I` / `-D`. Manual `-D` flags take precedence over workspace defines (warning `W002`); inputs not found in the build-system config emit warning `W001`. ## Permissions / params [Section titled “Permissions / params”](#permissions--params) | Flag | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | | `-P, --param KEY=VALUE` | Override `params:` for every loaded rule. Value is parsed as JSON when possible, otherwise stored as a string. Repeatable. | | `--allow-http ` | Append a substring to the HTTP allowlist for every loaded rule. Repeatable. | | `--allow-env ` | Append an OS env name to the env allowlist for every loaded rule. Repeatable. | | `--env KEY=VALUE` | Override a dotenv entry. Repeatable; takes precedence over per-rule and shared `.env`. | ## Run-mode flags [Section titled “Run-mode flags”](#run-mode-flags) | Flag | Default | Description | | ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--dry-run` | off | Don’t write outputs. Emits a unified diff (zero context, `--- a/... / +++ b/... / @@`) of every would-be file change to stdout. Available on every licence tier; intended for CI and LLM-driven workflows. | | `--show-tui` | off | With `--dry-run`, opens the interactive FTXUI diff viewer instead of printing plain output. Requires a Professional+ licence; on Community tier emits `E102` and falls back to a filename list. | | `-v, --verbose` | off | Per-entity progress log. | | `-c, --config ` | (unused) | Generic profile-config slot inherited from the support CLI; codegen does not currently consume this value. | | `-p, --profile ` | (unused) | Generic named-profile slot inherited from the support CLI; not currently consumed. | | `-h, --help` | — | Print usage and exit `0`. | ## Debugger flags (only present when built with `LUAU_ENV_ENABLE_DEBUGGER`) [Section titled “Debugger flags (only present when built with LUAU\_ENV\_ENABLE\_DEBUGGER)”](#debugger-flags-only-present-when-built-with-luau_env_enable_debugger) | Flag | Default | Description | | ---------------------------- | ------------------------------ | -------------------------------------------------------------------- | | `--debug` | off | Enable the LuaU DAP server; the engine blocks until an IDE attaches. | | `--debug-port ` | `50001` | TCP port the DAP server listens on. | | `--debug-source-root ` | absolute path to `--rules-dir` | Source root reported to the IDE for file navigation. | The Professional-tier `vscode_dap_debugger` capability is not currently enforced at runtime — when the binary is built with the debugger enabled, `--debug` works on any tier. The licence gate ships in a future release. ## License subcommands [Section titled “License subcommands”](#license-subcommands) ```plaintext codegen license check Show current licence status codegen license fingerprint Print this machine's fingerprint ``` The `license` subcommand short-circuits the regular pipeline (no rules are loaded). Activation and deactivation moved to the **CodeXX DTDK manager** (`codexx_dtdk_manager`); `codegen license activate` / `deactivate` now print a pointer to the manager and exit non-zero. Output formats: ```plaintext $ codegen license check License valid. Tier: Professional Email: user@example.com Offline: yes (cached token) Expiry: 2026-12-31T23:59:59Z # only if the licence carries an expiry $ codegen license fingerprint <64-char hex SHA-256> ``` ## Exit codes [Section titled “Exit codes”](#exit-codes) | Code | Meaning | | ---- | ---------------------------------------------------------------------------------------------------- | | `0` | Success — all entities processed and all files written (or the dry-run preview completed). | | `1` | Recoverable failure — bad CLI, missing input, rule load failure, preamble error, licence error, etc. | | `2` | Fatal — handler internal error or LuaU environment creation failure. | ## Examples [Section titled “Examples”](#examples) ```sh # Run one rule against a directory of headers codegen -i ./include -r .codegen/rules -a ToString # Multiple rules in one invocation codegen -i ./include -a ToString -a MarkdownDocs # Build-system integration: pull includes/defines from CMake or workspace YAML codegen --use-workspace --workspace-dir . -i src/api.hpp -a TypeScriptTypes # Dry-run preview (unified diff to stdout — works on every tier, ideal for CI / LLMs) codegen -i ./include -a ToString --dry-run # Interactive TUI diff viewer (Professional+ licence) codegen -i ./include -a ToString --dry-run --show-tui # Manual preprocessor flags + HTTP allowlist override codegen -i ./include -a SchemaSync \ -I third_party/include -D BUILD_FEATURE=1 \ --allow-http "https://schema.example.com/" ``` # CMake Integration > Wire codegen rules into your CMake build graph. **🚧 Roadmap — WIP.** The `find_package(codegen)`, `codegen_rule()`, and `CODEGEN_OUTPUTS` helpers below are the design target for the [Team-tier](/licensing/tiers/) `cmake_integration` feature and are **not yet shipped**. The supported path today is `add_custom_command` driving the `codegen` binary; if you have a Professional or Team licence, `--use-workspace` reads includes/defines from `codexx.workspace.yaml` (or `compile_commands.json`) automatically. This page documents both the current and the planned path; track progress in the codegen changelog. ## Today: `add_custom_command` [Section titled “Today: add\_custom\_command”](#today-add_custom_command) Until the package ships, drive codegen from a regular `add_custom_command` / `add_custom_target`: CMakeLists.txt ```cmake set(CODEGEN_INPUT ${CMAKE_SOURCE_DIR}/include/color.hpp) set(CODEGEN_OUTPUT ${CMAKE_SOURCE_DIR}/include/color.g.cpp) add_custom_command( OUTPUT ${CODEGEN_OUTPUT} COMMAND codegen -i ${CODEGEN_INPUT} -r ${CMAKE_SOURCE_DIR}/.codegen/rules -a ToString DEPENDS ${CODEGEN_INPUT} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "codegen: ToString -> ${CODEGEN_OUTPUT}" VERBATIM ) add_custom_target(codegen_color DEPENDS ${CODEGEN_OUTPUT}) target_sources(mylib PRIVATE ${CODEGEN_OUTPUT}) add_dependencies(mylib codegen_color) ``` By default the engine writes outputs next to the input header, so the `OUTPUT` path is predictable: `.g.cpp` for cpp rules, `.` for other languages. Override with `outputDirectory` and `outputNameTemplate` in the rule’s config. If you use `--use-workspace` (Professional/Team tiers only), run codegen from the project root so the build-system config is found. Pass `--workspace-dir` to point at a different directory. ## Planned: `find_package(codegen)` + `codegen_rule()` [Section titled “Planned: find\_package(codegen) + codegen\_rule()”](#planned-find_packagecodegen--codegen_rule) The Team-tier package will install a CMake config module (`codegenConfig.cmake`) and a `codegen_rule()` helper that wraps the underlying `add_custom_command` and exposes the generated files for `target_sources`: CMakeLists.txt (planned) ```cmake find_package(codegen REQUIRED) codegen_rule( RULE ToString RULES_DIR ${CMAKE_SOURCE_DIR}/.codegen/rules INPUT ${CMAKE_SOURCE_DIR}/include OUTPUT_VAR CODEGEN_TOSTRING_OUTPUTS ) target_sources(mylib PRIVATE ${CODEGEN_TOSTRING_OUTPUTS}) ``` The helper will parse the rule config, derive `OUTPUT` paths from `outputDirectory` / `outputNameTemplate`, depend on the input headers, and chain the generated files into the target’s source list — eliminating the “forgot to regenerate” CI failure class. This page will be updated when the package ships. Key Takeaways * The `find_package(codegen)` / `codegen_rule()` flow is **not yet implemented**. * Today, drive codegen with `add_custom_command` calling the binary directly. * Default output paths are predictable from the rule config (`.g.cpp` for cpp, `.` otherwise). * The packaged helper is a Team-tier feature on the roadmap; this page tracks the target. # VS Code DAP Debugger > Set breakpoints in LuaU rule scripts and step through execution in VS Code using the LuaU debugger over DAP. The VS Code DAP debugger is a **Professional tier** feature. Today the gate is the `LUAU_ENV_ENABLE_DEBUGGER` compile-time flag — when present, `--debug` works on any tier. The licence gate ships in a future release. ## Overview [Section titled “Overview”](#overview) codegen embeds the [Roblox luau-debugger](https://github.com/roblox/luau-debugger) (vendored at `vendor/luau-debugger/`). When started with `--debug`, the engine spins up a DAP TCP server and blocks until a debugger attaches. You can then: * Set breakpoints in `.luau` scripts (preamble, handler, grouping) * Inspect the entity payload and any local variables * Step through execution * Watch the call stack ## Setup [Section titled “Setup”](#setup) 1. **Install the LuaU VS Code extension** Use the extension that ships with `vendor/luau-debugger/extensions/vscode/`. There is no codegen-specific VS Code extension. Any DAP-capable editor that speaks the same protocol works equivalently. 2. **Add a launch configuration** .vscode/launch.json ```json { "version": "0.2.0", "configurations": [ { "type": "luau", "request": "attach", "name": "attach to codegen", "address": "localhost", "port": 50001 } ] } ``` The default port is `50001`. Match it with `--debug-port` if you need a different one. 3. **Run codegen with the debugger flag** ```sh codegen --debug \ --debug-port 50001 \ --debug-source-root "$PWD/.codegen/rules" \ -i ./include \ -r .codegen/rules \ -a ToString ``` `--debug-source-root` should be the **absolute** path to the rules root, so VS Code can map breakpoints to the on-disk script files. If omitted, the engine uses the absolute path of `--rules-dir`. The engine prints a “waiting for debugger” line and blocks. 4. **Attach from VS Code** Press `F5` (or use the Run menu) with the launch configuration above active. Once attached, the engine resumes; execution pauses at any breakpoint you’ve set in a `.luau` script. ## What’s available in the inspector [Section titled “What’s available in the inspector”](#whats-available-in-the-inspector) When paused inside a handler, expand the local that holds the decoded entity to navigate the AST: ```lua local node = json.decode(input) -- ← break after this line, expand `node` ``` The Variables panel shows the full table including `_namespaces`, `_registryId`, `attributes`, `memberVariables`, and so on. This is the fastest way to understand the exact JSON shape your rule receives. ## Limitations [Section titled “Limitations”](#limitations) * The debugger attaches to a single rule run; it doesn’t keep a session across re-invocations. * Hot-reload (editing the script while paused) isn’t supported. Restart the debug session after edits. * The `--debug-port` is required for non-default ports; there’s no port discovery. * Debugger support requires the binary to be built with `LUAU_ENV_ENABLE_DEBUGGER`. The default build profiles enable it; release builds may not. Key Takeaways * Use the `luau` DAP launch type with `request: "attach"`; there is no codegen-specific extension. * The engine blocks at startup with `--debug` and resumes when the debugger attaches. * `--debug-source-root` is the absolute rules root used for VS Code’s source mapping. * Breakpoints work in handler, preamble, and grouping scripts. # License Activation > How to activate a CodeXX license in the DTDK manager, and the offline verification path. ## Where activation happens [Section titled “Where activation happens”](#where-activation-happens) Licenses are activated in the **CodeXX DTDK manager** (`codexx_dtdk_manager`), not in the `codegen` CLI. The manager owns activation for every licensed component, so individual tools no longer ship their own activation flow. Moved in a recent release Earlier builds activated with `codegen license activate `. That subcommand has been removed — `codegen` now prints a pointer to the manager and exits. See [Tiers & Pricing](/licensing/tiers/) for what each tier unlocks. ## Activating a component [Section titled “Activating a component”](#activating-a-component) 1. Launch the manager: `codexx_dtdk_manager`. 2. Use **↑ / ↓** to select a licensed component (e.g. **codegen**). 3. Press **Enter** — for an unactivated licensed component this opens the activation pane in place of the version list. 4. Paste your license key and confirm. The manager verifies the key, binds it to this machine’s fingerprint, and writes a per-product signed token to disk: * Linux/macOS: `$XDG_CONFIG_HOME/codexx/licensing/.token` (default `~/.config/codexx/licensing/.token`) * Windows: `%APPDATA%\CodeXX\licensing\.token` Each licensed component caches its own token (`codegen.token`, …). A legacy single-file `license.token` from an earlier release is auto-migrated to the per-product name on first read. Until a licensed component is activated the manager will not query its release channels; activation unlocks them. ## Checking the cached licence [Section titled “Checking the cached licence”](#checking-the-cached-licence) `codegen` keeps two read-only licence subcommands: ```sh codegen license check ``` Prints the cached tier and offline status: ```plaintext License valid. Tier: Professional Email: user@example.com Offline: yes (cached token) Expiry: 2026-12-31T23:59:59Z # only if the licence carries an expiry ``` `check` reads the local token, verifies the Ed25519 signature and machine fingerprint, and confirms the cache TTL has not elapsed. On TTL expiry it returns `Unreachable` and the `codegen` binary continues with Community-tier defaults rather than failing the run. ```sh codegen license fingerprint ``` Prints this machine’s stable 64-char hex SHA-256 fingerprint — useful when reporting an activation problem. ## Token format [Section titled “Token format”](#token-format) License tokens are Ed25519-signed JSON, verified against the public key baked in at build time (CMake define `KEYGEN_ED25519_PUBLIC_KEY`); a signed key verifies locally with no server round-trip. The token carries the licensed tier, the Keygen product, an optional ISO-8601 expiry, the user email, and the machine fingerprint. Every issued token is machine-bound — verification rejects a token whose fingerprint does not match the running host (`MachineMismatch`). Floating / cross-machine licences are not currently supported. ## Deactivating [Section titled “Deactivating”](#deactivating) To release a machine, select the activated component in the manager and press **Shift + D** (the `[D] deactivate-license` action in the footer). This clears the local token and releases the Keygen-side seat best-effort. Transferring a licence to another machine is deactivate-here, then activate-there. ## Offline / air-gapped activation [Section titled “Offline / air-gapped activation”](#offline--air-gapped-activation) There is no fingerprint-exchange CLI yet. The supported air-gapped path: activate in the manager on a temporarily-connected machine, then keep it offline — the cached signed token verifies without further network access for as long as the cache TTL holds. A first-class fingerprint-exchange flow is on the roadmap. ## Diagnostics [Section titled “Diagnostics”](#diagnostics) | Code | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `E101` | Licence invalid or not activated. Activate the component in `codexx_dtdk_manager`. | | `E103` | Cached token failed signature, machine-fingerprint, or TTL checks (or is corrupt / suspended / expired). Re-activate the component in `codexx_dtdk_manager`. | When the licence service is unreachable on a `check()` (no network and no cache, or TTL elapsed), the engine treats it as `Unreachable` and continues with Community-tier defaults — no fatal error. Key Takeaways * Activation lives in the `codexx_dtdk_manager` TUI — select a component, press Enter, paste the key. * Verification is offline against a per-product Ed25519-signed token; no server round-trip at runtime. * Tokens are machine-bound; transfer is deactivate (Shift + D in the manager) then re-activate. * `codegen` keeps read-only `license check` and `license fingerprint`; `activate` / `deactivate` were removed. # Support Policy > How to get technical support for CodeXX DTDK, what response times to expect at each licence tier, what is in and out of scope, and how your privacy is preserved when you send a report. ## How to get help [Section titled “How to get help”](#how-to-get-help) There is a single support channel for all tiers: * **Email:** Use the same address whether you are on Community, Professional, or Team. Your licence determines **priority and response time**, not which channel you use or the quality of the answer. Before emailing, check the [documentation](/) and the [Diagnostic Codes](/reference/diagnostics/) reference — most `E1xx` errors are explained there with a fix. A self-served answer is the fastest answer. ## What to include in a report [Section titled “What to include in a report”](#what-to-include-in-a-report) CodeXX DTDK runs entirely on your machine. We cannot see your code, your environment, or what went wrong unless you tell us. A report that includes the following gets resolved in one round-trip instead of five: * **Version** — output of `codegen --version` (or the affected tool’s `--version`). * **Operating system** and architecture. * **What you ran** — the exact command line. * **What happened** — the full error output, including any `E1xx` code. * **What you expected** instead. * **A minimal repro** if you can — the smallest input that triggers it. Only send what you are comfortable sharing. Redact anything proprietary — a repro that reproduces the bug is worth more than a real-but-secret one. See [Privacy](#privacy) below. ## Response targets [Section titled “Response targets”](#response-targets) Targets are measured in **business days, Central European Time**. They are response/triage targets — an acknowledgement and a path forward — not resolution guarantees. A fix may take longer depending on complexity. Community: free **Best effort.** Community reports are answered when time allows and are deprioritised behind paid tiers. The full documentation and the [Diagnostic Codes](/reference/diagnostics/) reference are your first port of call. No guaranteed response time. Professional: €12 / month **Best-effort priority.** Email is triaged ahead of Community. Target first response: **within 2 business days.** Team: from €24 / seat / month **Priority support.** Bug reports from Team customers are triaged **within 1 business day**, ahead of all other mail. Include your licence key or organisation name so your mail is routed to the priority queue automatically. Need a contractual SLA, a named contact, or air-gapped / on-prem support assurances? Bespoke arrangements are available on top of Team — email with your requirements. ## Comparison [Section titled “Comparison”](#comparison) | | Community | Professional | Team | | --------------------- | :---------: | :-------------: | :------------: | | Channel | Email | Email | Email | | First-response target | Best effort | 2 business days | 1 business day | | Priority queue | — | ✓ | ✓ (highest) | | Custom SLA available | — | — | on request | ## Scope [Section titled “Scope”](#scope) **In scope** — we will help with: * Installation, activation, and licensing issues. * Bugs, crashes, and incorrect output in CodeXX DTDK tools. * Diagnostic (`E1xx`) codes and how to resolve them. * Documented features behaving differently than documented. * Reasonable “how do I do X” guidance for documented functionality. **Out of scope** — we cannot take on: * Writing your rules, transforms, or build configuration for you (though we will point you at the right docs and examples). * Debugging your application’s own C++ code. * Third-party tools, compilers, or build systems, except where they integrate directly with CodeXX DTDK. * Features on tiers you do not hold — see [Tiers & Pricing](/licensing/tiers/). ## Privacy [Section titled “Privacy”](#privacy) CodeXX DTDK makes [no outbound network requests during execution](/trust/no-call-home/). Support does not change that: * We never have access to your code, AST, or environment unless **you** attach it to an email. * We only use what you send to reproduce and fix the specific issue you reported. * We never ask for your source tree wholesale — a minimal repro is always sufficient and always preferred. ## Reporting a security issue [Section titled “Reporting a security issue”](#reporting-a-security-issue) Do **not** open a public report for security vulnerabilities. Email with `SECURITY` in the subject line and we will coordinate a private fix and disclosure. This applies to every tier, including Community. # Tiers & Pricing > codegen licensing tiers, Community, Professional, and Team. Feature breakdown and guidance on which tier fits your use case. ## Overview [Section titled “Overview”](#overview) codegen is available in three tiers. All tiers use the same binary and the same rule engine — the tier controls which features are unlocked, not which version of the engine you run. Some features below are gated by the licence at run time today; others are documented as gated but the runtime gate will land in a future release. Each section flags its current enforcement status. Community: Free **For:** Open-source projects, personal experiments, evaluation. **License:** Free, no time limit. No commercial use. The Community tier includes the full rule engine. There are no artificial rule count limits, no watermarks in generated output, and no expiry. Professional: €12 / month or €99 / year **For:** Individual developers building commercial software. **License:** Per-user, non-transferable. Covers one person across all their machines. Everything in Community, plus: ### TUI diff viewer [Section titled “TUI diff viewer”](#tui-diff-viewer) `codegen --dry-run --show-tui` opens a side-by-side FTXUI diff of the existing and proposed contents of every output file (and every inline-edit target) before anything is written. You approve or reject the apply step interactively. ```sh codegen -i ./include -a ToString --dry-run --show-tui ``` **Today:** enforced. Without the Professional flag on the cached licence, `--show-tui` emits `E102` and prints the count of would-be files instead of opening the viewer. > Note: plain `--dry-run` (without `--show-tui`) is **available on every tier** — it emits a unified diff (zero context) to stdout. The Professional gate covers only the interactive viewer. ### VS Code DAP debugger [Section titled “VS Code DAP debugger”](#vs-code-dap-debugger) Step through `.luau` rule scripts in VS Code using the LuaU DAP server. Set breakpoints, inspect the entity payload, walk the call stack. See [VS Code DAP Debugger](/integrations/vscode-debugger/) for setup. **Today:** enforced. Builds require the `LUAU_ENV_ENABLE_DEBUGGER` compile-time flag for the `--debug` flag to be exposed at all; at runtime, without the Professional flag set on the cached licence, `--debug` emits `E102` and exits. ### Workspace integration (`--use-workspace`) [Section titled “Workspace integration (--use-workspace)”](#workspace-integration---use-workspace) `codegen --use-workspace` pulls include paths and preprocessor defines straight from your build system (`codexx.workspace.yaml` or `compile_commands.json`), so rules see the same translation-unit context as your compiler — no hand-maintained `-I`/`-D` lists. ```sh codegen -i ./include -a JSONSerializable --use-workspace ``` **Today:** enforced. Without the Professional flag set on the cached licence, `--use-workspace` emits `E102` and exits — fall back to passing `-I`/`-D` manually. Team: from €24 / seat / month or €230 / seat / year **For:** Engineering organisations that ship production software. **License:** Per-seat, organisation-managed. Seats are assigned via the team dashboard. Everything in Professional, plus: ### CMake helper package [Section titled “CMake helper package”](#cmake-helper-package) A `find_package(codegen)` config module + `codegen_rule()` helper that wires generated files into your CMake target graph, so headers re-trigger codegen automatically before compilation. **Today:** the helper package is not yet shipped. The Professional-tier `--use-workspace` flow already pulls includes/defines from `codexx.workspace.yaml` or `compile_commands.json` automatically; drive codegen with `add_custom_command` against the binary in the meantime. See [CMake Integration](/integrations/cmake/) for the current and planned path. ### Shared preamble & grouping libraries [Section titled “Shared preamble & grouping libraries”](#shared-preamble--grouping-libraries) Drop reusable Lua modules into `/../shared/` (for example `.codegen/shared/`): * Every `.luau` file in that directory is loaded in alphabetical filename order, concatenated, and prepended to every rule’s handler at execution time. Anything declared at the top level (functions, tables of constants, type maps) is therefore available to every rule without `require`. * A `/../shared/.env` file is loaded as a baseline dotenv; per-rule `.env` entries override it. Version-control the `shared/` directory alongside your rules; standardise output conventions across the whole codebase. **Today:** enforced. Without the Team flag set on the cached licence, the engine detects `/../shared/`, emits `E102`, and skips loading it (rules run with their own scripts only). ### Priority support [Section titled “Priority support”](#priority-support) Direct response SLA. Bug reports from Team customers are triaged within one business day. ## Comparison table [Section titled “Comparison table”](#comparison-table) | Feature | Community | Professional | Team | Today’s enforcement | | -------------------------------------------------------- | :-------: | :----------: | :-------------------: | ------------------- | | Full rule engine | ✓ | ✓ | ✓ | — | | Commercial use | ✗ | ✓ | ✓ | not enforced | | TUI diff viewer | ✗ | ✓ | ✓ | enforced | | VS Code DAP debugger | ✗ | ✓ | ✓ | enforced | | Workspace integration (`--use-workspace`) | ✗ | ✓ | ✓ | enforced | | CMake helper package (`find_package` + `codegen_rule()`) | ✗ | ✗ | ✓ | not implemented | | Shared `.codegen/shared/` libs | ✗ | ✗ | ✓ | enforced | | Priority support | ✗ | ✗ | ✓ | n/a | | **Price (monthly)** | **Free** | **€12/mo** | **from €24/seat/mo** | | | **Price (annual)** | **Free** | **€99/yr** | **from €230/seat/yr** | | *** ## Frequently asked questions [Section titled “Frequently asked questions”](#frequently-asked-questions) **Can I evaluate the Professional features before buying?** Yes — Professional includes a 14-day free trial and Team includes a 30-day free trial. A card is required at checkout, but no charge happens during the trial. Cancel before it ends and you’re not billed. Trials apply to monthly and annual plans alike. The Community tier separately ships the full rule engine free forever for everything outside the Professional/Team gated features. **Are there volume discounts on Team?** Yes. Team is €24 / seat / month at 1–4 seats, €20.40 / seat / month at 5–19 seats (15% off), and €18 / seat / month at 20+ seats (25% off). Annual billing stacks \~20% on top of each bracket. **Does a Team seat cover the same person across multiple machines?** A seat covers one person, but each machine activates separately and binds the cached licence token to that machine’s fingerprint. Today, transferring a licence to a different machine means deactivating it on the old machine and re-activating on the new one — both done in the `codexx_dtdk_manager` TUI (`[D] deactivate-license`, then `Enter` to activate). **What happens if I stop paying?** The engine reverts to Community-tier behaviour. Your rules, configs, and generated files are unaffected. **Is there an academic or OSS project discount?** OSS projects (public repository, OSI-approved license) can request a Professional licence at no cost. Contact [support](mailto:support@codexx-dtdk.com) with a link to your repository. **Can the Team tier be used in air-gapped environments?** See [License Activation](/licensing/activation/). # Config Schema (.yaml) > Complete reference for the codegen rule configuration file format. Each rule lives in `//` and ships with a `config.yaml` (always named `config.yaml` — the directory name is what carries the rule identity). This page is the authoritative schema reference for that file. ## Top-level structure [Section titled “Top-level structure”](#top-level-structure) ```yaml version: 1 # required, must be 1 extends: ../base.yaml # optional, path relative to this file output: language: cpp # required for non-default; see table below autoDeduceIncludes: true # default: true outputDirectory: "" # optional, override default output directory outputNameTemplate: "" # optional, override output filename pattern permissions: # optional http: allowlist: - "schema-registry.example.com" env: os_allowlist: - "BUILD_CHANNEL" params: # optional, free-form, exposed to scripts as `params` apiVersion: "v1" emitDocs: true node_kinds: # optional, defaults to ["Struct"] - Struct - Class - Enum shared: # optional, controls which shared/*.luau scripts load include: ["json_*"] # absent = include all; [] = include none exclude: ["legacy_*"] # subtracted from include ``` ## `version` [Section titled “version”](#version) Must be the integer `1`. Any other value emits `E002` and aborts. ## `extends` [Section titled “extends”](#extends) Optional path (relative to this config file) to a base config. The base is loaded first, then values in this file override matching keys. Chains deeper than 8 levels emit `E001`. A missing target emits `E001`. ## `output.language` [Section titled “output.language”](#outputlanguage) Selects the default file extension and the C++-specific assembly path (banner, self-include, project-include rebasing). Must be one of: | Value | Default extension | | ------------ | ----------------- | | `cpp` | `.g.cpp` | | `csharp` | `.cs` | | `kotlin` | `.kt` | | `swift` | `.swift` | | `typescript` | `.ts` | | `plain` | `.txt` | | `markdown` | `.md` | | `html` | `.html` | | `mdx` | `.mdx` | Any other value emits `E001`. Defaults to `cpp` when omitted. Only `cpp` triggers the C++ assembly path: `// Generated with Codegen - DO NOT EDIT!!!` banner, `#pragma once` for header outputs, automatic self-include of the input header, and project-include rebasing when `outputDirectory` relocates the output. ## `output.autoDeduceIncludes` [Section titled “output.autoDeduceIncludes”](#outputautodeduceincludes) Default `true`. When enabled, the engine takes the rule’s emitted `includes` list and removes any entries already covered by the input header’s transitive include graph (per the analyzer). Set to `false` to emit every requested include verbatim. This is **not** a stdlib auto-detector — it filters explicit includes against the include graph; it does not synthesize includes from generated source text. ## `output.outputDirectory` [Section titled “output.outputDirectory”](#outputoutputdirectory) Overrides the default output directory (which is the input header’s parent directory). Path is taken as-is — relative paths are resolved against the process working directory. ## `output.outputNameTemplate` [Section titled “output.outputNameTemplate”](#outputoutputnametemplate) Overrides the default output filename. Tokens (Go-template style): | Token | Substitution | | ---------------- | --------------------------------------------------- | | `{{.InputFile}}` | Input header filename without extension | | `{{.InputDir}}` | Input header’s parent directory (generic-form path) | | `{{.RuleName}}` | The rule (attribute) name | Default filename when this is unset: * `cpp` → `.g.cpp` * everything else → `.` The full path is `/`. ## `permissions.http.allowlist` [Section titled “permissions.http.allowlist”](#permissionshttpallowlist) A list of host patterns. Each entry is matched against the **host component** of the URL passed to `http.get(...)` (NOT a substring of the full URL). Supported forms: | Entry | Meaning | | --------------- | ---------------------------------------------------------------------------------------------------------- | | `example.com` | Exact host match. `evil.example.com` is **not** matched. | | `*.example.com` | Wildcard: any subdomain (`api.example.com`, `a.b.example.com`). Does **not** match the apex `example.com`. | | `*` | Match any host (escape hatch). | An empty or absent allowlist disables HTTP entirely; calls to `http.get` raise `http.get: url not in allowlist:` and emit `E010`. The CLI flag `--allow-http ` appends entries to the allowlist at run time. ## `permissions.env.os_allowlist` [Section titled “permissions.env.os\_allowlist”](#permissionsenvos_allowlist) A list of OS environment variable names the rule’s scripts may read. When non-empty, the engine activates the env capability and exposes the named vars to the LuaU env module. The CLI flag `--allow-env ` appends entries to this list at run time. Dotenv files (`//.env` and `/../shared/.env`) are loaded automatically when present and exposed via the same env module; they do not require an allowlist entry. The CLI flag `--env KEY=VALUE` overrides individual dotenv entries. ## `params` [Section titled “params”](#params) Free-form YAML, converted to JSON and exposed to every script as `params`: * Preamble script: `params` is a key on the JSON context object passed in. * Handler (transform) script: `params` is added under the `params` key on the entity payload. * Grouping script: `params` is a sibling of `entities` on the input object. The CLI flag `-P key=value` (repeatable) overrides params per invocation. The value is parsed as JSON when possible, otherwise stored as a string. ## `node_kinds` [Section titled “node\_kinds”](#node_kinds) A list of `NodeKind` enum names. Defaults to `["Struct"]` when omitted. Controls which AST node kinds the rule’s attribute matcher will fire on. Valid entries are listed in `engine/node_introspection.cpp` (a few common ones: `Struct`, `Class`, `Union`, `Function`, `Variable`, `Constructor`, `Destructor`, `Operator`, `EnumSpecifier`). Unknown entries emit a `warning: unknown node_kind ''` line on stderr but do not fail the load. ## `shared` [Section titled “shared”](#shared) Controls which `/../shared/*.luau` scripts get prepended to this rule’s transform. Both keys take a list of fnmatch-style globs (`*`, `?`, `[abc]`) matched against the script’s filename **stem** (i.e. `json_helpers.luau` matches as `json_helpers`). | Key | Behaviour | | ------------------------------ | --------------------------------------------------------------- | | `shared.include` absent | Include every `shared/*.luau` (default). | | `shared.include: []` | Include none — no shared prelude for this rule. | | `shared.include: ["a", "b_*"]` | Include only the matching stems. | | `shared.exclude: [...]` | Subtract matching stems from whatever the include step yielded. | Independence assumption: shared scripts are concatenated in alphabetical filename order with no `require`-style dependency tracking. If your shared scripts depend on each other, name them so dependencies sort before dependents. ## Companion files (not part of the YAML) [Section titled “Companion files (not part of the YAML)”](#companion-files-not-part-of-the-yaml) * `transform.luau` — handler script, **required**. * `preamble.luau` — preamble script, **required** (an empty `return function() return "" end` is fine). * `grouping.luau` — grouping script, optional. * `.env` — per-rule dotenv, optional. Plus the workspace-shared directory: * `/../shared/*.luau` — auto-loaded in alphabetical filename order, filtered through each rule’s `shared.include` / `shared.exclude`, and prepended to that rule’s transform. Team-tier feature; today the directory is loaded unconditionally and the licence gate ships later. * `/../shared/.env` — baseline dotenv; per-rule entries override. ## Full annotated example [Section titled “Full annotated example”](#full-annotated-example) config.yaml ```yaml version: 1 extends: ../base.yaml output: language: cpp autoDeduceIncludes: false outputDirectory: generated/advanced outputNameTemplate: "{{.InputFile}}.{{.RuleName}}.g.cpp" permissions: http: allowlist: - "https://schema-registry.internal.example.com/" env: os_allowlist: - "BUILD_CHANNEL" params: apiVersion: "v1" emitDocs: true node_kinds: - Struct - Class ``` # Diagnostic Codes > Engine error and warning codes, what they mean and how to resolve them. All diagnostics are written to `stderr` in the format: ```plaintext [E006] ToString | Color @ include/color.h ``` The fields after the code are: rule name, entity name, source path, detail. Any of the trailing fields can be empty. ## Error codes [Section titled “Error codes”](#error-codes) | Code | Trigger | Resolution | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `E001` | Rules directory not found | Check the path passed to `-r`. | | `E002` | `version:` in the rule config is not `1` | Set `version: 1` or upgrade the codegen binary. | | `E003` | `transform.luau` or `preamble.luau` not found in the rule directory | Add the missing handler/preamble. | | `E004` | Grouping script returned a map missing one or more entity `registryId`s | Ensure every entity passed in is keyed in the returned map. The affected entity is skipped; the run continues. | | `E005` | A path returned by grouping resolves outside the project root (CWD) | Use a path that stays under the working directory. The affected entity is skipped. | | `E006` | Handler script raised a Lua error or an internal LuaU error occurred | See the detail line for the script line and reason. Internal errors are fatal (exit 2). | | `E007` | Preamble script raised an error | The whole run aborts with exit 1. Fix the preamble before re-running. | | `E008` | Grouping script raised an error or returned invalid JSON | The affected rule’s grouping is skipped (entities keep their default 1:1 path). | | `E009` | Handler returned something that wasn’t `json.encode({...})` | Wrap the return in `json.encode({...})`. | | `E010` | `http.get(url)` called with a URL whose host is not on `permissions.http.allowlist` | Add the host (`example.com`) or a wildcard (`*.example.com`) to the allowlist, or stop calling `http.get` for that URL. | | `E011` | The `--input` path doesn’t exist | Check the path passed to `-i`. | | `E012` | Output directory could not be created or output file is not writable | Check filesystem permissions and free space. | | `E013` | `--use-workspace` path doesn’t exist | Check `--workspace-dir` (or the CWD when omitted). | | `E014` | `--use-workspace` could not load the build-system config | See the detail line; usually a malformed `codexx.workspace.yaml` or unparseable CMake file-api reply. | | `E015` | Rule config schema/shape error: unknown `output.language`, malformed `permissions.http.allowlist` / `permissions.env.os_allowlist`, `extends:` target missing, or `extends:` chain too deep | Fix the offending field in `config.yaml` (see detail line for which one). | | `E016` | Rule config YAML parse failure | The detail line carries the YAML parser error. Fix the syntax in `config.yaml`. | | `E017` | Engine init failure (LuaU environment could not be created) | Fatal (exit 2). Indicates an internal bug — file an issue with the detail line. | | `E018` | Source analysis pipeline failed before any rule fired | Check the input headers parse cleanly with `ast_dump`. | | `E019` | Two or more rules’ default output paths collide | Set `outputNameTemplate` or a `grouping.luau` to disambiguate. | | `E101` | License invalid key (no signed token, malformed activation key) | Activate the component in `codexx_dtdk_manager`. | | `E102` | Feature requires a higher tier (e.g. `--dry-run` TUI viewer is Professional+) | See [Tiers & Pricing](/licensing/tiers/). | | `E103` | Cached license token failed signature, machine-mismatch, expired, suspended, or corrupt | Re-activate the component in `codexx_dtdk_manager`. | ## Warning codes [Section titled “Warning codes”](#warning-codes) | Code | Trigger | Notes | | ------ | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | | `W001` | `--use-workspace` is set but the input has no entry in the loaded build-system config | Includes/defines won’t be auto-resolved for this input. | | `W002` | A `-D NAME=VALUE` on the CLI overrides a workspace define of the same name | The CLI value wins; the warning is informational. | | `W003` | An entity’s anchor is not present in its source header (or the anchor references an unknown entity) | The `.g.cpp` is still written; only the inline forward-declaration injection is skipped. | | `W004` | Inline-edit file I/O issue (cannot open source for read, cannot write the temp file, or rename failed) | The inline edit for the affected file is skipped; the `.g.cpp` itself is unaffected. | | `W005` | Unknown `node_kind` entry in `config.yaml` | The unknown entry is dropped; remaining `node_kinds` are honoured. | | `W006` | Unsupported config feature referenced (e.g. `permissions.registry`) | The entry is ignored; the rule otherwise loads normally. | ## Exit codes [Section titled “Exit codes”](#exit-codes) | Code | Meaning | | ---- | ------------------------------------------------------------------------------------------------------ | | `0` | Success — all entities processed and all files written (or the dry-run preview completed). | | `1` | Recoverable failure — license error, bad CLI, missing input, rule load failure, preamble failure, etc. | | `2` | Fatal — handler internal error or LuaU environment creation failure. | # LuaU Globals > Complete reference for the globals injected into every codegen rule script. ## Always-available globals [Section titled “Always-available globals”](#always-available-globals) These are present in every script (preamble, handler, grouping) regardless of permissions. ### `json` [Section titled “json”](#json) | API | Signature | | -------------------- | ---------------------------------------------- | | `json.encode(value)` | `any -> string` | | `json.decode(s)` | `string -> any` (returns `nil` on parse error) | Tables with integer keys serialise as JSON arrays; tables with string keys serialise as objects. Mixed tables are not supported. ```lua local node = json.decode(input) if not node then error("invalid input") end return json.encode({ source = "...", inline = {} }) ``` ### Native introspection API [Section titled “Native introspection API”](#native-introspection-api) These functions are registered by the engine and use stable opaque integer IDs (the `_registryId` field on the entity payload, or the result of a lookup). | Function | Signature | Description | | ------------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `get_node(id)` | `int -> string\|nil` | Returns the node’s full JSON, or nil. | | `find_struct_by_name(name)` | `string -> int\|nil` | Returns the registry ID of a struct by unqualified name (first match across all parsed sources). | | `struct_has_attribute(id, name)` | `int, string -> bool\|nil` | True if the struct carries `[[codegen::name]]` (or any namespaced attribute named `name`). | | `struct_has_method(id, name)` | `int, string -> bool\|nil` | True if the struct has a member function, static member function, or constructor with this name. | | `struct_has_member(id, name)` | `int, string -> bool\|nil` | True if the struct has a member or static member variable with this name. | | `struct_has_default_constructor(id)` | `int -> bool\|nil` | True if the struct has no constructors, or if any constructor’s parameters all have default values. | | `get_base_classes(id)` | `int -> string\|nil` | Returns a JSON array of `{access, name, qualifiedName, id}` for each base class. `id` is `null` when the analyzer can’t resolve the symbol. | | `get_definition_header(name)` | `string -> string\|nil` | Returns the path to the header that defines a given qualified or unqualified name, when known to the analyzer. | All functions return `nil` on bad arguments (wrong type or missing); only the lookup functions return `nil` for “not found.” ## Context objects [Section titled “Context objects”](#context-objects) ### Preamble context [Section titled “Preamble context”](#preamble-context) Injected via the `input` parameter to `preamble.luau`: ```json { "language": "cpp", "outputFile": "path/to/output.cpp", "ruleName": "RuleName", "params": { /* rule.params */ }, "entities": [ { "registryId": 42, "qualifiedName": "app::MyStruct", "structName": "MyStruct", "namespaces": ["app"], "inputFile": "include/app/my_struct.hpp", "includes": [""] } ] } ``` The `entities` array contains every entity bound to this output file. Use it to conditionally emit includes or other file-level content. ### Grouping context [Section titled “Grouping context”](#grouping-context) Injected via the `input` parameter to `grouping.luau`: ```json { "params": { /* rule.params */ }, "entities": [ { "registryId": 42, "qualifiedName": "app::MyStruct", "structName": "MyStruct", "namespaces": ["app"], "inputFile": "include/app/my_struct.hpp", "defaultPath": "include/app/my_struct.g.cpp" } ] } ``` The `defaultPath` is the 1:1 output path (derived from the input header). Override it by returning a map from `registryId` to a new path. ### Handler context [Section titled “Handler context”](#handler-context) Injected via the `input` parameter to `transform.luau`: The full entity JSON from the codex AST, with these synthetic fields added: * `_namespaces` (array of strings, the qualifying namespace path) * `_registryId` (integer, stable identifier) * `_outputPath` (string, the post-grouping output file path) * `params` (object, copied from rule config) The `_outputPath` is the final output file this entity will land in (after grouping, if present). ## Standard LuaU library [Section titled “Standard LuaU library”](#standard-luau-library) Available subset (the LuaU sandbox restricts the unsafe parts): | Library | Use | | -------- | ----------------------------------------------------------------------------------------------------------- | | `table` | `insert`, `remove`, `concat`, `sort`, `unpack` | | `string` | `format`, `find`, `gmatch`, `gsub`, `sub`, `len`, `rep`, `upper`, `lower` | | `math` | `floor`, `ceil`, `min`, `max`, `abs`, `huge`, `pi` | | Globals | `ipairs`, `pairs`, `tostring`, `tonumber`, `error`, `pcall`, `xpcall`, `type`, `select`, `assert`, `unpack` | ## Permission-gated globals [Section titled “Permission-gated globals”](#permission-gated-globals) These appear only when the corresponding capability is requested via `permissions:` in the rule config (or via CLI overrides). ### `http` [Section titled “http”](#http) Enabled when `permissions.http.allowlist` is non-empty (or the CLI passes `--allow-http`). | API | Signature | | --------------- | ---------------------------------------------------------------------------------------- | | `http.get(url)` | `string -> string` (response body), or yields `(nil, error_string)` on transport failure | The URL’s host component is matched against the allowlist (exact host, `*.domain` wildcard, or `*` as escape hatch — see [`permissions.http.allowlist`](/reference/config-schema/#permissionshttpallowlist)). A URL whose host is not on the allowlist raises a runtime error and emits diagnostic `E010`. Concurrent requests on the same VM are not supported. ### `env` [Section titled “env”](#env) Enabled when `permissions.env.os_allowlist` is non-empty, or when any dotenv source is configured (per-rule `.env`, shared `/../shared/.env`, or CLI `--env KEY=VALUE`). The env module exposes the allowlisted OS env vars and the merged dotenv (CLI `--env` > per-rule `.env` > shared `.env`). See the [Permissions](/rules/permissions/) page for the surface API. ## Absent globals [Section titled “Absent globals”](#absent-globals) The following are explicitly **not** provided: * `require` and any module loading mechanism * `io` (file I/O) * `os` (process/time/system access — `os.getenv` is **not** the env capability; that’s a separate module) * `debug` (Lua debug library; the DAP debugger is a separate feature, see [VS Code DAP Debugger](/integrations/vscode-debugger/)) * `coroutine` (the engine drives the coroutine for `http.get` internally; user scripts cannot create their own) * `loadstring` / `loadfile` * Any FFI Key Takeaways * `json.encode` / `json.decode` and the native introspection API are always present. * HTTP and env access are opt-in via `permissions:` in the rule config; nothing else opens the sandbox. * The native API gives scripts cross-entity lookup (base classes, definition headers, finding structs by name) without re-implementing the analyzer. # Core Nodes ## `TemplateArgument` [Section titled “TemplateArgument”](#templateargument) | Field | Type | | --------------- | ---------------- | | `keyword` | `string` | | `typeSignature` | `TypeSignature?` | | `expr` | `Node?` | | `value` | `string` | *** ## `TemplateParameter` [Section titled “TemplateParameter”](#templateparameter) | Field | Type | | ----------------- | ----------------------- | | `paramKind` | `TemplateParameterKind` | | `name` | `string` | | `isVariadic` | `boolean` | | `defaultValue` | `Node?` | | `keyword` | `string` | | `constraint` | `IdentifierNode?` | | `typeSignature` | `TypeSignature` | | `innerParameters` | `TemplateParameter[]` | *** ## `FunctionArgument` [Section titled “FunctionArgument”](#functionargument) | Field | Type | | ----------------- | --------- | | `expr` | `Node?` | | `isPackExpansion` | `boolean` | *** ## `FunctionParameter` [Section titled “FunctionParameter”](#functionparameter) | Field | Type | | --------------- | --------------- | | `typeSignature` | `TypeSignature` | | `name` | `string` | | `defaultValue` | `Node?` | *** ## `IdentifierNode` *(extends `Node`)* [Section titled “IdentifierNode (extends Node)”](#identifiernode-extends-node) *kind discriminant: `"Identifier"`* | Field | Type | | --------------- | -------------------- | | `qualification` | `IdentifierNode?` | | `name` | `string` | | `templateArgs` | `TemplateArgument[]` | *** ## `TypeDeclarator` [Section titled “TypeDeclarator”](#typedeclarator) | Field | Type | | --------------- | ---------------- | | `kind` | `DeclaratorKind` | | `isConst` | `boolean` | | `isVolatile` | `boolean` | | `arraySizeExpr` | `Node?` | *** ## `PlaceholderTypeSpecifier` [Section titled “PlaceholderTypeSpecifier”](#placeholdertypespecifier) | Field | Type | | ------------ | ---------------- | | `kind` | `Auto` | | `constraint` | `TypeSignature?` | *** ## `FunctionPointerSignature` [Section titled “FunctionPointerSignature”](#functionpointersignature) | Field | Type | | ----------------------- | --------------------------- | | `scopeName` | `string` | | `parameterTypes` | `TypeSignature[]` | | `returnFunctionPointer` | `FunctionPointerSignature?` | | `isConst` | `boolean` | *** ## `TypeSignature` [Section titled “TypeSignature”](#typesignature) | Field | Type | | ---------------------- | --------------------------- | | `identifier` | `IdentifierNode?` | | `isConst` | `boolean` | | `isVolatile` | `boolean` | | `isMutable` | `boolean` | | `declarators` | `TypeDeclarator[]` | | `functionPointer` | `FunctionPointerSignature?` | | `decltypeSpecifier` | `DecltypeExpressionNode?` | | `placeholderSpecifier` | `PlaceholderTypeSpecifier?` | *** ## `SourceNode` *(extends `Node`)* [Section titled “SourceNode (extends Node)”](#sourcenode-extends-node) *kind discriminant: `"Source"`* | Field | Type | | ---------- | --------- | | `source` | `Source?` | | `children` | `Node[]` | *** ## `LambdaCaptureItem` [Section titled “LambdaCaptureItem”](#lambdacaptureitem) | Field | Type | | ------------ | ------------------- | | `kind` | `LambdaCaptureKind` | | `identifier` | `IdentifierNode?` | | `init` | `Node?` | *** ## `MacroParameter` [Section titled “MacroParameter”](#macroparameter) | Field | Type | | ------------ | --------- | | `name` | `string` | | `isVariadic` | `boolean` | *** ## `FunctionQualifiers` [Section titled “FunctionQualifiers”](#functionqualifiers) | Field | Type | | ------------------- | --------- | | `isConst` | `boolean` | | `isVolatile` | `boolean` | | `isVirtual` | `boolean` | | `isPureVirtual` | `boolean` | | `isOverride` | `boolean` | | `isFinal` | `boolean` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `isConstexpr` | `boolean` | | `isConsteval` | `boolean` | | `isExplicit` | `boolean` | | `explicitCondition` | `Node?` | | `isInline` | `boolean` | | `isStatic` | `boolean` | | `isDefaulted` | `boolean` | | `isDeleted` | `boolean` | | `refQualifier` | `None` | | `requiresClause` | `Node?` | *** ## `CommentNode` *(extends `Node`)* [Section titled “CommentNode (extends Node)”](#commentnode-extends-node) *kind discriminant: `"Comment"`* | Field | Type | | ------ | -------- | | `text` | `string` | *** ## `NamespaceSegment` [Section titled “NamespaceSegment”](#namespacesegment) | Field | Type | | ---------- | --------- | | `name` | `string` | | `isInline` | `boolean` | *** ## `Source` [Section titled “Source”](#source) | Field | Type | | --------------------- | ------------- | | `name` | `string` | | `path` | `string` | | `content` | `string` | | `encoding` | `string` | | `lastModifiedTime` | `number` | | `rawContent` | `string?` | | `sourceMap` | `SourceMap?` | | `macroTable` | `MacroTable?` | | `pendingIncludeEdges` | `string[]` | | `unsavedBuffer` | `string?` | *** ## `Attribute` [Section titled “Attribute”](#attribute) | Field | Type | | ------ | ---------- | | `ns` | `string` | | `name` | `string` | | `args` | `string[]` | *** ## `Node` [Section titled “Node”](#node) | Field | Type | | ------------- | ---------- | | `kind` | `NodeKind` | | `startLine` | `number` | | `startColumn` | `number` | | `endLine` | `number` | | `endColumn` | `number` | | `comment` | `Node?` | *** # Declaration Nodes ## `AttributeDeclarationNode` *(extends `DeclarationNode`)* [Section titled “AttributeDeclarationNode (extends DeclarationNode)”](#attributedeclarationnode-extends-declarationnode) *kind discriminant: `"AttributeDeclaration"`* | Field | Type | | ------------ | ------------- | | `attributes` | `Attribute[]` | *** ## `TemplateNode` *(extends `DeclarationNode`)* [Section titled “TemplateNode (extends DeclarationNode)”](#templatenode-extends-declarationnode) *kind discriminant: `"Template"`* | Field | Type | | ---------------- | --------------------- | | `parameters` | `TemplateParameter[]` | | `requiresClause` | `Node?` | *** ## `NamespaceNode` *(extends `DeclarationNode`)* [Section titled “NamespaceNode (extends DeclarationNode)”](#namespacenode-extends-declarationnode) *kind discriminant: `"Namespace"`* | Field | Type | | ------------- | -------------------- | | `segments` | `NamespaceSegment[]` | | `isAnonymous` | `boolean` | | `isInline` | `boolean` | | `children` | `Node[]` | *** ## `UsingNamespaceNode` *(extends `DeclarationNode`)* [Section titled “UsingNamespaceNode (extends DeclarationNode)”](#usingnamespacenode-extends-declarationnode) *kind discriminant: `"UsingNamespace"`* | Field | Type | | ------------ | ----------------- | | `identifier` | `IdentifierNode?` | *** ## `StaticAssertNode` *(extends `DeclarationNode`)* [Section titled “StaticAssertNode (extends DeclarationNode)”](#staticassertnode-extends-declarationnode) *kind discriminant: `"StaticAssert"`* | Field | Type | | ----------- | -------- | | `condition` | `Node?` | | `message` | `string` | *** ## `NamespaceAliasNode` *(extends `DeclarationNode`)* [Section titled “NamespaceAliasNode (extends DeclarationNode)”](#namespacealiasnode-extends-declarationnode) *kind discriminant: `"NamespaceAlias"`* | Field | Type | | ----------------- | ----------------- | | `aliasName` | `string` | | `targetNamespace` | `IdentifierNode?` | *** ## `TypedefNode` *(extends `DeclarationNode`)* [Section titled “TypedefNode (extends DeclarationNode)”](#typedefnode-extends-declarationnode) *kind discriminant: `"Typedef"`* | Field | Type | | ------------ | ------------------ | | `aliasName` | `string` | | `typeDecl` | `DeclarationNode?` | | `targetType` | `TypeSignature?` | *** ## `TypeAliasNode` *(extends `DeclarationNode`)* [Section titled “TypeAliasNode (extends DeclarationNode)”](#typealiasnode-extends-declarationnode) *kind discriminant: `"TypeAlias"`* | Field | Type | | -------------- | --------------- | | `aliasName` | `string` | | `targetType` | `TypeSignature` | | `templateDecl` | `Node?` | *** ## `UsingDeclarationNode` *(extends `DeclarationNode`)* [Section titled “UsingDeclarationNode (extends DeclarationNode)”](#usingdeclarationnode-extends-declarationnode) *kind discriminant: `"UsingDeclaration"`* | Field | Type | | ------------ | --------------- | | `aliasName` | `string` | | `targetType` | `TypeSignature` | *** ## `EnumNode` *(extends `DeclarationNode`)* [Section titled “EnumNode (extends DeclarationNode)”](#enumnode-extends-declarationnode) *kind discriminant: `"EnumeratorSpecifier"`* | Field | Type | | ---------------------- | ----------------- | | `identifier` | `IdentifierNode?` | | `underlyingType` | `TypeSignature` | | `isScoped` | `boolean` | | `isForwardDeclaration` | `boolean` | | `enumerators` | `Node[]` | *** ## `EnumSpecifierNode` *(extends `DeclarationNode`)* [Section titled “EnumSpecifierNode (extends DeclarationNode)”](#enumspecifiernode-extends-declarationnode) *kind discriminant: `"Enumerator"`* | Field | Type | | ------- | -------- | | `name` | `string` | | `value` | `Node?` | *** ## `VariableNode` *(extends `DeclarationNode`)* [Section titled “VariableNode (extends DeclarationNode)”](#variablenode-extends-declarationnode) *kind discriminant: `"Variable"`* | Field | Type | | --------------- | ----------------- | | `isStatic` | `boolean` | | `isConstexpr` | `boolean` | | `isThreadLocal` | `boolean` | | `isInline` | `boolean` | | `isExtern` | `boolean` | | `isConstinit` | `boolean` | | `attributes` | `Attribute[]` | | `alignasExprs` | `string[]` | | `typeSignature` | `TypeSignature` | | `identifier` | `IdentifierNode?` | | `defaultValue` | `Node?` | *** ## `VariableGroupNode` *(extends `DeclarationNode`)* [Section titled “VariableGroupNode (extends DeclarationNode)”](#variablegroupnode-extends-declarationnode) *kind discriminant: `"VariableGroup"`* | Field | Type | | ----------- | ---------------- | | `variables` | `VariableNode[]` | *** ## `StructuredBindingNode` *(extends `DeclarationNode`)* [Section titled “StructuredBindingNode (extends DeclarationNode)”](#structuredbindingnode-extends-declarationnode) *kind discriminant: `"StructuredBinding"`* | Field | Type | | --------------- | --------------- | | `names` | `string[]` | | `typeSignature` | `TypeSignature` | | `initializer` | `Node?` | | `isStatic` | `boolean` | | `isConstexpr` | `boolean` | | `isConstinit` | `boolean` | | `isThreadLocal` | `boolean` | | `isInline` | `boolean` | *** ## `ConceptNode` *(extends `DeclarationNode`)* [Section titled “ConceptNode (extends DeclarationNode)”](#conceptnode-extends-declarationnode) *kind discriminant: `"Concept"`* | Field | Type | | ---------------- | ----------------- | | `identifier` | `IdentifierNode?` | | `constraintExpr` | `Node?` | | `templateDecl` | `Node?` | *** ## `FunctionNode` *(extends `DeclarationNode`)* [Section titled “FunctionNode (extends DeclarationNode)”](#functionnode-extends-declarationnode) *kind discriminant: `"Function"`* | Field | Type | | ------------------- | --------------------- | | `isStatic` | `boolean` | | `isConst` | `boolean` | | `isVolatile` | `boolean` | | `isVirtual` | `boolean` | | `isPureVirtual` | `boolean` | | `isOverride` | `boolean` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `isFinal` | `boolean` | | `isInline` | `boolean` | | `isConstexpr` | `boolean` | | `isConsteval` | `boolean` | | `isExplicit` | `boolean` | | `explicitCondition` | `Node?` | | `isDefaulted` | `boolean` | | `isDeleted` | `boolean` | | `isTrailingReturn` | `boolean` | | `refQualifier` | `RefQualifier` | | `requiresClause` | `Node?` | | `body` | `BlockNode?` | | `attributes` | `Attribute[]` | | `returnSignature` | `TypeSignature` | | `identifier` | `IdentifierNode?` | | `parameters` | `FunctionParameter[]` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | *** ## `ConstructorNode` *(extends `DeclarationNode`)* [Section titled “ConstructorNode (extends DeclarationNode)”](#constructornode-extends-declarationnode) *kind discriminant: `"Constructor"`* | Field | Type | | ------------------- | --------------------- | | `isExplicit` | `boolean` | | `explicitCondition` | `Node?` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `isDefaulted` | `boolean` | | `isDeleted` | `boolean` | | `isConstexpr` | `boolean` | | `isInline` | `boolean` | | `isCopyConstructor` | `boolean` | | `isMoveConstructor` | `boolean` | | `requiresClause` | `Node?` | | `body` | `BlockNode?` | | `attributes` | `Attribute[]` | | `identifier` | `IdentifierNode?` | | `parameters` | `FunctionParameter[]` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | *** ## `DestructorNode` *(extends `DeclarationNode`)* [Section titled “DestructorNode (extends DeclarationNode)”](#destructornode-extends-declarationnode) *kind discriminant: `"Destructor"`* | Field | Type | | ------------------- | ----------------- | | `isVirtual` | `boolean` | | `isPureVirtual` | `boolean` | | `isDefaulted` | `boolean` | | `isDeleted` | `boolean` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `isInline` | `boolean` | | `isConstexpr` | `boolean` | | `requiresClause` | `Node?` | | `body` | `BlockNode?` | | `attributes` | `Attribute[]` | | `identifier` | `IdentifierNode?` | *** ## `OperatorNode` *(extends `DeclarationNode`)* [Section titled “OperatorNode (extends DeclarationNode)”](#operatornode-extends-declarationnode) *kind discriminant: `"Operator"`* | Field | Type | | ------------------- | --------------------- | | `isStatic` | `boolean` | | `isConst` | `boolean` | | `isVolatile` | `boolean` | | `isVirtual` | `boolean` | | `isPureVirtual` | `boolean` | | `isOverride` | `boolean` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `isFinal` | `boolean` | | `isInline` | `boolean` | | `isConstexpr` | `boolean` | | `isExplicit` | `boolean` | | `explicitCondition` | `Node?` | | `isDefaulted` | `boolean` | | `isDeleted` | `boolean` | | `isTrailingReturn` | `boolean` | | `refQualifier` | `RefQualifier` | | `requiresClause` | `Node?` | | `body` | `BlockNode?` | | `attributes` | `Attribute[]` | | `operatorSymbol` | `string` | | `returnSignature` | `TypeSignature` | | `castTargetType` | `TypeSignature` | | `parameters` | `FunctionParameter[]` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | *** ## `UnionNode` *(extends `DeclarationNode`)* [Section titled “UnionNode (extends DeclarationNode)”](#unionnode-extends-declarationnode) *kind discriminant: `"Union"`* | Field | Type | | ----------------------- | -------------------- | | `isAnonymous` | `boolean` | | `isForwardDeclaration` | `boolean` | | `attributes` | `Attribute[]` | | `alignasExprs` | `string[]` | | `identifier` | `IdentifierNode?` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | | `memberVariables` | `Node[]` | | `memberFunctions` | `Node[]` | | `staticMemberVariables` | `Node[]` | | `staticMemberFunctions` | `Node[]` | | `constructors` | `Node[]` | | `destructors` | `Node[]` | | `operators` | `Node[]` | | `nestedTypes` | `Node[]` | *** ## `FriendNode` *(extends `DeclarationNode`)* [Section titled “FriendNode (extends DeclarationNode)”](#friendnode-extends-declarationnode) *kind discriminant: `"Friend"`* | Field | Type | | ------------ | ----------------- | | `kind` | `string` | | `identifier` | `IdentifierNode?` | *** ## `StructNode` *(extends `DeclarationNode`)* [Section titled “StructNode (extends DeclarationNode)”](#structnode-extends-declarationnode) *kind discriminant: `"Struct"`* | Field | Type | | ----------------------- | ----------------------- | | `isFinal` | `boolean` | | `isForwardDeclaration` | `boolean` | | `attributes` | `Attribute[]` | | `alignasExprs` | `string[]` | | `identifier` | `IdentifierNode?` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | | `baseClasses` | `[?, IdentifierNode][]` | | `derivedClasses` | `string[]` | | `memberVariables` | `Node[]` | | `memberFunctions` | `Node[]` | | `staticMemberVariables` | `Node[]` | | `staticMemberFunctions` | `Node[]` | | `constructors` | `Node[]` | | `destructors` | `Node[]` | | `operators` | `Node[]` | | `friends` | `Node[]` | | `nestedTypes` | `Node[]` | | `statements` | `Node[]` | *** ## `ClassNode` *(extends `DeclarationNode`)* [Section titled “ClassNode (extends DeclarationNode)”](#classnode-extends-declarationnode) *kind discriminant: `"Class"`* | Field | Type | | ----------------------- | ----------------------- | | `isFinal` | `boolean` | | `isForwardDeclaration` | `boolean` | | `attributes` | `Attribute[]` | | `alignasExprs` | `string[]` | | `identifier` | `IdentifierNode?` | | `templateDecl` | `Node?` | | `templateArgs` | `TemplateArgument[]` | | `baseClasses` | `[?, IdentifierNode][]` | | `derivedClasses` | `string[]` | | `memberVariables` | `[?, Node][]` | | `memberFunctions` | `[?, Node][]` | | `staticMemberVariables` | `[?, Node][]` | | `staticMemberFunctions` | `[?, Node][]` | | `constructors` | `[?, Node][]` | | `destructors` | `[?, Node][]` | | `operators` | `[?, Node][]` | | `friends` | `[?, Node][]` | | `nestedTypes` | `[?, Node][]` | | `statements` | `Node[]` | *** ## `ModuleNode` *(extends `DeclarationNode`)* [Section titled “ModuleNode (extends DeclarationNode)”](#modulenode-extends-declarationnode) *kind discriminant: `"Module"`* | Field | Type | | ------------------- | --------- | | `moduleName` | `string` | | `partition` | `string` | | `isExported` | `boolean` | | `isGlobalFragment` | `boolean` | | `isPrivateFragment` | `boolean` | | `children` | `Node[]` | *** ## `ModuleImportNode` *(extends `DeclarationNode`)* [Section titled “ModuleImportNode (extends DeclarationNode)”](#moduleimportnode-extends-declarationnode) *kind discriminant: `"ModuleImport"`* | Field | Type | | ------------ | --------- | | `moduleName` | `string` | | `partition` | `string` | | `header` | `string` | | `isSystem` | `boolean` | | `isExported` | `boolean` | *** ## `ExternCNode` *(extends `DeclarationNode`)* [Section titled “ExternCNode (extends DeclarationNode)”](#externcnode-extends-declarationnode) *kind discriminant: `"ExternC"`* | Field | Type | | ---------- | --------- | | `language` | `string` | | `isBlock` | `boolean` | | `children` | `Node[]` | *** ## `ExportDeclarationNode` *(extends `DeclarationNode`)* [Section titled “ExportDeclarationNode (extends DeclarationNode)”](#exportdeclarationnode-extends-declarationnode) *kind discriminant: `"ExportDeclaration"`* | Field | Type | | ---------- | -------- | | `children` | `Node[]` | *** # Expression Nodes ## `LiteralNode` *(extends `ExpressionNode`)* [Section titled “LiteralNode (extends ExpressionNode)”](#literalnode-extends-expressionnode) *kind discriminant: `"Literal"`* | Field | Type | | ----------- | -------- | | `udlSuffix` | `string` | *** ## `NumberLiteralNode` *(extends `LiteralNode`)* [Section titled “NumberLiteralNode (extends LiteralNode)”](#numberliteralnode-extends-literalnode) *kind discriminant: `"Literal"`* | Field | Type | | ---------- | ---------------- | | `base` | `NumberBase` | | `category` | `NumberCategory` | | `value` | `string` | | `suffix` | `string` | *** ## `StringLiteralNode` *(extends `LiteralNode`)* [Section titled “StringLiteralNode (extends LiteralNode)”](#stringliteralnode-extends-literalnode) *kind discriminant: `"Literal"`* | Field | Type | | ------------- | ---------------- | | `encoding` | `StringEncoding` | | `isRaw` | `boolean` | | `isMultiLine` | `boolean` | | `value` | `string` | *** ## `ConcatenatedStringNode` *(extends `LiteralNode`)* [Section titled “ConcatenatedStringNode (extends LiteralNode)”](#concatenatedstringnode-extends-literalnode) *kind discriminant: `"Literal"`* | Field | Type | | ------- | --------------- | | `parts` | `LiteralNode[]` | *** ## `CharLiteralNode` *(extends `LiteralNode`)* [Section titled “CharLiteralNode (extends LiteralNode)”](#charliteralnode-extends-literalnode) *kind discriminant: `"Literal"`* | Field | Type | | ---------- | ---------------- | | `encoding` | `StringEncoding` | | `value` | `string` | *** ## `BoolLiteralNode` *(extends `LiteralNode`)* [Section titled “BoolLiteralNode (extends LiteralNode)”](#boolliteralnode-extends-literalnode) *kind discriminant: `"Literal"`* | Field | Type | | ------- | --------- | | `value` | `boolean` | *** ## `NullptrLiteralNode` *(extends `LiteralNode`)* [Section titled “NullptrLiteralNode (extends LiteralNode)”](#nullptrliteralnode-extends-literalnode) *kind discriminant: `"Literal"`* *No own serialized fields.* *** ## `PointerExpressionNode` *(extends `ExpressionNode`)* [Section titled “PointerExpressionNode (extends ExpressionNode)”](#pointerexpressionnode-extends-expressionnode) *kind discriminant: `"PointerExpression"`* | Field | Type | | --------- | --------------- | | `op` | `PointerExprOp` | | `operand` | `Node?` | *** ## `FieldExpressionNode` *(extends `ExpressionNode`)* [Section titled “FieldExpressionNode (extends ExpressionNode)”](#fieldexpressionnode-extends-expressionnode) *kind discriminant: `"FieldExpression"`* | Field | Type | | -------- | --------------- | | `object` | `Node?` | | `op` | `FieldAccessOp` | | `member` | `string` | *** ## `SubscriptExpressionNode` *(extends `ExpressionNode`)* [Section titled “SubscriptExpressionNode (extends ExpressionNode)”](#subscriptexpressionnode-extends-expressionnode) *kind discriminant: `"SubscriptExpression"`* | Field | Type | | -------- | ------- | | `object` | `Node?` | | `index` | `Node?` | *** ## `ParenthesizedExpressionNode` *(extends `ExpressionNode`)* [Section titled “ParenthesizedExpressionNode (extends ExpressionNode)”](#parenthesizedexpressionnode-extends-expressionnode) *kind discriminant: `"ParenthesizedExpression"`* | Field | Type | | ------- | ------- | | `inner` | `Node?` | *** ## `UnaryExpressionNode` *(extends `ExpressionNode`)* [Section titled “UnaryExpressionNode (extends ExpressionNode)”](#unaryexpressionnode-extends-expressionnode) *kind discriminant: `"UnaryExpression"`* | Field | Type | | --------- | -------- | | `op` | `string` | | `operand` | `Node?` | *** ## `BinaryExpressionNode` *(extends `ExpressionNode`)* [Section titled “BinaryExpressionNode (extends ExpressionNode)”](#binaryexpressionnode-extends-expressionnode) *kind discriminant: `"BinaryExpression"`* | Field | Type | | ----- | -------- | | `lhs` | `Node?` | | `op` | `string` | | `rhs` | `Node?` | *** ## `UpdateExpressionNode` *(extends `ExpressionNode`)* [Section titled “UpdateExpressionNode (extends ExpressionNode)”](#updateexpressionnode-extends-expressionnode) *kind discriminant: `"UpdateExpression"`* | Field | Type | | ---------- | --------- | | `op` | `string` | | `isPrefix` | `boolean` | | `operand` | `Node?` | *** ## `NewExpressionNode` *(extends `ExpressionNode`)* [Section titled “NewExpressionNode (extends ExpressionNode)”](#newexpressionnode-extends-expressionnode) *kind discriminant: `"NewExpression"`* | Field | Type | | ----------------- | --------------- | | `typeSignature` | `TypeSignature` | | `isArray` | `boolean` | | `arraySize` | `Node?` | | `placementArgs` | `Node[]` | | `constructorArgs` | `Node[]` | *** ## `DeleteExpressionNode` *(extends `ExpressionNode`)* [Section titled “DeleteExpressionNode (extends ExpressionNode)”](#deleteexpressionnode-extends-expressionnode) *kind discriminant: `"DeleteExpression"`* | Field | Type | | --------- | --------- | | `isArray` | `boolean` | | `operand` | `Node?` | *** ## `CastExpressionNode` *(extends `ExpressionNode`)* [Section titled “CastExpressionNode (extends ExpressionNode)”](#castexpressionnode-extends-expressionnode) *kind discriminant: `"CastExpression"`* | Field | Type | | ------------ | --------------- | | `castKind` | `CastKind` | | `targetType` | `TypeSignature` | | `operand` | `Node?` | *** ## `IntrospectionExpressionNode` *(extends `ExpressionNode`)* [Section titled “IntrospectionExpressionNode (extends ExpressionNode)”](#introspectionexpressionnode-extends-expressionnode) | Field | Type | | ------------- | --------------- | | `isTypeForm` | `boolean` | | `typeOperand` | `TypeSignature` | | `exprOperand` | `Node?` | *** ## `SizeofExpressionNode` *(extends `IntrospectionExpressionNode`)* [Section titled “SizeofExpressionNode (extends IntrospectionExpressionNode)”](#sizeofexpressionnode-extends-introspectionexpressionnode) *kind discriminant: `"SizeofExpression"`* *No own serialized fields.* *** ## `AlignofExpressionNode` *(extends `IntrospectionExpressionNode`)* [Section titled “AlignofExpressionNode (extends IntrospectionExpressionNode)”](#alignofexpressionnode-extends-introspectionexpressionnode) *kind discriminant: `"AlignofExpression"`* *No own serialized fields.* *** ## `TypeidExpressionNode` *(extends `IntrospectionExpressionNode`)* [Section titled “TypeidExpressionNode (extends IntrospectionExpressionNode)”](#typeidexpressionnode-extends-introspectionexpressionnode) *kind discriminant: `"TypeidExpression"`* *No own serialized fields.* *** ## `DecltypeExpressionNode` *(extends `IntrospectionExpressionNode`)* [Section titled “DecltypeExpressionNode (extends IntrospectionExpressionNode)”](#decltypeexpressionnode-extends-introspectionexpressionnode) *kind discriminant: `"DecltypeExpression"`* *No own serialized fields.* *** ## `CallExpressionNode` *(extends `ExpressionNode`)* [Section titled “CallExpressionNode (extends ExpressionNode)”](#callexpressionnode-extends-expressionnode) *kind discriminant: `"CallExpression"`* | Field | Type | | ------------------ | -------------------- | | `callKind` | `CallKind` | | `callee` | `Node?` | | `calleeIdentifier` | `IdentifierNode?` | | `args` | `FunctionArgument[]` | *** ## `AssignmentExpressionNode` *(extends `ExpressionNode`)* [Section titled “AssignmentExpressionNode (extends ExpressionNode)”](#assignmentexpressionnode-extends-expressionnode) *kind discriminant: `"AssignmentExpression"`* | Field | Type | | ----- | -------- | | `lhs` | `Node?` | | `op` | `string` | | `rhs` | `Node?` | *** ## `ConditionalExpressionNode` *(extends `ExpressionNode`)* [Section titled “ConditionalExpressionNode (extends ExpressionNode)”](#conditionalexpressionnode-extends-expressionnode) *kind discriminant: `"ConditionalExpression"`* | Field | Type | | ----------- | ------- | | `condition` | `Node?` | | `thenExpr` | `Node?` | | `elseExpr` | `Node?` | *** ## `LambdaExpressionNode` *(extends `ExpressionNode`)* [Section titled “LambdaExpressionNode (extends ExpressionNode)”](#lambdaexpressionnode-extends-expressionnode) *kind discriminant: `"LambdaExpression"`* | Field | Type | | -------------------- | --------------------- | | `captureDefault` | `number` | | `captures` | `LambdaCaptureItem[]` | | `templateParameters` | `TemplateParameter[]` | | `parameters` | `FunctionParameter[]` | | `trailingReturn` | `TypeSignature` | | `isMutable` | `boolean` | | `isNoexcept` | `boolean` | | `noexceptCondition` | `Node?` | | `body` | `BlockNode?` | *** ## `InitializerListNode` *(extends `ExpressionNode`)* [Section titled “InitializerListNode (extends ExpressionNode)”](#initializerlistnode-extends-expressionnode) *kind discriminant: `"InitializerList"`* | Field | Type | | ---------- | -------- | | `elements` | `Node[]` | *** ## `FoldExpressionNode` *(extends `ExpressionNode`)* [Section titled “FoldExpressionNode (extends ExpressionNode)”](#foldexpressionnode-extends-expressionnode) *kind discriminant: `"FoldExpression"`* | Field | Type | | -------------- | -------- | | `op` | `string` | | `leftOperand` | `Node?` | | `rightOperand` | `Node?` | *** ## `ThrowExpressionNode` *(extends `ExpressionNode`)* [Section titled “ThrowExpressionNode (extends ExpressionNode)”](#throwexpressionnode-extends-expressionnode) *kind discriminant: `"ThrowExpression"`* | Field | Type | | --------- | ------- | | `operand` | `Node?` | *** ## `NoexceptExpressionNode` *(extends `ExpressionNode`)* [Section titled “NoexceptExpressionNode (extends ExpressionNode)”](#noexceptexpressionnode-extends-expressionnode) *kind discriminant: `"NoexceptExpression"`* | Field | Type | | --------- | ------- | | `operand` | `Node?` | *** ## `ThisExpressionNode` *(extends `ExpressionNode`)* [Section titled “ThisExpressionNode (extends ExpressionNode)”](#thisexpressionnode-extends-expressionnode) *kind discriminant: `"ThisExpression"`* *No own serialized fields.* *** ## `CoYieldExpressionNode` *(extends `ExpressionNode`)* [Section titled “CoYieldExpressionNode (extends ExpressionNode)”](#coyieldexpressionnode-extends-expressionnode) *kind discriminant: `"CoYieldExpression"`* | Field | Type | | --------- | ------- | | `operand` | `Node?` | *** ## `CoAwaitExpressionNode` *(extends `ExpressionNode`)* [Section titled “CoAwaitExpressionNode (extends ExpressionNode)”](#coawaitexpressionnode-extends-expressionnode) *kind discriminant: `"CoAwaitExpression"`* | Field | Type | | --------- | ------- | | `operand` | `Node?` | *** ## `CommaExpressionNode` *(extends `ExpressionNode`)* [Section titled “CommaExpressionNode (extends ExpressionNode)”](#commaexpressionnode-extends-expressionnode) *kind discriminant: `"CommaExpression"`* | Field | Type | | ----- | ------- | | `lhs` | `Node?` | | `rhs` | `Node?` | *** ## `RequirementNode` *(extends `Node`)* [Section titled “RequirementNode (extends Node)”](#requirementnode-extends-node) *No own serialized fields.* *** ## `SimpleRequirementNode` *(extends `RequirementNode`)* [Section titled “SimpleRequirementNode (extends RequirementNode)”](#simplerequirementnode-extends-requirementnode) *kind discriminant: `"SimpleRequirement"`* | Field | Type | | ------------ | ------- | | `expression` | `Node?` | *** ## `TypeRequirementNode` *(extends `RequirementNode`)* [Section titled “TypeRequirementNode (extends RequirementNode)”](#typerequirementnode-extends-requirementnode) *kind discriminant: `"TypeRequirement"`* | Field | Type | | ---------- | --------------- | | `typeName` | `TypeSignature` | *** ## `CompoundRequirementNode` *(extends `RequirementNode`)* [Section titled “CompoundRequirementNode (extends RequirementNode)”](#compoundrequirementnode-extends-requirementnode) *kind discriminant: `"CompoundRequirement"`* | Field | Type | | ---------------------- | --------------- | | `expression` | `Node?` | | `isNoexcept` | `boolean` | | `returnTypeConstraint` | `TypeSignature` | *** ## `RequiresExpressionNode` *(extends `ExpressionNode`)* [Section titled “RequiresExpressionNode (extends ExpressionNode)”](#requiresexpressionnode-extends-expressionnode) *kind discriminant: `"RequiresExpression"`* | Field | Type | | -------------- | --------------------- | | `parameters` | `FunctionParameter[]` | | `requirements` | `RequirementNode[]` | *** # Preprocessor Nodes ## `IncludeNode` *(extends `PreprocessorNode`)* [Section titled “IncludeNode (extends PreprocessorNode)”](#includenode-extends-preprocessornode) *kind discriminant: `"IncludeDirective"`* | Field | Type | | ---------- | --------- | | `path` | `string` | | `isSystem` | `boolean` | *** ## `ObjectLikeMacroNode` *(extends `PreprocessorNode`)* [Section titled “ObjectLikeMacroNode (extends PreprocessorNode)”](#objectlikemacronode-extends-preprocessornode) *kind discriminant: `"ObjectLikeMacro"`* | Field | Type | | ------ | -------- | | `name` | `string` | | `body` | `string` | *** ## `FunctionLikeMacroNode` *(extends `PreprocessorNode`)* [Section titled “FunctionLikeMacroNode (extends PreprocessorNode)”](#functionlikemacronode-extends-preprocessornode) *kind discriminant: `"FunctionLikeMacro"`* | Field | Type | | ------------ | ------------------ | | `name` | `string` | | `body` | `string` | | `parameters` | `MacroParameter[]` | *** ## `PragmaNode` *(extends `PreprocessorNode`)* [Section titled “PragmaNode (extends PreprocessorNode)”](#pragmanode-extends-preprocessornode) *kind discriminant: `"Pragma"`* | Field | Type | | --------------- | ---------- | | `pragmaKind` | `Unknown` | | `rawArg` | `string` | | `packAction` | `string` | | `packAlignment` | `number` | | `packLabel` | `string` | | `warningAction` | `string` | | `warningCodes` | `number[]` | | `messageText` | `string` | | `regionName` | `string` | | `stdcSetting` | `string` | | `stdcValue` | `string` | | `commentType` | `string` | | `commentValue` | `string` | *** # Statement Nodes ## `BlockNode` *(extends `StatementNode`)* [Section titled “BlockNode (extends StatementNode)”](#blocknode-extends-statementnode) *kind discriminant: `"Block"`* | Field | Type | | ------------ | -------- | | `statements` | `Node[]` | *** ## `IfNode` *(extends `StatementNode`)* [Section titled “IfNode (extends StatementNode)”](#ifnode-extends-statementnode) *kind discriminant: `"IfStatement"`* | Field | Type | | --------------- | ------------ | | `isConstexpr` | `boolean` | | `initStatement` | `Node?` | | `condition` | `Node?` | | `thenBody` | `BlockNode?` | | `elseBody` | `Node?` | *** ## `SwitchNode` *(extends `StatementNode`)* [Section titled “SwitchNode (extends StatementNode)”](#switchnode-extends-statementnode) *kind discriminant: `"SwitchStatement"`* | Field | Type | | --------------- | -------- | | `initStatement` | `Node?` | | `condition` | `Node?` | | `cases` | `Node[]` | *** ## `CaseLabelNode` *(extends `StatementNode`)* [Section titled “CaseLabelNode (extends StatementNode)”](#caselabelnode-extends-statementnode) *kind discriminant: `"CaseLabel"`* | Field | Type | | ------------ | -------- | | `value` | `Node?` | | `statements` | `Node[]` | *** ## `BreakNode` *(extends `StatementNode`)* [Section titled “BreakNode (extends StatementNode)”](#breaknode-extends-statementnode) *kind discriminant: `"BreakStatement"`* *No own serialized fields.* *** ## `ContinueNode` *(extends `StatementNode`)* [Section titled “ContinueNode (extends StatementNode)”](#continuenode-extends-statementnode) *kind discriminant: `"ContinueStatement"`* *No own serialized fields.* *** ## `ReturnNode` *(extends `StatementNode`)* [Section titled “ReturnNode (extends StatementNode)”](#returnnode-extends-statementnode) *kind discriminant: `"ReturnStatement"`* | Field | Type | | ------- | ------- | | `value` | `Node?` | *** ## `GotoNode` *(extends `StatementNode`)* [Section titled “GotoNode (extends StatementNode)”](#gotonode-extends-statementnode) *kind discriminant: `"GotoStatement"`* | Field | Type | | ------- | -------- | | `label` | `string` | *** ## `LabeledStatementNode` *(extends `StatementNode`)* [Section titled “LabeledStatementNode (extends StatementNode)”](#labeledstatementnode-extends-statementnode) *kind discriminant: `"LabeledStatement"`* | Field | Type | | ------- | -------- | | `label` | `string` | | `body` | `Node?` | *** ## `DefaultLabelNode` *(extends `StatementNode`)* [Section titled “DefaultLabelNode (extends StatementNode)”](#defaultlabelnode-extends-statementnode) *kind discriminant: `"DefaultLabel"`* | Field | Type | | ------------ | -------- | | `statements` | `Node[]` | *** ## `WhileNode` *(extends `StatementNode`)* [Section titled “WhileNode (extends StatementNode)”](#whilenode-extends-statementnode) *kind discriminant: `"WhileStatement"`* | Field | Type | | --------------- | ------------ | | `initStatement` | `Node?` | | `condition` | `Node?` | | `body` | `BlockNode?` | *** ## `DoWhileNode` *(extends `StatementNode`)* [Section titled “DoWhileNode (extends StatementNode)”](#dowhilenode-extends-statementnode) *kind discriminant: `"DoWhileStatement"`* | Field | Type | | ----------- | ------------ | | `condition` | `Node?` | | `body` | `BlockNode?` | *** ## `ForNode` *(extends `StatementNode`)* [Section titled “ForNode (extends StatementNode)”](#fornode-extends-statementnode) *kind discriminant: `"ForStatement"`* | Field | Type | | ----------- | ------------ | | `init` | `Node?` | | `condition` | `Node?` | | `update` | `Node?` | | `body` | `BlockNode?` | *** ## `ForRangeNode` *(extends `StatementNode`)* [Section titled “ForRangeNode (extends StatementNode)”](#forrangenode-extends-statementnode) *kind discriminant: `"ForRangeStatement"`* | Field | Type | | --------------- | ------------ | | `initStatement` | `Node?` | | `loopVar` | `Node?` | | `range` | `Node?` | | `body` | `BlockNode?` | *** ## `CatchClauseNode` *(extends `StatementNode`)* [Section titled “CatchClauseNode (extends StatementNode)”](#catchclausenode-extends-statementnode) *kind discriminant: `"CatchClause"`* | Field | Type | | ------------ | ------------ | | `isCatchAll` | `boolean` | | `parameter` | `Node?` | | `body` | `BlockNode?` | *** ## `TryNode` *(extends `StatementNode`)* [Section titled “TryNode (extends StatementNode)”](#trynode-extends-statementnode) *kind discriminant: `"TryStatement"`* | Field | Type | | -------------- | ------------------- | | `body` | `BlockNode?` | | `catchClauses` | `CatchClauseNode[]` | *** # Rule Anatomy > The files that make up a codegen rule, config, preamble, grouping, and handler, and the contract each must satisfy. A rule lives in `//` (the default `` is `.codegen/rules`, override with `-r`). The directory contains: The directory name *is* the rule name — it’s how the rule is referenced on the CLI (`-a RuleName`) and as the C++ attribute (`[[codegen::RuleName]]`). Companion files are named by their *role*, not the rule, so a directory always looks the same. A workspace-shared directory at `/../shared/` (e.g. `.codegen/shared/`) is loaded automatically: every `.luau` file there is concatenated in alphabetical filename order and prepended to each rule’s transform at execution time, and a `shared/.env` file becomes the baseline dotenv. By default every shared script is included; rules can narrow the set with `shared.include` / `shared.exclude` globs in their config (see [Config Schema Reference](/reference/config-schema/#shared)). This is the [Team-tier](/licensing/tiers/) shared-libraries feature; today the directory is loaded unconditionally and the licence gate ships in a future release. ## `config.yaml` [Section titled “config.yaml”](#configyaml) Controls engine behaviour for this rule. Minimum viable config: ```yaml version: 1 output: language: cpp ``` Full schema: see [Config Schema Reference](/reference/config-schema/). ## `transform.luau` — handler [Section titled “transform.luau — handler”](#transformluau--handler) Receives one matched entity as JSON, returns JSON describing the output. **Contract:** * Input: a JSON-encoded entity payload (the codex AST node with heavy fields stripped, plus synthetic `_namespaces`, `_registryId`, and `_outputPath`, plus a `params` key copied from the rule config). Decode with `json.decode(input)`. * `_outputPath` is the post-grouping output path (the `defaultPath` if no grouping script exists, or the value returned by grouping). * Output: `json.encode({ source = "...", inline = { {source = "..."}, ... }, includes = { "", "\"my/header.h\"" } })`. The `inline` and `includes` keys are optional. * A Lua `error()` skips this entity, emits `E006`, and continues. An *internal* LuaU error is fatal (exit 2). * The script is a pure function: no I/O, no globals carried between invocations (each call gets a fresh execution). ## `preamble.luau` — preamble [Section titled “preamble.luau — preamble”](#preambleluau--preamble) Required. Runs **once per unique (rule, outputFile) pair**; the result is later deduplicated by content across rules contributing to the same output file. **Contract:** * Input: a JSON-encoded context object: `{ language: "cpp", outputFile: "path/to/output.cpp", ruleName: "", params: , entities: [{registryId, qualifiedName, structName, namespaces, inputFile, includes}, ...] }`. * `entities` contains every entity the handler produced output for that’s bound to this output file, with handler-emitted `includes` exposed for conditional preamble logic. * Output: a raw string (not JSON), prepended to the output file once per `(rule, outputFile)` pair (deduplicated by content across rules contributing to the same file). Return `""` if you don’t need a preamble — but the file must still exist. * A Lua `error()` here aborts the entire run with `E007` (exit 1). ## `grouping.luau` — grouping [Section titled “grouping.luau — grouping”](#groupingluau--grouping) Optional. Runs **once per rule, before handlers fire**, with the list of candidate entities. Used to override each entity’s default output path (the 1:1 routing derived from the input header). **Contract:** * Input: `json.decode(input)` returns `{ params = , entities = [...] }`. Each entity has only the summary fields: * `registryId` (integer) * `qualifiedName` (string, e.g. `"app::Color"`) * `structName` (string, unqualified) * `namespaces` (array of strings) * `inputFile` (string, the source header path) * `defaultPath` (string, the 1:1 output path derived from the input header) * Output: `json.encode({ [""] = "path/to/output.cpp", ... })`. Keys are `registryId`s **stringified**; values are output paths. * Paths are resolved against the project root (the process working directory). A path that escapes the project root is rejected with `E005` and the entity is skipped. An entity missing from the returned map emits `E004` and is skipped. The run continues in both cases. ## `.env` — per-rule dotenv [Section titled “.env — per-rule dotenv”](#env--per-rule-dotenv) Optional. Standard dotenv format (`KEY=VALUE`, `#` comments, optional surrounding quotes on the value). Loaded automatically when present; entries override the shared `/../shared/.env` baseline. Surfaced to scripts via the env module when env capability is active. ## Triggering a rule [Section titled “Triggering a rule”](#triggering-a-rule) Two trigger mechanisms can fire a rule: **Attribute annotation** at declaration sites. Honoured for any node kind in `node_kinds` (default: `Struct` only): ```cpp struct [[codegen::RuleName]] MyStruct { /* ... */ }; ``` **Anchor comment** in the source. Useful for entities the C++ grammar doesn’t allow attributes on (enums, free functions in some contexts) and as the injection site for `inline` patches: ```cpp // [[codegen::generated::RuleName::namespace::EntityName]] enum class MyEnum { A, B, C }; ``` The third segment of the anchor is the *qualified name* of the target entity (namespace path included). Anchors are scanned per-file and matched against the AST to find the corresponding entity. See [Inline Injection](/rules/inline-injection/) for the on-disk anchor lifecycle. Key Takeaways * Up to five files in a rule directory: `config.yaml`, `transform.luau`, and `preamble.luau` are required; `grouping.luau` and `.env` are optional. * Handler is a pure function: JSON in, JSON out (`source`, optional `inline`, optional `includes`). Receives `_outputPath` (post-grouping route). * Preamble runs once per (rule, outputFile) pair; its return is a raw string, deduplicated by content per output file. Receives the entities bound to that file. * Grouping runs once per rule, before handlers, and rewrites output paths via a stringified-`registryId` map. * Shared scripts in `/../shared/` and a shared `.env` are loaded automatically. # Grouping Logic > Reference for grouping.luau scripts, input format, output contract, and routing patterns. See [Grouping & Fan-in](/concepts/grouping/) for the conceptual overview. This page is the contract reference. ## When it runs [Section titled “When it runs”](#when-it-runs) The grouping script runs **once per rule, before handler invocations**. The engine collects all candidate entities, calls grouping with that summary list, and uses the returned map to override each entity’s default output path. The handler then runs with the post-grouping paths available as `_outputPath`. This means grouping influences output routing but cannot access handler results. Grouping is purely a path-routing pass. ## Input [Section titled “Input”](#input) ```json { "params": { /* rule.params, copied from RuleName.config.yaml */ }, "entities": [ { "registryId": 42, "qualifiedName": "myns::MyStruct", "structName": "MyStruct", "namespaces": ["myns"], "inputFile": "include/myns/my_struct.hpp", "defaultPath": "include/myns/my_struct.g.cpp" } ] } ``` These are the **only** per-entity fields exposed to grouping. There is no `attributes` array, no full AST node, no `kind`, and no handler output (that hasn’t run yet). If you need the full AST for routing decisions, look the entity up via `get_node(entity.registryId)` from inside the handler instead, and route in grouping using only the summary fields. ## Output [Section titled “Output”](#output) ```json { "42": "generated/myns/output.cpp" } ``` * Keys are `registryId` values, **stringified** (`tostring(entity.registryId)` from Lua). * Values are output paths, resolved relative to the project root (the process working directory). * A path that resolves outside the project root emits `E005` and the entity is skipped. * An entity missing from the returned map emits `E004` and is skipped. Other entities continue normally; the run does not abort. ## Routing patterns [Section titled “Routing patterns”](#routing-patterns) | Pattern | Description | Example expression | | --------------- | ------------------------------------------------- | --------------------------------------------------------------------- | | 1:1 (default) | Entity → file derived from input header | omit `grouping.luau` | | Fan-in | All entities → one file | `"generated/all.md"` | | Namespace-based | Entity → file named after first namespace segment | `entity.namespaces[1] .. "/registry.cpp"` | | Path-based | Internal vs public split by `inputFile` substring | `entity.inputFile:find("internal/") and "internal.md" or "public.md"` | | Param-driven | Output dir varies by `params.target` | `data.params.target .. "/" .. entity.structName .. ".ts"` | ## Worked example: namespace fan-in [Section titled “Worked example: namespace fan-in”](#worked-example-namespace-fan-in) MyRule.grouping.luau ```lua return function(input) local data = json.decode(input) if not data then error("failed to decode input") end local result = {} for _, ent in ipairs(data.entities) do local top = ent.namespaces[1] or "global" result[tostring(ent.registryId)] = "generated/" .. top .. "/registry.cpp" end return json.encode(result) end ``` ## Ordering guarantees [Section titled “Ordering guarantees”](#ordering-guarantees) Within a single output file, entities appear in the order they were matched. Matching follows the order of headers passed to the engine, then declaration order within each header. If you need a different order (alphabetical by qualified name, for example), sort the entity list in the grouping script before assigning paths. Output assembly visits the entities in the order they appear in the post-grouping entity list. Key Takeaways * Grouping runs before handlers, with a summary view of each candidate entity (6 fields) plus `params`. * Output is a ` → path` map; missing keys skip that entity. * Paths must stay under the project root; escapes are rejected with `E005`. * Grouping cannot access handler output (not run yet); use the handler (with `get_node`) for AST-level routing decisions. # Inline Injection > How the inline field injects declarations back into original C++ headers at anchor comment sites. ## The problem it solves [Section titled “The problem it solves”](#the-problem-it-solves) For rules that generate function implementations (`.g.cpp`), you typically also need the declaration in the original header. Without inline injection you must add the declaration manually, which defeats the purpose of generation and breaks whenever the generated signature changes. Inline injection lets the rule emit both the implementation and the declaration, keeping them in sync automatically. ## How it works [Section titled “How it works”](#how-it-works) The `inline` field in the handler return value is a list of objects: ```lua return json.encode({ source = "std::string_view toString(Color e) { ... }", inline = { { source = "std::string_view toString(Color e);" } } }) ``` Each item supplies one chunk of source text. The engine looks for the anchor matching this entity’s `::` and writes one paired `:begin]]/:end]]` block per item at that site, in the order returned. N items ⇒ N consecutive blocks (see [Multiple `inline` items](#multiple-inline-items-per-entity) below). ## Anchor formats [Section titled “Anchor formats”](#anchor-formats) Two on-disk forms are recognised. You can write either form by hand; the engine will rewrite to the **paired** form on first run. ### Bare anchor (one line, write-by-hand) [Section titled “Bare anchor (one line, write-by-hand)”](#bare-anchor-one-line-write-by-hand) ```cpp // [[codegen::generated::RuleName::qualifiedName]] enum class Color { Red, Green, Blue }; ``` The anchor must be on its own line, immediately above the declaration. After a successful run, the engine **expands** this single line into a paired block (see below) — bare anchors don’t survive the first regeneration. ### Paired anchor (engine-managed) [Section titled “Paired anchor (engine-managed)”](#paired-anchor-engine-managed) ```cpp // [[codegen::generated::RuleName::qualifiedName:begin]] std::string_view toString(Color e); // [[codegen::generated::RuleName::qualifiedName:end]] enum class Color { Red, Green, Blue }; ``` Subsequent regenerations rewrite the **whole contiguous run** of `:begin]]/:end]]` blocks for this key, replacing it with one block per current `inline` item. Item count can grow or shrink between runs — the engine resizes the run accordingly. Within each block, the markers stay; only the inner text moves. Diff noise is bounded to the run. ## Multiple `inline` items per entity [Section titled “Multiple inline items per entity”](#multiple-inline-items-per-entity) When `inline` has N items, the engine emits N consecutive `:begin]]/:end]]` blocks at the anchor site, one block per item, in the order returned by the handler. For example: ```lua inline = { { source = "void start();" }, { source = "void stop();" }, { source = "bool isRunning();" }, } ``` writes: ```cpp // [[codegen::generated::RuleName::qualifiedName:begin]] void start(); // [[codegen::generated::RuleName::qualifiedName:end]] // [[codegen::generated::RuleName::qualifiedName:begin]] void stop(); // [[codegen::generated::RuleName::qualifiedName:end]] // [[codegen::generated::RuleName::qualifiedName:begin]] bool isRunning(); // [[codegen::generated::RuleName::qualifiedName:end]] ``` On re-run with a different item count, the run is collapsed or expanded in place. Reordering items in the script reorders the blocks on disk. If you need code at *different* sites in the source, use multiple anchors and multiple matching entities — one anchor per qualified name. Future: named slots? We’re considering an additive `inline = { slotName = "..." }` form that would let one entity target multiple distinct anchor sites — e.g. `// [[codegen::generated::Rule::Qual::publicApi]]` and `// [[codegen::generated::Rule::Qual::implDetails]]` — without forcing you to split the entity. If that fits your use case (or if it doesn’t), tell us in the [feedback form](https://tally.so/r/1AbdKM). ## What gets written [Section titled “What gets written”](#what-gets-written) For an entity with N `inline` items, the engine writes N consecutive blocks of the form: ```plaintext // [[codegen::generated::RuleName::qualifiedName:begin]] // [[codegen::generated::RuleName::qualifiedName:end]] ``` The C++ declaration the anchor sits above is left untouched — the engine only modifies the anchor block itself. Files are written via a `.codegen_tmp` rename for atomicity. If an entity returns `inline` but no anchor exists in the source, the engine emits diagnostic `W003` and skips the injection (the `.g.cpp` is still written). The run between the first `:begin]]` and last `:end]]` for a given key is treated as engine-owned: do not write hand-edits between markers. Anything you put there will be overwritten on the next run. ## When to omit `inline` [Section titled “When to omit inline”](#when-to-omit-inline) Rules that only emit `.g.cpp` content can omit the field or return an empty list: ```lua return json.encode({ source = impl }) -- or return json.encode({ source = impl, inline = {} }) ``` Key Takeaways * Bare anchors are write-by-hand convenience; the engine rewrites them on first run into one paired block per `inline` item. * N items ⇒ N consecutive `:begin]]/:end]]` blocks. The contract is “ordered list of injected blocks”, not “concatenated single block”. * Re-runs grow or shrink the run in place to match the current item count and order. * Missing anchors produce diagnostic `W003`, not a fatal error. # LuaU Sandbox > What the LuaU VM provides to rule scripts, what it withholds, and how to reason about rule safety. codegen embeds the LuaU VM (Roblox’s typed Lua variant with a JIT compiler) as the rule execution environment. Each rule invocation gets a fresh execution context with no shared state between calls. ## What rules always have access to [Section titled “What rules always have access to”](#what-rules-always-have-access-to) | API | Description | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `json.encode` / `json.decode` | JSON serialisation | | Native introspection API | `get_node`, `find_struct_by_name`, `struct_has_attribute`, `struct_has_method`, `struct_has_member`, `struct_has_default_constructor`, `get_base_classes`, `get_definition_header` — see [LuaU Globals](/reference/luau-globals/) | | LuaU stdlib subset | `table`, `string`, `math`, `ipairs`, `pairs`, `tostring`, `tonumber`, `error`, `pcall`, `xpcall`, `type`, `select`, `assert` | ## What rules cannot access by default [Section titled “What rules cannot access by default”](#what-rules-cannot-access-by-default) | Capability | Status | | ------------------------------------- | --------------------------------------------------- | | File system (`io.open`, `os.execute`) | Blocked — no `io` or `os` library | | Network | Blocked — opt in via `permissions.http.allowlist` | | OS environment variables | Blocked — opt in via `permissions.env.os_allowlist` | | `require` / module loading | Blocked — no module system in the sandbox | | `loadstring` / `loadfile` / FFI | Blocked | | Global state across rule invocations | Impossible — each call gets fresh state | | Inter-rule communication | Impossible — rules cannot reach each other | | Process spawning | Blocked | ## Why LuaU specifically [Section titled “Why LuaU specifically”](#why-luau-specifically) * **Typed:** LuaU adds gradual typing, allowing rule authors to annotate their scripts for better IDE support. * **Fast:** The JIT compiler keeps tight loops over large entity lists performant. * **Sandboxable:** The LuaU VM is designed for embedding with a restricted environment; the engine replaces the global table entirely. * **Auditable:** LuaU is a small language. A non-expert can read and audit a rule script in minutes. ## Error handling [Section titled “Error handling”](#error-handling) A handler that calls `error()` (or hits a Lua runtime error like indexing nil) skips that entity, emits `E006`, and the engine continues with remaining entities. An *internal* engine error (failed VM init, etc.) is fatal and exits 2. A preamble that errors aborts the whole run with `E007` (exit 1) — the preamble runs once per `(rule, outputFile)` pair after handlers, and there’s no per-entity granularity to fall back to. A grouping script that errors emits `E008` and the engine falls back to default 1:1 routing for that rule’s entities. ```lua return function(input) local ok, node = pcall(json.decode, input) if not ok or not node then error("failed to decode input: " .. tostring(node)) end -- ... end ``` Validate defensively at the top of every handler. The AST schema can evolve, and future engine versions may pass additional fields or change optional field presence. ## HTTP permission [Section titled “HTTP permission”](#http-permission) Add an allowlist to the rule’s config: ```yaml permissions: http: allowlist: - "https://schema-registry.internal.example.com/" ``` When the allowlist is non-empty, the engine injects `http.get(url)` into the sandbox. Each entry is matched against the URL’s **host** — exact (`example.com`), wildcard (`*.example.com`, subdomains only), or `*` for any host. A URL whose host is not on the allowlist raises an error and emits `E010`. Never add a domain to the allowlist for a rule you didn’t write and audit yourself — an allowlisted host gives that rule read access to your AST data. Wildcards (`*.example.com`) widen the trust boundary to every subdomain; use exact hosts when you can. ## Env permission [Section titled “Env permission”](#env-permission) Add an OS env allowlist or a dotenv file: ```yaml permissions: env: os_allowlist: - "BUILD_CHANNEL" ``` Or drop a `.env` next to the rule, or a shared `/../shared/.env`. Either path activates the env capability and exposes the values via the env module. See [Permissions](/rules/permissions/) for surface details. Key Takeaways * Each rule invocation gets fresh state — no shared globals, no inter-rule communication. * Always available: `json` + the native introspection API + a LuaU stdlib subset. * Network and OS env access are opt-in via `permissions:` in the rule config. * Handler errors skip the entity; preamble errors abort the run; grouping errors fall back to default routing. # Permissions Model > How to grant rules access to HTTP, OS environment variables, and dotenv values, and why the default is to grant nothing. ## Default: no I/O capabilities [Section titled “Default: no I/O capabilities”](#default-no-io-capabilities) Every rule runs with no I/O capabilities by default. The LuaU sandbox provides `json`, the native introspection API, and a stdlib subset (see [LuaU Globals](/reference/luau-globals/)). No filesystem, no network, no env, no process execution. Two opt-in capabilities can be granted in the rule’s config: HTTP and env. ## HTTP [Section titled “HTTP”](#http) MyRule.config.yaml ```yaml version: 1 output: language: cpp permissions: http: allowlist: - "schema-registry.example.com" - "*.api.example.com" ``` When the allowlist is non-empty, the engine exposes `http.get(url)` to all of the rule’s scripts. Each entry is matched against the URL’s **host component** — exact hosts (`example.com`), wildcard subdomains (`*.example.com`, which does not match the apex), or `*` for any host. Calls whose host is not on the allowlist raise an error and emit `E010`. The CLI flag `--allow-http ` (repeatable) appends entries at run time without editing the config. Allowlist a domain only for rules you’ve written or audited yourself — an allowlisted host gives that rule read access to your AST data and lets it exfiltrate it. Wildcards (`*.example.com`) widen the trust boundary to anything served under the suffix; use exact hosts when you can. ## Env [Section titled “Env”](#env) Three independent sources can populate the env capability: **OS env allowlist** — variables read directly from the host’s environment: ```yaml permissions: env: os_allowlist: - "BUILD_CHANNEL" - "GIT_COMMIT" ``` The CLI flag `--allow-env ` (repeatable) appends entries at run time. **Per-rule dotenv** at `//.env`. Standard format: `KEY=VALUE`, optional surrounding quotes, `#` line comments, blank lines ignored. Loaded automatically when present; no allowlist required. **Shared dotenv** at `/../shared/.env` (e.g. `.codegen/shared/.env`). Loaded as a baseline; per-rule entries with the same key override. The CLI flag `--env KEY=VALUE` (repeatable) overrides individual dotenv entries at run time. The merge order, lowest precedence to highest: shared `.env` → per-rule `.env` → CLI `--env`. The CLI `--allow-env` allowlist is unioned across sources. When any of these is non-empty the engine activates the env capability and exposes the values via the env module to the rule’s scripts. ## What the model intentionally excludes [Section titled “What the model intentionally excludes”](#what-the-model-intentionally-excludes) | Capability | Status | | ------------------------------------ | ------------------------------------------------------------ | | Direct filesystem read | Not provided | | Filesystem write | Not provided (engine writes outputs; rules only return text) | | Process spawning | Not provided, not planned | | Outbound HTTP methods other than GET | Not provided | If you need data from the filesystem, point an `http.get(...)` at a local file server, or pipe it through dotenv. The sandbox is intentionally narrow. Key Takeaways * Default: no I/O. HTTP and env are opt-in via `permissions:` in the rule config. * HTTP allowlist entries match the URL’s host (exact, `*.subdomain` wildcard, or `*` for any). * Env values can come from OS allowlist, per-rule `.env`, or shared `.env`; CLI flags override per-invocation. * Security review = read the rule’s config; the script can’t reach anything not declared there. # Writing Transforms > Practical patterns for handler scripts, type mapping, string building, defensive validation, and testing. ## Structure of a handler [Section titled “Structure of a handler”](#structure-of-a-handler) Every handler script follows the same pattern: ```lua return function(input) -- 1. Decode the entity payload local node = json.decode(input) if not node then error("failed to decode input") end -- 2. Extract what you need local name = node.identifier and node.identifier.name if not name or name == "" then error("entity has no name") end -- 3. Build the output text local source = "// generated for " .. name .. "\n" -- 4. Return encoded result return json.encode({ source = source }) end ``` The return shape recognised by the engine: `source` (string, written to the entity’s output file), optional `inline` (array of `{source = "..."}` objects injected at the anchor site), optional `includes` (array of include strings such as `""` or `"\"my/header.h\""`). ## Type mapping pattern [Section titled “Type mapping pattern”](#type-mapping-pattern) For rules that translate C++ types to another type system (TypeScript, Rust, protobuf, SQL): ```lua local TYPE_MAP = { ["bool"] = "boolean", ["int"] = "number", ["std::string"] = "string", -- add entries as needed } local function mapType(tSig) if not tSig or not tSig.identifier then return "unknown" end local name = tSig.identifier.name if TYPE_MAP[name] then return TYPE_MAP[name] end -- Handle generics recursively if name == "vector" then local args = tSig.identifier.templateArguments or {} return "Array<" .. mapType(args[1]) .. ">" end -- Fall back to the C++ name return name end ``` ## Handler payload [Section titled “Handler payload”](#handler-payload) The handler receives a full entity JSON with these synthetic fields injected: * `_namespaces` (array) — the qualifying namespace path * `_registryId` (integer) — stable entity ID * `_outputPath` (string) — the post-grouping output file path (after grouping, if present; otherwise the 1:1 default) * `params` (object) — rule config copied in Use `_outputPath` if your handler needs to know the final output location (e.g., to compute relative paths from the output file back to the input header). ## Namespace qualification [Section titled “Namespace qualification”](#namespace-qualification) Always build the qualified name from `_namespaces`; do not assume the unqualified name is unique: ```lua local function qualifiedName(node) local ns = table.concat(node._namespaces or {}, "::") local name = node.identifier.name return ns ~= "" and (ns .. "::" .. name) or name end ``` ## Building multi-line output [Section titled “Building multi-line output”](#building-multi-line-output) Use a lines table and `table.concat` — string concatenation in a loop is O(n²) in Lua: ```lua local lines = {} table.insert(lines, "void process(" .. qualifiedName(node) .. " const& v) {") for _, var in ipairs(node.memberVariables or {}) do table.insert(lines, " handle(v." .. var.identifier.name .. ");") end table.insert(lines, "}") local source = table.concat(lines, "\n") ``` ## Cross-entity lookup [Section titled “Cross-entity lookup”](#cross-entity-lookup) For rules that need to inspect more than the entity they were called for (base classes, sibling structs, methods on related types), use the [native introspection API](/reference/luau-globals/#native-introspection-api): ```lua -- Fetch base classes (returns JSON-encoded array) local baseJson = get_base_classes(node._registryId) if baseJson then for _, base in ipairs(json.decode(baseJson)) do -- base = { access, name, qualifiedName, id } if base.id then local sibling = json.decode(get_node(base.id)) -- sibling is the full node JSON for the base class end end end ``` ## Common mistakes [Section titled “Common mistakes”](#common-mistakes) **Forgetting to handle `VariableGroup`.** Declarations like `int x, y, z;` arrive as `kind = "VariableGroup"` with a `variables` array. Always handle both cases: ```lua for _, varNode in ipairs(node.memberVariables or {}) do if varNode.kind == "Variable" then processVar(varNode) elseif varNode.kind == "VariableGroup" then for _, v in ipairs(varNode.variables or {}) do processVar(v) end end end ``` **Returning plain text instead of JSON.** The handler must return `json.encode({...})`. Returning a plain string emits `E009`. **Reading `node.annotations`.** The actual key is `attributes` (`node.attributes[i].ns`, `node.attributes[i].name`, `node.attributes[i].arguments`). Codex uses C++ attribute terminology throughout. **Mutating the input table.** LuaU tables are shared by reference within a single invocation. Don’t mutate `node`; build new tables for output. ## Inspecting the JSON your script will receive [Section titled “Inspecting the JSON your script will receive”](#inspecting-the-json-your-script-will-receive) Use the `ast-dump` tool to dump a header in the codex JSON form: ```sh ast-dump -m codex -i include/my_header.hpp ``` The shape printed is the same one the handler decodes (minus the `_namespaces` and `_registryId` synthetic fields and minus the heavy `memberFunctions` / `staticMemberFunctions` / `constructors` / `destructors` / `operators` / `nestedTypes` / `statements` fields the engine strips before invocation). Key Takeaways * Handlers are pure functions: decode → extract → build → encode. No side effects. * Build qualified names from `_namespaces`; never assume uniqueness of unqualified names. * The codex JSON uses `attributes` (C++ terminology), not `annotations`. * Use `table.concat` for multi-line output; the native API for cross-entity lookup. * Return value must be `json.encode({source = "...", inline = {...}, includes = {...}})`.