Story: Close systemic codegen gaps
Table of Contents
This page documents a story in Sprint 25. It captures the goal, current status, acceptance criteria, and the tasks that compose it.
1. Goal
Close two systemic codegen gaps where no template exists at all, so a regenerated entity never needs hand-wiring afterwards:
- RLS policies: no template emits
ENABLE ROW LEVEL SECURITY/CREATE POLICYfor tables with a tenant or party scope column. Every policy today is hand-written into a per-component*_rls_policies_create.sqlfile; the model layer has no signal for which table wants which policy shape, so the step is manual, easy to forget, and propped up by 53RLS_001/RLS_002ignore entries. - Create/drop aggregator wiring: codegen writes each entity's
*_create.sql=/=_drop.sqlfile but the component aggregator's\\irinclude list is hand-maintained. A new entity's tables silently do not exist aftercompass db recreateuntil someone wires them in by hand; no validator catches a missed\\irline.
Both gaps get model-driven codegen ownership: an entity declares the policy shapes it wants, the generated per-entity SQL carries them, and reachability through the aggregator chain is enforced. ores.marketdata is the adoption-proof component; the other 16 components adopt the machinery inside their own drift stories.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent sprint | Sprint 25 |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-09-04 |
3. Acceptance
- An entity model can declare its RLS policy shapes (tenant isolation, party isolation, system-tenant-visible tenant isolation), as first-class variability features with catalogue entries and profile defaults.
- The per-entity SQL create emission carries the declared policies as a generated trailer, and the per-entity drop emission carries their drops, so a regenerated table owns its whole SQL lifecycle. The emitted blocks match the dominant hand-written block shapes verbatim.
- ores.marketdata is the adoption proof: its five tables' policies
migrate from
marketdata_rls_policies_create.sqlto the generated trailers, the now-empty hand file and its wiring are removed, and a second regeneration is byte-identical. - Reachability enforcement: a validation rule fails when a generated
SQL file is not
\\ir-reachable from its component aggregator chain (create and drop); it passes the current census with the deliberate exception classes encoded (RLS-layer files, top-level wired bundles). compass db recreate+validate_schemas.shstay green throughout; theRLS_001/RLS_002ignore entries for the migrated population are removed where the policy now exists, with the remainder audited and recorded per table.- The history-provider registrar task (second task) is untouched by this one.
4. Tasks
| Task | State | Start | End | Description |
|---|---|---|---|---|
| Codegen leaves new entities unwired: RLS policies and the create/drop aggregators | DONE | 2026-09-04 | 2026-09-04 | No template in ores.codegen emits ENABLE ROW LEVEL SECURITY/CREATE POLICY for entities with :has_tenant_id: true, and generated per-table SQL is never linked into the component's hand-maintained *_create.sql/_drop.sql aggregator (the \ir include list) – both are manual steps every entity author has to remember, and both were missed while adding the CRM config tables. |
| Codegen the history-provider registrar wiring | DONE | 2026-09-04 | 2026-09-04 | refdata's registrar.cpp is a monolithic hand-maintained file of near-identical register_history_provider() blocks per entity; this should be a codegen archetype instead. |
5. Decisions
- 2026-09-04 (task 2DFEF8F6): the per-entity
register_<entity>_history_provider()surface is a codegen archetype pair; the component registrar stays a hand-written assembler. A whole-fileregistrar.cpptemplate was rejected: the file is a true assembler (NATS wiring, queue groups, the function-localhistory_registry()static, inline no-sub-registrar handlers), not a per-entity shape, so templating it would force every future structural change through mustache for no per-entity gain. Registrar.cpp keeps its current 53 calls, so the registered key set is unchanged, and the 11 mapper-bearing entities without a provider stay unregistered with identical runtime behaviour until their own wiring work adds them. - 2026-09-04 (task 2DFEF8F6): emission follows physical-space
admission, the key follows the SQL side. The facet gates exactly
like the presentation facet it renders through:
#+model_types: domain_entity, default-enabled, so emission equals the mapper set by construction for any component that regenerates. The provider key is composedproduct.component.entity_singular, identically to the notify-trigger SQL facet'sentity_name, so the server-side dispatch key and the row-change side of the history pipeline cannot drift apart. The service method spelling comes fromentity_singular_short, the same model property that already drives the irregularget_type_history/get_centre_historyspellings on the generated services.