Skip to content

Grouping Logic

See Grouping & Fan-in for the conceptual overview. This page is the contract reference.

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.

{
"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.
PatternDescriptionExample expression
1:1 (default)Entity → file derived from input headeromit grouping.luau
Fan-inAll entities → one file"generated/all.md"
Namespace-basedEntity → file named after first namespace segmententity.namespaces[1] .. "/registry.cpp"
Path-basedInternal vs public split by inputFile substringentity.inputFile:find("internal/") and "internal.md" or "public.md"
Param-drivenOutput dir varies by params.targetdata.params.target .. "/" .. entity.structName .. ".ts"
MyRule.grouping.luau
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

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 <stringified-registryId> → 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.