Codegen Fix Drift
Table of Contents
1. When to use this skill
A component's codegen entity models must reach zero drift: every entity
bound to a variability profile, regeneration byte-identical to the
checked-in output, and the drift check extended to that component.
Reach for this skill at the start of a per-component remediation story,
and again whenever a regeneration surfaces drift later. One component
per invocation; the composite components (ores.dq, ores.iam,
ores.trading) run it once per subcomponent task.
2. How to use this skill
Run the steps in order. No step is skippable, and partial codegen
coverage is not a valid end state. The drift check lives at
projects/ores.codegen/scripts/check_component_drift.py and runs in two
modes: --component <name> regenerates one component in place and
exits non-zero on any diff against the checked-in files or on any
untracked file the regeneration materializes; --all runs the same
check over the shared COMPONENTS_UNDER_TEST list in
component_registry.py, the set the
compass-pr-raise gate covers. Measure a component not yet at zero drift with
--component; it cannot pass --all until it joins the list.
This is the tool that measures, the surveys and regenerations below do
the work.
- Establish the baseline before any model edit. Regenerate the
component's models with the files untouched and diff against the
checked-in output. Whatever differs is pre-existing drift: record
it and revert the output. A later diff is attributable only against
this baseline. Both pilots found real codegen bugs this way, before
touching any model: the pilot's
currency_calendarhad stale includes and missing facets; the dq run'sorg_loader.pynever read thelookup_entityprimary-key heading and silently corrupted DDL for every lookup entity in the repo. - Survey every model file. Walk the component's modeling
directory; when two component entries share one directory (dq and
dq-cpp both use
projects/ores.dq/modeling), survey it once. Classify each file by metatype. Only entity metatypes carry variability:domain_entity,junction, and legacylookup_entity(migrate these todomain_entityrather than binding them, per the lookup-entity retirement).field_group,module, andcomponentare structurally not entities: record and exclude them with the reason, matching the pilot's precedent. - Compute resolved feature values. For each entity, read the
features the profile catalogue assigns and the entity's own
properties, and resolve defaults the way
org_loader.pydoes. Compare every profile-assigned feature against the entity's actual resolved value, not only the features the entity already sets: an unset feature that the profile fixes to a value is a real mismatch (the pilot'sleg_typeandbusiness_unit: one was a genuine gap, the other genuinely bespoke). The presentation half of a profile is inert when the entity has no** Presentationdrawer — match on theroot=/=sqlfeatures alone in that case. - Bind every matching entity. Replace the explicit per-entity
feature properties the profile covers with
:profile: <name>, and keep the structural fields and per-entity naming. An explicit per-entity property still overrides the profile's value where both are present. When no profile matches, record the entity with its actual feature values and the reason — never force the nearest profile onto it (thebusiness_unitprecedent: a forced bind would add a real, unwantedworkspace_idcolumn). A novel combination is a new-profile candidate only once a second entity repeats it. A deliberate-looking mismatch (a UI or product decision, e.g. pagination unset where the profile fixes it) is real data: record it and flag it for a product conversation. Never silently drop a no-match. - Regenerate; require byte-identical output. Process the entities
one at a time through a scripted, per-entity pipeline: regenerate
the unmodified file (the baseline from step 1), revert, apply the
bind, regenerate, and require the new diff to be an exact subset of
that baseline — zero additional diff — before keeping the edit;
otherwise revert and flag it. Any additional diff is a bug in the
profile, the matching, or a template: find the root cause in
org_loader.py, the template, or the archetype, fix it at the source, and add a regression test. Workarounds are not a fix. Regenerate a second time and require byte-identical output: the component's regeneration must be idempotent. - Review the generated code for C++ idiomaticity and smells. Read the regenerated output as you would a hand-written PR: naming, shape, and structure must match the component's best existing code (the stateless repository shape, the standard controller constructor order, the generic history dialog). A finding is a template bug: fix the template, never the generated output.
- Verify every materialized registrar family is composed. A bind
can materialize registrar families (messaging registrar, event
mapping, history provider) that compile and check in cleanly but
that nothing calls. The drift gate cannot see this: it compares
regeneration output, and an unwired function produces no diff.
ctest cannot see it either: the generated eventing tests register
the event mapping inside the test, so they never pass through the
production composition points. For every registrar family the
regeneration adds or rewrites, find the registration function and
confirm the component's composition points call it. The usual
composition points are the core
messaging/registrar.cppfan-out (NATS handlers and history providers; the refdata registrar is the canonical shape) and the serviceapplication.cppevent mappings. Trading hit this trap twice: the party_role_type and trade_type stacks were materialized but never registered, and the 2026-09-06 bind landed four registrar families that had no caller on main. Each family must end the run in one of two states: composed at the composition points, or recorded as a deliberately unwired subject with its reason (the compute orphaned-junction precedent). Do not leave a family unwired and unrecorded. - Extend the drift gate last. Add the component to the
COMPONENTS_UNDER_TESTlist in component_registry.py. Two things then runcheck_component_drift.py --allover that list: thecodegen-drift.ymlworkflow, which gates a PR onprojects/*/modeling/**andprojects/ores.codegen/**, and the compass-pr-raise skill's local codegen-drift check. The gate lands only on a component at verified zero drift: it enforces the method, it does not replace it. Run--allonce more before the PR that lands the gate.
2.1. Exception: an entity too complex to code-generate
Partial coverage is not an end state, but a genuinely too-complex-to-generate entity is a recorded exception, not a forced bind. An exception needs, per entity: the recorded justification (the feature values that make it too complex and why codegen cannot express them), and the entity stays visible to the drift check — no exclusion that hides a manual artefact from the gate. Revisit the justification when the systemic codegen-gap stories land; a gap closed upstream may make the entity regular after all.
2.2. Automation ramp (levels L0-L2)
Automation starts granular and rises only on evidence:
- Level 0 — manual, per fix. Every fix is verified by build, test, and byte-identical regen diff before its PR; PRs stay small and human-reviewable. This is the mandatory mode for the remediation stories.
- Level 1 — scripted verification, same granularity. Promote when one full component has passed through level 0 end-to-end, its drift gate is live on main, and every codegen bug the run surfaced was fixed at source with a regression test — no temporary workarounds outstanding. The pilot's scripted per-entity pipeline (baseline regen, bind, subset-diff) automates the verification only; commits stay one entity per logical change. The pilot itself ran at this level.
- Level 2 — batched within a component. Promote when a further component has completed level 1 with no template fix needed, i.e. the templates held steady across components and the systemic codegen-gap stories are closed. Then a component's remaining binds may be regenerated as one reviewed unit instead of one commit per entity.
3. Recipes
- (None yet. The ordered procedure above is self-contained; the worked records in Reference are the closest procedure documents.)
4. Reference
- Bind ores.refdata entities to profiles; verify zero-diff regen — the
pilot worked record: baseline-first attribution, per-entity
pipeline, profile-matching lessons,
business_unit=/=leg_typecases. - Bind ores.dq entities to profiles; verify zero-diff regen — the second worked record: composite-component survey, idempotency verification, deliberate-mismatch recording, no-match reasons.
- Bind ores.iam entities to profiles; verify zero-diff regen — the third worked record: SQL rule restorations through codegen, acceptance runs on a recreated database, and the live system test that surfaced the service-grant defect ctest cannot see.
- Bind ores.trading entities to profiles; verify zero-diff regen — the fourth worked record: baseline probe on an unmigrated component, lookup-entity retirement, recorded no-matches, the registry-refusal decision for a hand-authored estate, and the unwired-registrar trap that step 7 now checks for.
- Entity classification and drift baseline: ores.refdata pilot — the sprint 24 dimension settlement that grounds the survey.
- Entity classification and drift baseline across all components — the 2026-09-03 program decisions this skill encodes (foundations-first ordering, exception rule, automation principle).
- Variability Profiles — the named profiles entities bind to; the source of truth for profile-assigned features.