Grouping Logic
See Grouping & Fan-in for the conceptual overview. This page is the contract reference.
When it runs
Section titled “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.
{ "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”{ "42": "generated/myns/output.cpp"}- Keys are
registryIdvalues, 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
E005and the entity is skipped. - An entity missing from the returned map emits
E004and is skipped. Other entities continue normally; the run does not abort.
Routing patterns
Section titled “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”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)endOrdering guarantees
Section titled “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.
- Grouping runs before handlers, with a summary view of each candidate entity (6 fields) plus
params. - Output is a
<stringified-registryId> → pathmap; 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.