- WooCommerce reads product meta directly and writes stock by SQL, so a flat copy drifted (R-2.1 V1 T4); product meta now stays in native only (plan decision 2). TMDO_Post_Fields registers wp_core, attachment and nav_menu_item. Existing wp_wpdo_post_wc_product tables are left in place and still dropped on uninstall; the orphan restore registers nothing for them while native meta holds the keys; `wp tmdo bridge-restore-native post --group=wc_product --retire` archives them.
- Tests that route products register the former group themselves (tmdo_test_register_wc_product_group(), same name and table); live router Q06 runs on wp_core's _wp_page_template instead of _sku. The post stress tester already skips flat tables that do not exist.
- Verify: PostFieldsRegistrationTest (RED before); dev36: wc_product absent from the registry and not restored as an orphan, a CLI order took stock 452 → 450 in native with no DB error (reverted), drift post 10 groups 0.
- PR: PR-077
- member-backfill / member-audit take their groups from the registry (TMDO_Entity_Migration_Engine::backfillable_groups(), restored orphans excluded) instead of a hard-coded list that still named the never-registered `sso`; `--all` backfills every user group (R-2.1 had to use wp eval). TMDO_Post_Migration::backfillable_groups() now delegates to it.
- member-cutover checks the Mode_Manager result with is_wp_error(): a guarded or invalid transition used to crash with a fatal (WP_Error used as an array).
- Verify: CliMemberBackfillTest (--all and the orphan filter RED before; the --group case cannot fail under the non-throwing WP_CLI stub); dev36 `member-backfill --all --reset` 9 groups, 0 errors, checker user miss/diff/extra 0; `member-cutover --confirm` exits 1 with the Guard message, mode unchanged.
- PR: PR-068
- Admin "backfill all" and `wp tmdo post-migrate-group` take any registered post group via TMDO_Post_Migration::backfillable_groups() instead of a hard-coded list of 7 (dev36 registers 11; the 4 HP add-on groups were never backfilled). Restored orphan groups are excluded and rejected — their values live only in the flat table.
- Groups with json fields (attachment, nav_menu_item) go row by row through the migration engine instead of leaving the json columns NULL in the bulk pivot.
- Verify: PostBackfillJsonTest (3 RED on hardening/2.2); dev36 post-migrate-group attachment / nav_menu_item / hp_draft_flags: json columns semantically equal native (24/24, 21/21), checker unchanged.
- PR: PR-067
- TMDO_Native_Restorer::run() gains a policy: overwrite (flat wins; the
default in aeav_only, where writes only reached flat and native is
stale) or only_missing (native wins, flat only fills gaps; the default in
every other mode, where native is the source of truth and may be newer —
on dev36 an aeav_only round trip overwrote WooCommerce stock 42 with 0).
CLI: --overwrite / --only-missing.
- Native values are read first and each owned key is classified as
missing / differs / same / default_skipped; unchanged values are no
longer rewritten. A flat value equal to its field default is not
written for an entity without a native row (dev36: 1,368 invented
session_count / account_flags rows). --dry-run reports the counts.
- Verify: NativeRestorer* 17 OK (5 new: policy by mode, forced
overwrite, only-missing in aeav_only, dry-run counts, no invented
defaults); unit 687 OK, integration 490 OK, phpcs (phpcs.xml) 0,
stan OK; dev36 `bridge-restore-native user --dry-run`: policy
only_missing, activity 120 defaults skipped
- PR: PR-058
- TMDO_Type_Caster: text / textarea values that are arrays or objects are
stored with maybe_serialize() (was (string) → the literal "Array", which
bridge-restore-native then wrote back over native meta); from_db()
unserializes them with TMDO_Safe_Unserialize. text columns are
VARCHAR(255): longer values are stored NULL (native keeps them) and
logged as text_overflow.
- Add `wp tmdo repair-array-literals [--entity] [--confirm]`
(TMDO_Array_Literal_Repair): rewrites flat "Array" values from native
meta and lists native rows that are "Array" themselves (backup needed).
- Verify: TypeCasterSerializeTest 13 OK, ArrayLiteralRepairTest 3 OK;
unit 685 OK, integration 483 OK, phpcs (phpcs.xml) 0, stan OK;
live `serialized` 3 PASS; dev36 repair dry-run lists the value corrupted
by the 2026-09-26 round trip (user #1 community-events-location)
- PR: PR-055
- Add TMDO_Promotion_Guard: HivePress models read every meta key with an
empty-key get_*_meta() and WooCommerce does the same for products (and
writes stock/sales with direct SQL), so on those sites native meta must
stay authoritative. Promotion to aeav_only is refused in
TMDO_Mode_Manager::set() (covers REST, CLI, phases, auto-promoter) for
post/user/term/comment with HivePress and post/user with WooCommerce;
TMDO_Post_Migration::cleanup() and the cleanup phase refuse too.
Demotion stays open; filter tmdo_promotion_guard_allow overrides.
- Admin notice lists guarded entities already in aeav_only with the
restore-native command (no forced demotion on read).
- Verify: PromotionGuardTest 8 OK; unit 668 OK, integration 480 OK,
phpcs (phpcs.xml) 0, stan OK; dev36 live: 4 promotions + post-cleanup
refused, wpdo_bridge_modes unchanged
- PR: PR-053
- CI PHPStan (level 6, baseline for pre-existing debt) failed on 35 errors in
code added on hardening/2.0: missing iterable value types (PHPDoc only), a
redundant method_exists() in uninstall.php, and the new WP-CLI commands
registered as array('Class', 'instanceMethod') — now array(new Class(), ...)
- Local runs had been masked by a PHPStan tmpDir shared with the act CI runner
(cached container pointing at a removed phar); use a private tmpDir
- Verify: phpstan (private tmpDir) no errors, phpcs clean on changed files,
phpunit Unit 658 OK; dev22 (running this branch) CLI post-status-sync /
bridge-restore-native dry runs OK
- retire() read native meta with wpdb::get_col(), which (via get_var())
returns null for an empty string, so a flat '' written back as '' was
counted as a mismatch and blocked the retirement. It now reads raw rows
with get_results().
- The integration test wpdb stub's get_var() / get_col() now read '' as
null like the real wpdb, so tests see WordPress behaviour.
- Found writing back the sso group on dev21 / dev22 (picture_url = '').
- Verify: core unit 658 / integration 480 green, phpcs clean
- PR: PR-042 follow-up
- TMDO_Native_Restorer::run() takes an optional group; the CLI gains
`bridge-restore-native <entity> --group=<g>`.
- New retire(): once native meta holds every non-NULL value of a group's
flat table (checked against the persisted field definitions), forget
its registry_meta row and rename the table to <table>_retired_<Ymd>, so
orphan restore no longer routes its keys to the flat table. Refused
while a plugin still registers the group or any value differs.
CLI: `--group=<g> --retire`.
- For retiring the user `sso` group (SSO AddOn deprecation).
- Verify: core unit 659 / integration 479 green, phpcs clean
- PR: PR-042
- TMDO_Native_Restorer copies an entity's flat-table values into its native meta table so the value each key reads today is what native returns afterwards: the owning group (by post type for posts) wins, values are stored as WP stores them, NULL removes the native row
- Resumable per-group id cursor, catch-up pass for rows updated during the run, optional demote to dual_write; CLI wp tmdo bridge-restore-native <entity> [--dry-run] [--batch=<n>] [--demote]
- Verify: NativeRestorerTest (5, user) + NativeRestorerPostTest (2, scoped vs legacy routing); core unit 652 / integration 456, HP 148, WC 10 OK; scale estimate dev21 user ~5.1M / post ~4.0M flat rows
- PR: PR-031
- TMDO_Post_Fields::post_type_for_group() / groups_for_post_type() derive HivePress-style groups from their registry post_type scope (core keeps its own attachment / wc_product / nav_menu_item map); the three copy-pasted switches and the query router's POST_TYPE_GROUP_MAP use them
- Term/comment backfill, shadow verifier and CLI take their groups from the registry (TMDO_Term_Comment_Backfill::registered_groups()); installer drops the hp_* entries already covered by its table patterns; REST listings default post type is filterable (tmdo_rest_default_post_type, default 'post')
- Verify: NoHpMappingsTest guard + mapping/default tests; core unit 640 / integration 449, HP 146/49, WC 10/22 OK
- PR: PR-020e
- TMDO_Post_Group_Reconciler copies non-NULL values that legacy routing stored in another group's table into the post type's own group (NULL/missing legacy values keep the scoped value), resumable via a cursor, with a catch-up pass for rows updated during the run, then switches on tmdo_post_scoped_routing
- CLI: wp tmdo post-reconcile-groups [--dry-run] [--batch=<n>]; registry exposes get_group_post_types()
- Verify: PostGroupReconcilerPlanTest (4), PostGroupReconcilerTest (6); core unit 635 / integration 449, HP 145/49, WC 10/22 OK; read-only dry-run: dev20 320 values / 0 overwrites, dev21/22 none
- PR: PR-034