Commit Graph

88 Commits

Author SHA1 Message Date
wpdev e8731c5119 docs(2meet-data-optimizer): add PR-057b and PR-062c to the 1.1.0 notes
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Successful in 18s
Tests / PHP Lint (pull_request) Successful in 59s
Tests / PHPCS (pull_request) Successful in 30s
Tests / PHPStan (pull_request) Successful in 1m12s
Tests / Unit Tests (pull_request) Successful in 47s
Tests / Integration Tests (pull_request) Successful in 57s
- CHANGELOG / readme 1.1.0: flat-first routing for flat-sorted searches (tmdo_router_flat_first) and the all-NULL flat row left after deleting an entity
- ADR-004: NULL sync never inserts a row; flat-first rule; front-end p50 from the R-2.1 acceptance run
- BENCHMARK-SCALE: front-end table from the acceptance run (flat tables backfilled) and the join-order finding behind PR-062c
- Verify: composer phpcs (0)
- PR: PR-064b
2026-09-26 21:07:51 +08:00
wpdev dc9a8e9886 perf(2meet-data-optimizer): drive flat-sorted searches from the flat table
- A filtered search ordered by a flat column is rewritten to SELECT STRAIGHT_JOIN … FROM flat INNER JOIN wp_posts, with posts.ID tie-breaks (ours and plugin-appended) read as the equal flat post_id, so MariaDB sorts one table instead of looking every post up and sorting in a temporary table
- Only when the routed conditions already require a flat row on the sort group, without the copy route or a selective post filter; filter tmdo_router_flat_first turns it off; split_the_query compares with the same rewrite
- Verify: tests/live/run.php router (13/13); integration 522; dev36 page SQL Q07 263→147 ms, Q15 250→142 ms; fe.sh hourly_rate__desc 850→367 ms, ids == native
- PR: PR-062c
2026-09-26 21:05:11 +08:00
wpdev a7134bfb80 fix(2meet-data-optimizer): do not recreate a deleted entity's flat row
- Deleting a post / user / term / comment drops its flat row in the adapter delete hook, then WordPress deletes each meta row; sync_from_native upserted NULL for each and brought the row back (512 orphan rows on dev36). With no native value it now only clears the column on an existing row
- Verify: tests/live/run.php resync (25/25, entity deletes in dual_write and shadow_read leave no flat row); mut.php T8 flat_row NO-ROW; integration 516
- PR: PR-057b
2026-09-26 20:34:28 +08:00
wpdev 4fc59b757e docs(2meet-data-optimizer): ADR-004 search-index mode and 1.1.0 release notes
- ADR-004: HivePress / WooCommerce sites keep native meta as the source of truth and use flat tables as its search index; records the 2.1 write order, shadow_read, Promotion Guard, restore policy and router rules, with known limits
- CONTEXT.md Entity Bridge, BENCHMARK-SCALE.md front-end and write-chain measurements (dev36), CHANGELOG and readme for 1.1.0; version 1.1.0
- composer phpcs uses phpcs.xml like CI (was --standard=WordPress with 2312 false errors)
- Verify: composer phpcs (0); composer test; composer stan
- PR: PR-064
2026-09-26 20:24:42 +08:00
wpdev aa2da274dd perf(2meet-data-optimizer): keep WordPress's split query on routed searches
- posts_request only drops SQL_CALC_FOUND_ROWS, but WordPress then treated the request as filtered and selected full post rows through a temporary table; split_the_query now keeps the ID-first split for routed searches when nothing else changed the request, and posts_request_ids drops SQL_CALC_FOUND_ROWS from the split ID query
- Verify: tests/live/run.php router (12/12, IDs-first request, found_posts == native); dev36 fe.sh price 589→340 ms, price__asc 961→427 ms, rating__desc 1724→872 ms, hourly_rate__desc 855→884 ms (was 1206)
- PR: PR-062b
2026-09-26 20:14:29 +08:00
wpdev ec75721976 feat(2meet-data-optimizer): route meta orderby including the HivePress sort clause
- Sort shape (relation OR of NOT EXISTS + EXISTS on one key, named in orderby): drop it from meta_query, LEFT JOIN the flat table and order by its column, ties by ID
- Named routed clause as the sole orderby, and meta_key + meta_value_num (meta_value on string columns) with IS NOT NULL; stay native when the column would not order like CAST(meta_value AS type), orderby has several keys, or meta_value filters
- Verify: tests/live/run.php router (11/11); fe.sh price__asc / hourly_rate__desc / rating__desc ids == native
- PR: PR-062
2026-09-26 20:04:23 +08:00
wpdev f2e261c522 fix(2meet-data-optimizer): join each routed group and drop the redundant GROUP BY
- posts_join joins the flat table of every routed group (a wp_core key next to a post type group no longer fails with "Unknown column")
- Flat tables are one row per post (UNIQUE post_id): remove the posts_groupby hook; skip the INNER JOIN status/date copy route for NOT EXISTS, which must match posts without a flat row
- Verify: tests/live/run.php router (9/9); qbench Q05 475 == native, no SQL error; price filter EXPLAIN has no Using temporary
- PR: PR-061
2026-09-26 19:47:53 +08:00
wpdev b338925605 fix(2meet-data-optimizer): route a meta query only when every registered clause is routable
- Leave the query to WordPress on OR, nested groups with registered keys, several post types, orderby on a named routed clause, and compare/type pairs whose flat-column semantics differ from native (untyped numeric ranges, CHAR on decimals, NUMERIC on text)
- Wrap LIKE values as %esc_like%; hook pre_get_posts at priority 1000 so plugin clauses (HivePress) are seen
- Verify: wp eval-file tests/live/run.php router (7/7); qbench Q04/Q06/Q08/Q14/Q16 == native on dev36
- PR: PR-060
2026-09-26 19:41:49 +08:00
wpdev 8c049f0cf2 fix(2meet-data-optimizer): restore native meta without overwriting newer values or inventing defaults
- 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
2026-09-26 19:22:34 +08:00
wpdev 77e338f9c3 fix(2meet-data-optimizer): resync flat rows from native meta after each write
- dual_write / shadow_read no longer write flat in the update_/add_/
  delete_{type}_metadata pre-filters, before WP decides whether native
  changes at all. added_/updated_/deleted_{type}_meta (priority 1) call
  TMDO_Hook_Bus::sync_from_native(), which copies the entity's first native
  row (plain SQL, NULL when gone) into flat. Flat now follows native when WP
  skips (same value), refuses (non-matching prev_value, add $unique on an
  existing key) or deletes (also delete_all: touched IDs are collected from
  the meta IDs in delete_{type}_meta), and a flat value left stale by a
  direct SQL write heals on the next write.
- update_metadata_by_mid key changes: the old key is remembered in
  update_{type}_metadata_by_mid and re-synced too.
- wpdo_after_write / wpdo_after_delete fire from the sync with the same
  arguments; aeav_only keeps the pre-filter short-circuit unchanged.
- $internal_ops lock key includes the entity type (user 5 ≠ post 5).
- Live suite resync: 4 entities × 2 modes × 3 scenarios (old code: 16 of
  24 FAIL); integration: pre-filter leaves flat to the sync, delete_all
  re-syncs every touched user.
- Verify: live resync + shadow_write + serialized + zero_side_effect 33/33
  PASS; unit 687 OK, integration 485 OK, phpcs (phpcs.xml) 0, stan OK
- PR: PR-057
2026-09-26 19:13:02 +08:00
wpdev 781f171770 fix(2meet-data-optimizer): skip zone deletes on tables that were never created
- TMDO_Zone_Router::delete_post() deleted from wp_wpdo_hot_/cold_{post_type}
  whenever fields were registered for that zone, so every post delete on a
  HivePress site (cold fields registered, module idle, table never created)
  logged a "table doesn't exist" error. Hot and cold deletes now also need
  the table to exist; module state is not checked, so a table left by a
  rolled-back module still loses the rows of deleted posts.
- Live suite zero_side_effect: saving / updating / deleting an hp_listing
  issues no query on missing zone tables (covers PR-054 and this PR).
- Verify: ZoneRouterTest 22 OK; live zero_side_effect + shadow_write +
  serialized 9/9 PASS (old code: zero_side_effect FAIL); unit 687 OK,
  integration 484 OK, phpcs (phpcs.xml) 0, stan OK
- PR: PR-054b
2026-09-26 19:02:10 +08:00
wpdev b84252d787 fix(2meet-data-optimizer): serve native meta in shadow_read and compare flat values
- In shadow_read, TMDO_Hook_Bus::intercept_get() returned the flat value.
  update_metadata() reads the old value through that filter after the Hook
  Bus has upserted the new one, so WP saw old === new and never wrote native
  meta: every update in shadow_read was lost on demotion (dev36 2026-09-26).
  shadow_read now returns $check (native) and compares flat on a sample of
  reads (filter tmdo_shadow_compare_sample_rate, default 0.1). aeav_only is
  unchanged; the post query router still routes in shadow_read.
- Live suite shadow_write: xfail markers removed, HivePress listing case
  added (new int key hp_rating_count); fixtures gain post() and a
  post_type-scoped field()/flat().
- Verify: live shadow_write + serialized 8/8 PASS (old code: 4 FAIL);
  unit 685 OK, integration 484 OK, phpcs (phpcs.xml) 0, stan OK
- PR: PR-056
2026-09-26 18:56:45 +08:00
wpdev 08f9e313e0 fix(2meet-data-optimizer): serialize arrays in text fields instead of storing "Array"
- 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
2026-09-26 18:45:59 +08:00
wpdev 20bd59ddae fix(2meet-data-optimizer): skip cold-zone cache warming unless the cold module is active
- TMDO_Cache_Layer::prefetch() / warm_post() (and so on_save_post /
  on_loop_start) queried wp_wpdo_cold_{post_type} whenever cold fields
  were registered, even with the cold module idle and the table never
  created: every HivePress / WC save logged "table doesn't exist" errors.
  They now return unless the module is write-active and the table exists.
- CacheLayerTest activates the module in setUp; new cases assert zero cold
  queries while idle or when the table is missing.
- Verify: CacheLayerTest 12 OK; unit 671 OK, integration 480 OK,
  phpcs (phpcs.xml) 0, stan OK; dev36 live: seeding listings/products/
  vendors with cold_hp_listing idle → 0 wpdo_cold errors (was hundreds)
- PR: PR-054
2026-09-26 18:34:09 +08:00
wpdev 79a6c4d724 fix(2meet-data-optimizer): refuse aeav_only and cleanup on HivePress / WooCommerce sites
- 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
2026-09-26 18:29:04 +08:00
wpdev f72a315986 test(2meet-data-optimizer): add live test runner and failing shadow_read write suite
- Add tests/live: a `wp eval-file` runner (PASS/FAIL/XFAIL/XPASS/SKIP) with
  fixtures that override entity modes in process (pre_option) and read
  native / flat values with plain SQL, so WP core meta flows run for real
- shadow_write suite reproduces the dev36 finding: in shadow_read,
  update_*_meta returns false and native meta is never written
  (3 XFAIL marked for PR-056); the dual_write control passes
- Move the dev36 integration harness into tests/live/tools; outputs go to
  cwd / $TMDO_LIVE_OUT, site URL and path are env-configurable
- Verify: wp --path=<dev36> eval-file tests/live/run.php shadow_write
  → 1 passed, 3 xfail; unit 658 OK, integration 480 OK,
  phpcs (phpcs.xml) 0, stan OK
- PR: PR-052
2026-09-26 18:21:31 +08:00
wpdev 2540d7c0e2 Merge pull request 'hardening 2.0: 2meet-data-optimizer (staged rollout, dev22 first)' (#3) from hardening/2.0 into main
Tests / Unit Tests (push) Successful in 49s
Anti-EAV Lint + Quality Gate / anti-eav-lint (push) Successful in 22s
Tests / PHP Lint (push) Successful in 52s
Tests / Integration Tests (push) Successful in 1m15s
Tests / PHPStan (push) Failing after 38s
Tests / PHPCS (push) Successful in 30s
2026-09-26 10:20:23 +08:00
wpdev b917e45c8c chore(2meet-data-optimizer): make the hardening branch pass PHPStan and PHPCS
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Successful in 14s
Tests / PHP Lint (pull_request) Successful in 1m0s
Tests / PHPCS (pull_request) Successful in 1m15s
Tests / Unit Tests (pull_request) Successful in 57s
Tests / Integration Tests (pull_request) Successful in 1m14s
Tests / PHPStan (pull_request) Successful in 34s
- 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
2026-09-26 07:45:05 +08:00
wpdev 08492dd795 fix(2meet-data-optimizer): compare empty strings correctly when retiring a group
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Successful in 14s
Tests / Unit Tests (pull_request) Successful in 33s
Tests / PHP Lint (pull_request) Successful in 36s
Tests / Integration Tests (pull_request) Successful in 57s
Tests / PHPCS (pull_request) Successful in 35s
Tests / PHPStan (pull_request) Failing after 50s
- 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
2026-09-26 04:24:14 +08:00
wpdev ecf4ff4938 refactor(2meet-data-optimizer): retire the SSO AddOn recommendation
- missing_integration_addons() no longer asks for the SSO AddOn when
  2meet-hub-core / 2meet-spoke-sso are active: they register their own
  tables and the `sso` values belong in usermeta (benchmark: no
  measurable gain from the flat table for by-user-id access).
- `sso` joins the deprecated AddOns; for hub / spoke / sso the notice says
  they go without replacement and to run
  `wp tmdo bridge-restore-native user --group=sso --retire` first.
- Verify: core unit 658 / integration 479 green, phpcs clean
- PR: PR-043
2026-09-26 04:07:40 +08:00
wpdev 44a188b7ee feat(2meet-data-optimizer): group-scoped native write-back and group retirement
- 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
2026-09-26 04:06:31 +08:00
wpdev c70dc99bf9 refactor(2meet-data-optimizer): stop registering the sso group in core
- TMDO_Member_Fields no longer registers user/sso; the definition moved
  unchanged (hash 03543f19) to 2meet-data-optimizer-sso-addon.
- missing_integration_addons(): 2meet-hub-core or 2meet-spoke-sso without
  the SSO AddOn is reported (admin notice + Site Health).
- The deprecated-AddOn notice points hub / spoke users to the SSO AddOn.
- Sites must activate the SSO AddOn before this ships (PR-041); aeav_only
  sites without it fall back to orphan-group restore.
- Verify: core unit 659 / integration 475, SSO 6, HP 142/49, WC 10/22
  green, phpcs clean
- PR: PR-040
2026-09-26 03:39:58 +08:00
wpdev 10671e7b78 perf(2meet-data-optimizer): cache found-rows counts for routed queries
- TMDO_Post_Query_Router drops SQL_CALC_FOUND_ROWS from routed searches
  (posts_request) and answers found_posts_query from a COUNT(*) of the
  same query, cached in the object cache under posts' last_changed.
- Hook Bus flat post writes and deletes move posts' last_changed: in
  aeav_only they skip update_metadata()'s own hooks, so query caches
  would otherwise outlive the write.
- Bench (1M): cached page 8 ms (was 694 / 363 ms for scenarios 1 / 2);
  after invalidation 8 ms + COUNT 608 / 344 ms, on par with before.
- REST X-WP-Total COUNT is on the frozen Hot zone path (ADR-003): not
  changed.
- Verify: core unit 659 / integration 475, HP 142/49, WC 10/22 green,
  phpcs clean
- PR: PR-030
2026-09-26 01:44:45 +08:00
wpdev 754f904e3b perf(2meet-data-optimizer): route status and date ordering to entity tables
- TMDO_Post_Query_Router: when a group's post_status / post_date copies
  are ready and none is NULL (one index probe per group per request), a
  single-post-type search with explicit statuses and date ordering INNER
  JOINs the flat table, filters the copied status, orders by the copied
  post_date and skips GROUP BY. Otherwise the query keeps its old shape.
- New posts_orderby filter; bench gains an `entity+copy` variant.
- Bench (1M, with SQL_CALC_FOUND_ROWS): scenario 1 p95 3,739 -> 773 ms
  (p50 670 -> 770); scenario 2 p50 356 -> 448 ms. Without found-rows the
  same shape runs in 8 ms (PR-030).
- Verify: core unit 659 / integration 471 green, phpcs clean
- PR: PR-029
2026-09-26 01:41:07 +08:00
wpdev aeca12b41d feat(2meet-data-optimizer): store post_status and post_date copies in post entity tables
- TMDO_Post_Status_Sync: post group tables get post_status / post_date
  columns (posts' collation) and an (post_status, post_date) index.
  Brand-new empty tables get them on creation; existing tables only via
  `wp tmdo post-status-sync [--dry-run] [--batch]`, never as DDL on a
  populated table in a web request. Per-group state in
  tmdo_post_status_columns ('columns' | 'ready').
- transition_post_status refreshes the copies; Hook Bus upserts that
  create a post row fill them in (existing rows keep theirs).
- Bench (1M rows): add columns 0.05 s, index 5.3 s, batched backfill 32 s,
  0 mismatches afterwards.
- Verify: core unit 659 / integration 466 green, phpcs clean
- PR: PR-028
2026-09-26 01:25:20 +08:00
wpdev f4eee2338b feat(2meet-data-optimizer): flag new Hot zone registrations as deprecated
- ADR-003 step 4 (core side): TMDO_Schema_Registry::register() raises
  _doing_it_wrong() the first time each provider registers a zone=hot
  field in a request (zone omitted defaults to hot). Fields still
  register, so existing hot data stays readable.
- Cold / warm / archive registrations are unaffected.
- Test bootstrap's _doing_it_wrong stub now records calls.
- Verify: core unit 658 / integration 459, HP 142/49, WC 10/22 green,
  phpcs clean
- PR: PR-038
2026-09-26 01:16:05 +08:00
wpdev 65c75dfc8d feat(2meet-data-optimizer): support composite indexes on entity groups
- register_group() accepts $args['indexes'] (name => ordered keys); indexes on
  unknown keys are dropped. Kept outside the field definition, so schema
  hashes are unchanged.
- Schema_Manager::ensure_group_indexes() adds missing cidx_* indexes to the
  existing flat table in place; the applied definition is remembered in
  tmdo_entity_indexes so later requests skip SHOW INDEX.
- Bench (1M listings, scenario 2): Entity p50 337 ms -> 278 ms with
  (hp_verified, hp_price), on par with Hot (269 ms).
- Verify: core unit 656 / integration 459 green, phpcs clean
- PR: PR-027
2026-09-26 01:05:23 +08:00
wpdev d6b2e417f4 docs(2meet-data-optimizer): decide hot table vs entity table consolidation (ADR-003)
- Compare the Hot zone and Entity bridge (capabilities, 1M-listing benchmark, per-site usage: no 2meet-DO site uses the Hot zone; only legacy dev34 / dev10 hold Hot copies of postmeta)
- Decision (user, 2026-09-26): converge on Entity; freeze the Hot zone, skip PR-026, keep PR-027, move Hot-only HivePress fields into new Entity groups, stop Hot registrations
- PR: PR-025
2026-09-26 00:49:20 +08:00
wpdev d86c0a7bac feat(2meet-data-optimizer): write flat-table data back to native meta
- 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
2026-09-26 00:44:53 +08:00
wpdev c84d2409b7 feat(2meet-data-optimizer): warn when HivePress / WooCommerce run without their AddOn
- TMDO_Compatibility::missing_integration_addons() reports active HivePress / WooCommerce whose AddOn bootstrap class is absent
- Surface it as Site Health test wpdo_missing_integration_addons (also on the admin Doctor tab) and an administrator notice (TMDO_Missing_Addons_Notice)
- Verify: MissingIntegrationAddonsTest (5, class stand-ins in separate processes); core unit 651 / integration 449, HP 148, WC 10 OK; read-only check: dev20/21 HivePress+WooCommerce, dev22 HivePress, others clean
- PR: PR-035
2026-09-26 00:36:02 +08:00
wpdev 3d650eb848 docs(2meet-data-optimizer): partner self-registration guide and deprecated AddOn notice
- Add docs/PARTNER-REGISTRATION.md: TMDO_Anti_EAV_Aware bridge setup (plugins_loaded:3), table and entity-group registration with post_types scopes, frozen definitions, and the AddOn → self-registration migration checked with [COLLISION]/[ORPHAN]
- TMDO_Deprecated_Addons_Notice warns administrators while any of the 8 in-house partner AddOns is active (site or network)
- Trait register_entity_fields() forwards an optional $args (post_types) to the registry
- Verify: DeprecatedAddonsNoticeTest (4); core unit 645 / integration 449, HP 148, WC 10 OK
- PR: PR-021
2026-09-26 00:31:34 +08:00
wpdev 0603537f0a refactor(2meet-data-optimizer): resolve post_type/group/table mappings through the registry
- 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
2026-09-26 00:22:20 +08:00
wpdev 8a1c9c0353 refactor(2meet-data-optimizer): remove HPCT legacy from core
- Feature flags keep only the live AddOn modules (wc_orders, latepoint) as ADDON_MODULES / addon_modules(); drop map_hpct_status() and the 7 HivePress HPCT modules whose interceptors were removed in PR-014
- Remove HPCT detection/notice (Compatibility, core), the admin HPCT import tab and status row, CLI import-hpct, the 7 advisor rules for dead modules, and the removed interceptors from COEXIST_WHITELIST
- Verify: NoHpctLegacyTest guard; core unit 635 / integration 449, HP 145/49, WC 10/22 OK; all sites have every module idle and never imported HPCT
- PR: PR-020c
2026-09-26 00:14:18 +08:00
wpdev 1eedaa9b3c refactor(2meet-data-optimizer): stop registering HivePress entity groups in core
- Drop hp_listing_core / hp_request_core / hp_vendor_core from TMDO_Post_Fields and hp_user from TMDO_Member_Fields; the HivePress AddOn registers identical definitions (PR-019/020a/033b)
- Tests: core asserts it registers none of them; HP-specific core tests removed (covered by the AddOn); SyncBridgeEntityGuardTest registers its own hp_price group
- Verify: core unit 633 / integration 449, HP 145/49, WC 10/22 OK
- Deploy: activate the HivePress AddOn on dev20/21/22 first
- PR: PR-020b
2026-09-26 00:07:31 +08:00
wpdev e0683a19b1 feat(2meet-data-optimizer): reconcile post rows written to the wrong group table
- 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
2026-09-26 00:04:38 +08:00
wpdev 3f4fe52e1b fix(2meet-data-optimizer): route post entity keys by post_type
- Entity groups can declare post_types; get_field() resolves a post's key against its own type's groups first, then unscoped ones (legacy last-wins lookup kept for callers without a post type)
- Hook Bus and the post query router pass the post type, so a listing's hp_rating / hp_status / hp_verified map to hp_listing_core instead of the vendor/request tables; core HP groups declare their post types (schema hashes unchanged)
- Behind tmdo_post_scoped_routing: on for sites without post flat rows, off where rows may sit in the wrong table until PR-034 reconciles them (dry-run: dev20–22 off, dev23/24/30/31/40 on)
- Verify: EntityRegistryScopeTest (8), PostScopedRoutingTest (4); core unit 630 / integration 443, HP 144/49, WC 10/22 OK
- PR: PR-033
2026-09-25 23:55:49 +08:00
wpdev 1653011f4f feat(2meet-data-optimizer): guard orphaned flat-table groups
- On init:1, re-register aeav_only groups whose flat table exists and whose definition is in wpdo_registry_meta but no active plugin registers them, so their data stays readable (routing only, never DDL)
- Keys owned by a live group, or with rows in native meta (newer data written while orphaned), are left alone; the native-meta check is cached for a day
- Surface restored groups via Site Health (wpdo_orphaned_entity_groups), the admin Doctor tab, and wp wpdo doctor
- Verify: EntityRegistryRestoreTest (9), OrphanGroupRestoreTest (7); core unit 622 / integration 439, HP 141/49, WC 10/22 OK; dry-run on dev20–22 restores 16/17/4 groups, keeps hp_taxonomy hp_sort_order/hp_icon on native
- PR: PR-018
2026-09-25 23:38:12 +08:00
wpdev e9d8696c93 fix(2meet-data-optimizer): report entity group name collisions instead of silently dropping
- register_group() keeps the first definition but records a field diff (added/removed/changed) when the same entity/group is re-registered differently; identical re-registration stays silent
- Surface collisions via Site Health (wpdo_entity_group_collisions), the admin Doctor tab, and wp wpdo doctor; warning log throttled to once a day
- Verify: EntityRegistryCollisionTest (7); core unit 613 / integration 432, HP 141/49, WC 10/22 OK
- PR: PR-017
2026-09-25 23:27:05 +08:00
wpdev 07db7734cf refactor(2meet-data-optimizer): delete dead HivePress defaults and hardcoded partner list
- Remove never-called register_hivepress_defaults() and core boot probes for AddOn classes that load at plugins_loaded:3/:6 and self-register (8 partner list entries, WC orders interceptor, HP transient filter)
- Keep core-owned TMDO_Member_Fields / TMDO_Post_Fields registration unchanged
- Verify: CoreBoundaryTest guard added; core unit 606 / integration 432, HP 141/49, WC 10/22 OK
- PR: PR-016
2026-09-25 23:19:22 +08:00
wpdev 9dc4516ca0 fix(2meet-data-optimizer): filter REST listings collection by post_status and post_type
- GET /wpdo/v1/listings rejects unregistered or non-viewable post types (400) and joins posts so Zone A only lists published posts of that type
- WP_Query fallback now returns only show_in_rest hot columns, matching the Zone A path
- Verify: core unit 605 / integration 432 OK (new RestApi tests); 1M bench: page 7ms, total COUNT 0.12s → 1.7s (tracked in PR-030)
- PR: PR-015
2026-09-25 23:10:46 +08:00
wpdev b407f235ac fix(2meet-data-optimizer): refuse uninstall while data lives only in flat tables
- uninstall.php checks every site first and wp_die()s (keeping tables,
  options and plugin files) on aeav_only modes, misc rows, or zone
  modules past cutover; a failed bootstrap also refuses
- Multisite now checks and cleans every site: network activation is always
  false at uninstall, so only the main site was being cleaned before
- Drop the inline fallback DROP loop, unreachable now that a missing
  helper refuses instead

Verify: UninstallGuardTest (8 tests incl. running uninstall.php single and
  multisite); unit 602 OK, integration 429 OK
PR: PR-008
2026-09-25 22:16:02 +08:00
wpdev fc0f018a94 fix(2meet-data-optimizer): stop auto-activating entity bridge on install
- install() no longer switches the Hook Bus on or puts user/term/comment
  into dual_write; the now-unused maybe_autoactivate_entity_bridge() is gone
- Hook Bus and Mode_Manager defaults are off/disabled; sites with stored
  modes (incl. aeav_only) keep them since only unset keys use defaults
- Guard run_dbdelta()'s upgrade.php require like install_v2_tables() so
  install() can run under the integration test stub

Verify: InstallerV2Test::test_install_does_not_enable_entity_bridge,
  ModeManagerDefaultsTest; unit 602 OK, integration 421 OK
PR: PR-007
2026-09-25 21:48:23 +08:00
wpdev 151aadd661 fix(2meet-data-optimizer): default misc bucket and garbage filter to off
- Both term/comment filters now default to disabled for new installs, so
  activating the plugin no longer reroutes or drops other plugins' meta
- One-time maybe_upgrade() migration keeps the misc bucket on for sites
  that already hold misc rows and never set the toggle (data stays readable)
- Admin toggles show the same defaults as the filters

Verify: phpunit-integration --filter 'defaults_false|preserve_default';
  unit 600 OK, integration 420 OK
PR: PR-006
2026-09-25 21:39:38 +08:00
wpdev d3b72bd1af test(2meet-data-optimizer): add scale benchmark report (native vs hot vs entity)
- Benchmark 7 scenarios at 100k vendors / 1M listings with SQL shaped like
  WP_Query, TMDO_Query_Router and TMDO_Post_Query_Router
- Native meta_query takes 14-17s; flat tables bring searches to 0.3-0.6s
- Hot table lacks an hp_vendor index (3.4s); entity lacks composite indexes

Verify: TMDO_TEST_DB_NAME=wp_wpdo_bench php tests/bench/run.php --iterations=20
PR: PR-005
2026-09-25 21:29:58 +08:00
wpdev 8fc1dc1f1c test(2meet-data-optimizer): add scale seeder for 100k vendors / 1M listings
- Seed a HivePress-shaped dataset three ways (native meta, entity tables,
  hot table) with flat tables built by TMDO's own schema code
- Refuse any database not named *_bench before the bootstrap connects;
  bootstrap now honours a pre-defined ABSPATH for the wp-admin stub
- Full scale seeds 9.5M postmeta rows in 7m25s; 0 value mismatches

Verify: TMDO_TEST_DB_NAME=wp_wpdo_bench php tests/bench/seed.php
  --vendors=100000 --listings=1000000; unit 600 OK, integration 416 OK
PR: PR-004
2026-09-25 20:55:25 +08:00
wpdev 9fa84845be fix: NinjaFirewall 相容性 — 保護 WAF 設定選項不被重導向
評估 NinjaFirewall (WP Edition) 4.9 的相容性,結論是兩者可共存且
不需要開發 AddOn(該外掛全 codebase 零個 apply_filters('nfw_*') /
do_action('nfw_*'),官方相容手段全在部署層)。但核心有一個隱患必須補。

TMDO_Options_Manager::register_settings_group() 以 pre_update_option_{key}
回傳 $old_value,讓選項不再落地 wp_options、改存專屬設定表。而
NinjaFirewall 的 Full WAF 走 auto_prepend_file,在 WordPress 載入前就以
原生 mysqli 直查 wp_options 取 nfw_options / nfw_rules。一旦這些鍵被
重導向,WAF 會讀不到設定而靜默停止防護 —— 不報錯、不寫 log。

nfw_rules 約 77KB 且 autoload=auto,正是 autoload 瘦身最誘人的目標,
因此這條路徑並非理論風險。

Added
- PROTECTED_OPTIONS 常數與註冊守衛(nfw_options / nfw_rules / nfw_checked),
  命中時發出 _doing_it_wrong()。守衛置於方法開頭,全部鍵都被擋時提前返回,
  不再建立空的設定表。
- tests/unit/OptionsManagerProtectedTest.php(4 tests / 10 assertions),
  鎖住「受保護鍵絕不會被掛上 pre_option_* / pre_update_option_* 攔截」。
- docs/WAF-COMPATIBILITY.md:模式差異、symlink 多租戶部署、WP SaaS 開站
  流程與驗證清單、三條開發約束、實測風險矩陣。

Changed
- Migration Wizard 輪詢 500ms → 2s,與四個 stress-test 面板一致。
  原本 2 req/s 打同一 REST endpoint,易觸發 WAF rate-limit 與 bot 偵測。

autoload 最佳化不受影響:optimize_autoload() 只改 autoload 欄位、不刪列,
而 Full WAF 的 SELECT * 不看 autoload。

驗證:591 tests / 1166 assertions 通過,PHPCS 零違規,版本一致性 1.0.2。
dev30 於 Full WAF 與 WP WAF 兩種模式下實測,firewall log 中 TMDO 相關
攔截 0 筆。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdjXAU473eekjPBB8vPVRS
2026-08-15 20:38:09 +08:00
wpdev 9f587c39dc fix: Schema Registry cold zone 註冊改為冪等(修復欄位重複累積)
Tests / PHP Lint (pull_request) Successful in 13s
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Failing after 25s
Tests / Unit Tests (pull_request) Successful in 36s
Tests / PHPCS (pull_request) Successful in 30s
Tests / PHPStan (pull_request) Successful in 53s
Tests / Integration Tests (pull_request) Successful in 2m47s
三個 zone 索引的寫入方式並不一致:
- hot  -> hot_columns[post_type][column]  以 column 為 key,天生冪等
- warm -> warm_fields[meta_key]           以 meta_key 為 key,天生冪等
- cold -> cold_fields[post_type][]        append,無去重

而 wpdo_register_fields 本來就會被觸發多次:核心自身於 plugins_loaded:4
觸發,主檔另有 late-bind safety net 於 :30 再觸發一次(讓在 :6 之後才
bootstrap 的 AddOn 趕上);加上主外掛與其 AddOn 可能同時註冊同一批欄位。

實測影響:某站台 cold_fields['hp_vendor'] 累積到 35 筆但只有 9 個相異值,
每個 key 重複 4 次。修正後為 9 筆、零重複。

修法:cold 分支加入 in_array() 去重,使 register() 對同一
(post_type, meta_key) 成為冪等操作,與 hot / warm 既有行為一致。

驗證:核心單元測試 587 tests / 1156 assertions 全綠;
wp wpdo doctor 於兩個站台皆 0 FAIL。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 14:27:15 +08:00
wpdev c5027b0fce docs: CHANGELOG 補上階段 6/7 的 Admin、邊界與品質數據
Anti-EAV Lint + Quality Gate / anti-eav-lint (push) Successful in 7s
Tests / Unit Tests (push) Successful in 9s
Tests / Integration Tests (push) Successful in 36s
Tests / PHP Lint (push) Successful in 7s
Tests / PHPCS (push) Successful in 20s
Tests / PHPStan (push) Successful in 24s
2026-07-31 10:44:46 +08:00
wpdev e33ae4e626 test(boundary): 新增 CoreBoundaryTest,並補完 PR-I 的兩個缺漏測試
Anti-EAV Lint + Quality Gate / anti-eav-lint (push) Successful in 9s
Tests / Unit Tests (push) Successful in 9s
Tests / Integration Tests (push) Successful in 31s
Tests / PHP Lint (push) Successful in 8s
Tests / PHPCS (push) Successful in 20s
Tests / PHPStan (push) Successful in 24s
CoreBoundaryTest 靜態掃描核心 126 個生產檔,斷言每一處對 AddOn 類別的
static 呼叫都在同一個函式內有 class_exists() 守衛。上一個 commit 修的
TMDO_Listing_Stats fatal 就是這類缺陷,這個測試讓它不會再回來。

它當場又抓到 3 處同類違規(都是實際會 fatal 的路徑),一併修掉:
- admin render_hpct_import():改印 admin notice 並 return
- wp tmdo import-hpct:改 WP_CLI::error 明示需要 hivepress-addon
- cli-post cleanup-hp-transients 其實早有守衛,是測試的行距啟發式太窄;
  判斷範圍改成「同一個函式內」而非固定 12 行

負向驗證:暫時注入一處無守衛呼叫 → 測試如預期失敗;還原後回綠。

同時補完計畫階段 7 PR-I 列的兩個缺漏測試:
- 核心 tests/unit/StandardPostInterceptorTest.php(10 tests)
- HP AddOn tests/unit/ListingStatsTest.php(9 tests)——AddOn 的 unit
  bootstrap 先前刻意不載入真實 TMDO_Listing_Stats,改以
  TMDO_TEST_SKIP_LISTING_STATS_STUB 常數讓它跳過核心的 stub
- 核心 unit bootstrap 補 add_post_meta() stub(flush 路徑用得到)

核心 unit 451 → 587、HP AddOn 145 → 154。
2026-07-31 10:41:13 +08:00
wpdev d52d604d7a style(admin): inline style 全面改用 CSS class(PR-H)
Anti-EAV Lint + Quality Gate / anti-eav-lint (push) Successful in 8s
Tests / Unit Tests (push) Successful in 10s
Tests / Integration Tests (push) Successful in 35s
Tests / PHP Lint (push) Successful in 7s
Tests / PHPCS (push) Successful in 19s
Tests / PHPStan (push) Successful in 24s
admin/ 的 inline style= 由 348 處降到 9 處,剩下的 9 處全是 CSS custom
property 載體(--wpdo-bar-width / --wpdo-cov-pct 等動態數值),與來源外掛的
設計一致。

- wpdo-admin.css 543 → 911 行:補上 148 個 class(來源檔在 selector 與
  rule-block 層級都是既有內容的超集,逐條核對過),另加 .wpdo-form-inline
- 5 個變數改為 class 版本:$mode_cls / $badge_mode_cls / $diffs_cls /
  $ap_cls / $e_mode_cls;3 個 stress-test template 補 $mode_text_cls /
  $mode_card_cls
- 刪掉因此變成孤兒的 $mode_badge / $style / $mode_style / $badge_style /
  $e_mode_bg / $badge_bg
- entity card 改 .wpdo-eb-card、pipeline dot 改 .wpdo-stage-pill、demote 按鈕
  改 .wpdo-eb-btn-dim;.wpdo-native-counts / .wpdo-recommendation /
  .wpdo-setup-banner-btn 的重複 inline style 移除
- dashboard 的 top-views 查詢一併改用上一個 commit 的 TMDO_Zone_Warm::VIEW_KEY

PHPStan 抓出自動轉換造成的 11 個未定義變數,已全數補回定義並複驗。
2026-07-31 10:34:57 +08:00