field_legacy_nid is mandatory on every content-entity migration
Area: migration / process / DX Raised during: Session 2026-07-10 (post-1b.2, config-sync drift incident) Jira: (add when available) Priority: High — every future content migration (Texts, Sources, AV, Mandala Home) must follow this or repeat the incident below
The convention
Any migration whose destination is a top-level content entity (node, group,
and any future bundle that gets its own identity — not paragraphs, not
relationship/membership entities) must:
- Have a
field_legacy_nid(integer, optional) field on the destination bundle, added via CMI. - Map it in the migration's
processpipeline:field_legacy_nid: nid(or the appropriate source ID property if notnid). - Get verified post-import: row count in
{entity_type}__field_legacy_nidmust equal the migrated entity count, with 0 mismatches against the migration's ownmigrate_map_*table.
This is already established for Images (shanti_image nodes, collection/
subcollection groups). It must be repeated for every future site migration.
Why this matters beyond audit trail: field_legacy_nid is the source for
the planned uid_legacy_s kmassets Solr field (old→new uid compatibility
shim at cutover — see
kmassets-uid-identity-across-migration.md),
and it's the only durable D7→D11 identity mapping that survives rollback/
reimport (the migrate_map_* tables are migration-run-scoped and get wiped
on rollback).
The incident (2026-07-10)
field_legacy_nid: nid was correctly added to the mandala_migrations
module's config/install/*.yml for d7_images_shanti_image (2026-07-07,
commit a6c9e78) and for the new d7_images_collections /
d7_images_subcollections migrations (1b.2, PR #27). But config/install
is a module's default configuration — Drupal only reads it when the config
doesn't already exist in active storage (fresh module install, or a
brand-new config object). It is never read by drush cim, which syncs from
drupal/config/sync/ only.
Because d7_images_shanti_image was already active before the
field_legacy_nid line was added, the fix never reached that migration's
active config or config/sync on any already-provisioned machine — the
line only took effect on a machine where the module was freshly reinstalled
after the edit. Result: shanti_image migrations run on affected machines
produced 111,343 nodes with an empty field_legacy_nid field, silently.
The collections/subcollections migrations happened to pick up the fix
because those config objects were newly created during 1b.2 (so Drupal read
them straight from the already-corrected config/install) — but their
uid: default_value: 1 fix (preventing Group's uid=0 auto-membership bug —
see [[project-1b2-group-collections]] Group 3.x API notes) had the identical
drift problem: correct in config/install, never exported to config/sync.
Discovered when a machine that had not freshly reinstalled the module
showed all 174 migrated groups owned by uid=0.
Fixed in this session (branch fix/1b-legacy-nid-migration-config-sync):
applied the config/install process definitions to active config, ran
drush cex to write them into config/sync, backfilled the empty
shanti_image.field_legacy_nid data, and corrected the 174 groups' uid
from 0 → 1. drush config:status confirmed no other config had drifted the
same way.
Process guardrail (applies beyond migrations)
Editing a module's config/install/*.yml after the module is already
enabled somewhere does nothing on its own. It only takes effect for a
config object that doesn't yet exist in active storage. To actually deploy
the change:
- Apply it to active config (
drush config:set, or a one-off script that merges the corrected keys and saves). - Run
drush cexand confirmgit diffonly touches the files you intended —drush config:statusshould be empty on a clean environment before you start, so you can trust the post-cexdiff is exactly your change. - Commit the
config/sync/*.ymlchange in the same PR as theconfig/installedit. Never let them land in separate commits/PRs.
This isn't migration-specific — it applies to any CMI config shipped in a
custom module's config/install/. Worth a general callout in module
development going forward, not just migrations.
Checklist for future content migrations (Texts, Sources, AV, Mandala Home)
- [ ]
field_legacy_nidfield added to CMI for every top-level destination bundle - [ ]
field_legacy_nid: nid(or equivalent) mapped in the migration process - [ ] Config change applied to active config and exported via
drush cexin the same commit — verify withdrush config:statusbefore and after - [ ] Post-import verification: row count + 0 mismatches against
migrate_map_*