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.

  1. 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_calendar had stale includes and missing facets; the dq run's org_loader.py never read the lookup_entity primary-key heading and silently corrupted DDL for every lookup entity in the repo.
  2. 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 legacy lookup_entity (migrate these to domain_entity rather than binding them, per the lookup-entity retirement). field_group, module, and component are structurally not entities: record and exclude them with the reason, matching the pilot's precedent.
  3. 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.py does. 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's leg_type and business_unit: one was a genuine gap, the other genuinely bespoke). The presentation half of a profile is inert when the entity has no ** Presentation drawer — match on the root=/=sql features alone in that case.
  4. 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 (the business_unit precedent: a forced bind would add a real, unwanted workspace_id column). 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.
  5. 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.
  6. 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.
  7. 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.cpp fan-out (NATS handlers and history providers; the refdata registrar is the canonical shape) and the service application.cpp event 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.
  8. Extend the drift gate last. Add the component to the COMPONENTS_UNDER_TEST list in component_registry.py. Two things then run check_component_drift.py --all over that list: the codegen-drift.yml workflow, which gates a PR on projects/*/modeling/** and projects/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 --all once 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

Emacs 29.3 (Org mode 9.6.15)