Rule Lifecycle
Phase 1: Rule load
Section titled “Phase 1: Rule load”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.
Phase 2: Anchor scan + AST build
Section titled “Phase 2: Anchor scan + AST build”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.
Phase 3: Entity match
Section titled “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::<RuleName>]] 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”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”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.
Phase 6: Handler
Section titled “Phase 6: Handler”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).
Phase 7: Preamble
Section titled “Phase 7: Preamble”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.
Phase 9: Collision check (non-cpp only)
Section titled “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 '<p>' 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”For each output path:
- Banner:
// Generated with Codegen - DO NOT EDIT!!! - For C++ outputs whose extension is a header (
.hpp/.h/.hxx/.hh):#pragma once. - Each contributing rule’s preamble result (one per rule), deduplicated by content.
- For C++ outputs: a self-include of the input header (rebased when the output is relocated), followed by the union of
includesfrom all contributing entities. WhenautoDeduceIncludes: true, project includes already covered by the input header’s transitive include graph are filtered out. - 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”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.
Error handling summary
Section titled “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. |
- 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.