Skip to content

Rule Lifecycle

The engine reads <rulesDir>/<RuleName>/ for each -a <RuleName> on the CLI: handler, preamble, optional grouping, optional .env, and config.yaml (with extends: chasing as needed). It also loads <rulesDir>/../shared/*.luau (alphabetical by filename, concatenated) as the shared prelude and <rulesDir>/../shared/.env as the baseline dotenv. CLI overrides (-P, --env, --allow-http, --allow-env) are folded in.

The engine scans every input file (under -i) for // [[codegen::generated::<rule>::<qualifiedName>]] 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.

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::<RuleName>]] attributes and queues a match. It then merges in entities referenced only by anchor (no attribute) so they can also fire the handler.

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).

For each rule that ships a grouping.luau, the engine calls it once per invocation, with all candidate entities:

input: JSON { params: <rule.params>,
entities: [{ registryId, qualifiedName, structName, namespaces,
inputFile, defaultPath }, ...] }
output: JSON { "<registryId>": "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.

For each matched entity (with updated output path if grouping fired), the engine calls transform.luau once:

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).

For each rule, the engine runs preamble.luau once per unique output file (the pair of rule + outputPath):

input: JSON { language: "cpp", outputFile: "path/to/output.cpp", ruleName: "<RuleName>", params: <rule.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.

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 '<p>' claimed by rules .... C++ outputs are exempt by design: stitching multiple rules’ contributions into one .g.cpp is intended.

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).

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.

Under --dry-run, edits are collected (not applied) and shown alongside the assembly previews.

FailureEffect
Bad CLI / missing input / rule load failureExit 1 (with the matching E0xx).
Grouping errorSkip grouping for that rule, fall back to 1:1 paths, emit E008.
Handler error()Skip the entity, emit E006, continue.
Handler internal/VM errorExit 2 (E006, fatal).
Preamble errorExit 1 (E007). The whole run aborts.
Inline anchor missingPlain 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.