From b63ab46f542f90de3205b17c198305571d1d1612 Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 31 Jul 2026 10:06:03 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E7=A7=BB=E6=A4=8D=20readme.txt=20/=20C?= =?UTF-8?q?ONTEXT.md=20/=20docs=EF=BC=88backport=20A=20v3.4.6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - readme.txt(WP 外掛目錄格式,隨 ZIP 發佈):Stable tag 對齊 1.0.0, changelog 補 1.0.0 條目 - CONTEXT.md(領域詞彙表):Status 區塊改寫為 v1.0.0 實況; HPCT_INTERCEPTORS 與 HivePress Adapter 兩節標註「已搬到 AddOn,核心無此常數」 - docs/:ENTITY_ADAPTER_COOKBOOK、2 篇 ADR、INTEGRATION_PATTERN_DECISION、 anti-eav-lint.yml.template - cookbook 修掉兩個死連結(ANTI_EAV_PLAYBOOK 在來源外掛就不存在) - INTEGRATION_PATTERN_DECISION 加 v1.0.0 後記:結論已被 AddOn 拆分取代 - template 改 wpdev/2meet-data-optimizer + ref v1.0.0 + wp tmdo lint - README.md 文件索引補上以上 7 個檔案 前綴改寫刻意只動類別/函式/slug(WPDO_→TMDO_、wp-data-optimizer→2meet-...), wpdo_ option/cron/hook/表名與 wpdo/v1 REST namespace 一律保留 —— 這是資料層 零遷移的前提。 --- CONTEXT.md | 324 +++++++++++ README.md | 10 + docs/ENTITY_ADAPTER_COOKBOOK.md | 560 ++++++++++++++++++++ docs/INTEGRATION_PATTERN_DECISION.md | 118 +++++ docs/adr-001-post-entity-source-of-truth.md | 85 +++ docs/adr-002-dual-write-naming-collision.md | 65 +++ docs/anti-eav-lint.yml.template | 63 +++ readme.txt | 339 ++++++++++++ 8 files changed, 1564 insertions(+) create mode 100644 CONTEXT.md create mode 100644 docs/ENTITY_ADAPTER_COOKBOOK.md create mode 100644 docs/INTEGRATION_PATTERN_DECISION.md create mode 100644 docs/adr-001-post-entity-source-of-truth.md create mode 100644 docs/adr-002-dual-write-naming-collision.md create mode 100644 docs/anti-eav-lint.yml.template create mode 100644 readme.txt diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..d469fd3 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,324 @@ +# CONTEXT.md — 2meet Data Optimizer Domain Glossary + +This file defines the canonical vocabulary for 2meet Data Optimizer. +Architecture reviews, AI assistance, and code documentation must use these terms exactly. + +> **Status (2026-07-31, v1.0.0):** 核心 451 unit / 416 integration · hivepress-addon 145 unit / 57 integration · woocommerce-addon 10 unit / 18 integration · PHPCS 0 errors · PHPStan L6(baseline 710)· gitea CI 6 job 全綠。 +> v1.0.0 取代 `wp-data-optimizer` v3.4.6,該外掛已退休(本機目錄改名 `.retired`,git 歷史留在 gitea)。版號自 1.0.0 重啟。 +> **本外掛只有通用引擎**:4 entity(post / user / term / comment)反 EAV、zone 表、migration、wizard、admin、CLI、REST、snapshot、diagnostic、notifications。HivePress / WooCommerce / LatePoint / 2meet-* 整合層全部在 11 個獨立 AddOn,核心不得引用它們的類別。 +> **共用命名空間刻意保留**:`wp_wpdo_*` 表、`wpdo_` option / cron / hook 前綴、`wpdo/v1` REST namespace 一律不改名 —— 這是資料層零遷移的前提。類別與函式前綴才是 `TMDO_` / `tmdo_`,並以 `class_alias()` 對外保留 `WPDO_*`。 +> HivePress addon detection uses the **WP active-plugins list** as the authoritative signal (addons publish no per-addon class/const; they register via `add_filter('hivepress/v1/extensions', …)`). See `TMDO_HivePress_Detector::detect()` / `active_plugin_files()` in the HivePress AddOn. +> **測試 harness 隔離**:`TMDO_FSM_GUARD_DISABLED`(integration bootstrap,讓測試可強制 module state)、`TMDO_Routing_Predicate::flush_cache()`、`TMDO_Schema_Manager::flush_table_exists_cache()` 三者是跨測試污染的解法,**不是** production 缺陷的補丁。 +> **gitea runner 為 wpdev 全 repo 共享**(host-mode label `ubuntu-latest:host`,無 service container,integration job 直接連本機 MariaDB)。CI 卡 pending 時先查 `GET /api/v1/user/actions/runners`。 + +--- + +## Zone + +A dedicated storage tier for WordPress postmeta, optimised for a specific access pattern. + +| Zone | Slug | Table pattern | Purpose | +|------|------|---------------|---------| +| Hot | `hot` | `wpdo_hot_{post_type}` | Flat columns for search/filter (index-friendly) | +| Warm | `warm` | `wpdo_warm` | TTL key-value store for counts and transient flags | +| Cold | `cold` | `wpdo_cold_{post_type}` | JSON blob for display-only fields | +| Archive | `archive` | `wpdo_archive` | gzip-compressed historical data | + +A post type's fields are assigned to exactly one zone via the **Schema Registry**. + +--- + +## Schema Registry + +`TMDO_Schema_Registry` — singleton that maps `(post_type, meta_key) → field definition`. + +A **field definition** carries `zone`, `column` (flat name), and optional `type`. +The registry is populated at `wpdo_register_fields` action by integrations and adapters. + +--- + +## Zone Router + +`TMDO_Zone_Router` — static dispatch layer introduced in v3.0.1. + +Single module that knows how to route `read`, `write`, and `delete_field` to the correct +zone handler (Hot / Warm / Cold / Archive) given a field definition. +Also produces canonical **module names** for Feature Flag lookups. + +> **Why it exists**: before v3.0.1, routing logic was duplicated across `TMDO_Sync_Bridge`, +> `TMDO_REST_API`, and two query files. Extracting it here creates one seam for tests and +> one place to change zone routing decisions. + +Interface (all static): +``` +TMDO_Zone_Router::module_name(zone, post_type) → string +TMDO_Zone_Router::read(field, post_id, post_type, meta_key) → mixed +TMDO_Zone_Router::write(field, post_id, post_type, meta_key, value) → void +TMDO_Zone_Router::delete_field(field, post_id, post_type, meta_key) → void +TMDO_Zone_Router::delete_post(post_id, post_type) → void # v3.4.0: all-zone post cleanup +``` + +--- + +## Routing Predicate + +`TMDO_Routing_Predicate` — centralised predicate module introduced in v3.4.0. + +Two recurring guard patterns previously scattered across `TMDO_Sync_Bridge`, +`TMDO_Query_Router`, and the REST API are now a single module: + +``` +TMDO_Routing_Predicate::entity_bridge_owns(meta_key) → bool +TMDO_Routing_Predicate::should_write_to_zone(post_type, zone) → bool +TMDO_Routing_Predicate::should_read_from_zone(post_type, zone) → bool +TMDO_Routing_Predicate::should_query_from_zone(post_type, zone) → bool +TMDO_Routing_Predicate::flush_cache() → void # test isolation helper +``` + +`entity_bridge_owns()` returns true when `TMDO_Mode_Manager::writes_to_flat('post')` AND +the key is registered in `TMDO_Entity_Registry` for 'post'. Result is memoised in a +request-level static cache (`$entity_bridge_cache`) keyed by `meta_key`. + +The three `should_*` predicates are thin compositions of +`TMDO_Zone_Router::module_name()` + the matching `TMDO_Feature_Flags::is_*()` method. + +> **Why it exists**: without this module, callers re-implemented the same two-step +> "module_name then Feature_Flags" pattern inline. The **deletion test** confirms depth: +> removing it pushes the repeated guard back into 8+ call sites in Sync_Bridge alone. + +--- + +## Sync Bridge + +`TMDO_Sync_Bridge` — zone-aware dual-write dispatcher hooked into the WordPress metadata API. + +Intercepts `get/update/add/delete_post_metadata` and `before_delete_post`. +Calls `TMDO_Zone_Router::read/write/delete_field/delete_post` directly — no private +I/O wrappers (v3.4.1: delegate methods inlined and removed). +Uses the **Routing Predicate** for all ownership and activation checks. + +The bridge skips fields owned by the **Entity Bridge** to avoid duplicate writes. + +Its only private helper is `get_field_cached()` — a request-level Schema Registry +lookup cache keyed by `post_type:meta_key`. + +--- + +## Module + +A named unit whose lifecycle is tracked by the **Feature Flags** 7-state machine. +Module names follow the convention: `hot_{post_type}`, `cold_{post_type}`, `warm`, `archive`. + +`TMDO_Zone_Router::module_name()` is the single authoritative source for this naming. + +--- + +## Feature Flags / 7-state FSM + +`TMDO_Feature_Flags` — state machine governing migration lifecycle for each module. + +States: `idle → dual_write → backfill → verify → cutover → cleanup → complete` + +`is_write_active(module)` returns true for `dual_write` and above. +`is_read_custom(module)` returns true for `cutover` and above. + +--- + +## Entity Bridge + +Newer anti-EAV system (v2.5+) covering user, term, comment, and post entities via a +**Hook Bus** that intercepts native `*meta()` API calls and writes to flat entity tables. + +`TMDO_Mode_Manager` controls per-entity mode: `disabled → dual_write → shadow_read → aeav_only`. +`TMDO_Entity_Registry` maps `(entity_type, group) → fields`. + +The Entity Bridge and the Zone system coexist; the Sync Bridge's +`is_owned_by_entity_bridge()` guard prevents duplicate writes. + +--- + +## Migration Phase (Strategy pattern) + +Interface: `TMDO_Migration_Phase_Interface` (v3.0.1). + +Each phase encapsulates a single step of the Entity Bridge migration pipeline: + +| Phase class | Slug | What it does | +|-------------|------|--------------| +| `TMDO_Phase_Diagnose` | `diagnose` | Records preflight EAV row count / ratio | +| `TMDO_Phase_Backup` | `backup` | Dumps native meta table to `uploads/wpdo-backups/` | +| `TMDO_Phase_Demote` | `demote` | aeav_only → dual_write (rollback entry point) | +| `TMDO_Phase_Install_Schema` | `install_schema` | Creates flat tables via Schema Manager | +| `TMDO_Phase_Backfill_Bulk` | `backfill_bulk` | Pivots text-only groups via INSERT…SELECT | +| `TMDO_Phase_Backfill_Unserialize` | `backfill_unserialize` | Row-by-row migration for JSON fields | +| `TMDO_Phase_Promote_Shadow` | `promote_shadow` | dual_write → shadow_read | +| `TMDO_Phase_Verify_Sample` | `verify_sample` | Samples entities, compares flat vs EAV | +| `TMDO_Phase_Promote_Aeav` | `promote_aeav` | shadow_read → aeav_only | +| `TMDO_Phase_Cleanup` | `cleanup` | Deletes managed keys from EAV table | +| `TMDO_Phase_Completed` | `completed` | Terminal — marks job done, returns `'done'` | + +`TMDO_Migration_Phase_Base` provides shared helpers (`log()`, `get_managed_keys()`, +`execute_bulk_pivot()`, `values_loose_equal()`). + +The **Migration Orchestrator** (`TMDO_Migration_Orchestrator`) injects phase objects and +calls `execute($job)` in sequence, advancing through the pipeline. + +> **Seam**: the interface is the test surface. A phase can be tested by constructing it +> with a stub entity type, calling `execute()` with a job array, and asserting on the +> job's `state`, `log`, and `metrics` — no hooks or DB needed for unit tests. + +--- + +## Standard Post Interceptor (Template Method pattern) + +`TMDO_Standard_Post_Interceptor` — abstract base (v3.0.1) for HPCT-inherited interceptors. + +Eliminates the three hook methods (`filter_update_meta`, `action_insert_post`, +`action_delete_post`) that were previously duplicated across four interceptor classes. +Subclasses declare only: + +```php +public const FIELD_MAP = ['meta_key' => 'flat_column', ...]; +protected function get_post_type(): string { ... } +protected function get_table_key(): string { ... } +protected function build_insert_data(int $post_id, WP_Post $post, string $now): array { ... } +``` + +Concrete subclasses: `TMDO_Reviews_Interceptor`, `TMDO_Messages_Interceptor`, +`TMDO_Memberships_Interceptor`, `TMDO_Requests_Interceptor`. + +--- + +## External Partners + +`TMDO_Core::EXTERNAL_PARTNERS` (v3.0.1) — PHP class-name constant array listing all +external plugin integrations that self-register via the Hook Bus. + +```php +['TMDO_Infocards', 'TMDO_Bookings', 'TMDO_Quotation', + 'TMDO_Mobile_Bridge', 'TMDO_Collab', 'TMDO_Playlist'] +``` + +Used in the late-bind priority-30 closure in `2meet-data-optimizer.php` to initialize +partner integrations only when their plugin class is present. Single source of truth — +previously the list existed only inside the closure and diverged from `TMDO_Core`. + +--- + +## HPCT Interceptors Manifest + +> **v1.0.0:本節描述的是 AddOn 的內部結構,核心已無此常數。** +> `HPCT_INTERCEPTORS` 與 `register_hpct_interceptors()` 隨整合層一起搬到 +> `2meet-data-optimizer-hivepress-addon` 的 bootstrap;核心刻意不保留,否則會引用 +> 8 個核心不存在的類別名。下表列的 interceptor ↔ query-handler 配對關係仍然成立。 + +`TMDO_Core::HPCT_INTERCEPTORS` (v3.4.0) — PHP class-constant array that is the single +authoritative list of HPCT-inherited interceptor → query-handler pairs: + +```php +TMDO_Reviews_Interceptor::class → TMDO_Reviews_Query::class +TMDO_Messages_Interceptor::class → TMDO_Messages_Query::class +TMDO_Favorites_Interceptor::class → null +TMDO_Memberships_Interceptor::class → TMDO_Memberships_Query::class +TMDO_Statistics_Interceptor::class → null +TMDO_Requests_Interceptor::class → TMDO_Requests_Query::class +TMDO_Listing_Meta_Interceptor::class → TMDO_Listing_Meta_Query::class +TMDO_LatePoint_Interceptor::class → null +``` + +`register_hpct_interceptors()` iterates this constant; adding a new interceptor requires +only one entry here — no code change in the registration method. + +--- + +## HPCT (HP Custom Tables) + +The plugin this replaces. Still referenced in import path (`wp tmdo import-hpct`). +Any "HPCT-inherited" interceptor means it originated in HPCT and was migrated here. + +--- + +## Adapter (HivePress) + +> **v1.0.0:這 13 個 adapter 住在 `2meet-data-optimizer-hivepress-addon`,不在核心。** +> 核心只提供 `TMDO_Schema_Registry` 與 `wpdo_register_fields` 這個 seam。 + +Each HivePress addon (core, bookings, events, …) has a corresponding +`TMDO_Hivepress_*_Adapter` that registers its meta keys into the Schema Registry at +`wpdo_register_fields`. The adapter is the seam between HivePress and the zone system. + +--- + +## Anti-EAV + +The overarching goal: eliminate Entity-Attribute-Value (EAV) reads from `wp_postmeta`, +`wp_usermeta`, etc. by moving data to flat tables. + +Compliance is enforced by `wpdo-policy-enforcer.php` (mu-plugin) and the +`wp tmdo lint` CLI gate. + +--- + +## Entity Group + +A named set of fields registered under one entity type (user / post / term / comment), +stored as one flat table: `wp_wpdo_{entity_type}_{group_name}`. + +Each group has **one row per entity** (keyed on `user_id` / `post_id` / etc.). +Fields within the group become typed columns; `sanitize_column_name()` produces the +column name (strips all non-`[a-zA-Z0-9_]` characters — hyphens are removed, not replaced). + +Group registration: `TMDO_Entity_Registry::register_group(entity_type, group_name, fields[])`. + +Canonical user groups (v3.1.4): + +| Group | Table | Purpose | +|-------|-------|---------| +| `membership` | `wp_wpdo_user_membership` | Tier, points, expiry | +| `activity` | `wp_wpdo_user_activity` | Login counters, last-active | +| `profile` | `wp_wpdo_user_profile` | Bio, avatar, display name | +| `sso` | `wp_wpdo_user_sso` | Hub token cache, SLO hash | +| `core_profile` | `wp_wpdo_user_core_profile` | WP first_name, last_name, nickname, description | +| `social` | `wp_wpdo_user_social` | 15 social platform URLs | +| `commerce` | `wp_wpdo_user_commerce` | WC billing/shipping + runtime stats (wc_last_active, wc_order_count_wp, last_update) | +| `hp_user` | `wp_wpdo_user_hp_user` | HP favorites, avatar, hp_verified | +| `admin_prefs` | `wp_wpdo_user_admin_prefs` | WP default admin-UI keys written by wp_insert_user + extended UI prefs (22 keys total, v3.1.4) | + +--- + +## EAV Floor + +The minimum irreducible rows that must remain in the native meta table even after full +Entity Bridge migration to `aeav_only`. + +For `wp_usermeta`, the floor consists of: + +| Key | Rows per user | Why irreducible | +|-----|---------------|-----------------| +| `wp_capabilities` | 1 | WP core reads directly in `WP_User` constructor (not through `get_user_meta`) | +| `session_tokens` | 0–N | WP authentication reads sessions directly; Hook Bus cannot intercept session creation safely | +| `_application_passwords` | 0–1 | WP reads directly during REST auth | + +**Theoretical minimum ratio** for a standard WordPress site: `(users + sessions + app_passwords) / users ≈ 1:1.07` (varies by active sessions). + +Any ratio above 1:1.07 represents reducible EAV that WPDO can absorb. + +--- + +## WP Admin Prefs + +The set of `wp_usermeta` keys that WordPress core writes automatically: + +**Written by `wp_insert_user()` for every new user (7 keys)**: +`rich_editing`, `syntax_highlighting`, `comment_shortcuts`, `admin_color`, `use_ssl`, +`show_admin_bar_front`, `dismissed_wp_pointers` + +**Written on first admin-page visit or explicit user action (extended, registered v3.1.4)**: +`show_welcome_panel`, `wp_persisted_preferences`, `nav_menu_recently_edited`, +`wp_dashboard_quick_press_last_post_id`, `edit_*_per_page` variants, +`community-events-location`, `wp_user-settings`, `wp_user-settings-time`, +`managenav-menuscolumnshidden`, `metaboxhidden_nav-menus`, +`dismissed_no_secure_connection_notice`, `meta-box-order_product` + +All 22 keys are registered in the `admin_prefs` Entity Group and routed to +`wp_wpdo_user_admin_prefs` by the Hook Bus when user entity mode ≥ `dual_write`. diff --git a/README.md b/README.md index 216bca7..655a4b5 100644 --- a/README.md +++ b/README.md @@ -119,9 +119,19 @@ GPL-2.0-or-later ## 文件 +- [readme.txt](readme.txt) — WordPress 外掛目錄格式說明(隨 ZIP 發佈) +- [CONTEXT.md](CONTEXT.md) — 領域詞彙表(Zone / Registry / Mode / FSM 的正式定義) - [PLAN.md](PLAN.md) — 開發任務追蹤 - [CHANGELOG.md](CHANGELOG.md) — 版本歷史 - [DEPLOY.md](DEPLOY.md) — 部署流程 - [SECURITY.md](SECURITY.md) — 安全聯絡 - [DESIGN.md](DESIGN.md) — 架構決策 - [CLAUDE.md](CLAUDE.md) — AI 協作指引 + +給夥伴外掛作者(`docs/`,不進 ZIP): + +- [ENTITY_ADAPTER_COOKBOOK.md](docs/ENTITY_ADAPTER_COOKBOOK.md) — 五層整合階梯與決策樹 +- [adr-001-post-entity-source-of-truth.md](docs/adr-001-post-entity-source-of-truth.md) — post entity 的權威來源契約 +- [adr-002-dual-write-naming-collision.md](docs/adr-002-dual-write-naming-collision.md) — `dual_write` 在兩套 FSM 的同名衝突 +- [INTEGRATION_PATTERN_DECISION.md](docs/INTEGRATION_PATTERN_DECISION.md) — 整合模式取捨紀錄(含 v1.0.0 後記) +- [anti-eav-lint.yml.template](docs/anti-eav-lint.yml.template) — 夥伴外掛 CI gate 樣板 diff --git a/docs/ENTITY_ADAPTER_COOKBOOK.md b/docs/ENTITY_ADAPTER_COOKBOOK.md new file mode 100644 index 0000000..90b6528 --- /dev/null +++ b/docs/ENTITY_ADAPTER_COOKBOOK.md @@ -0,0 +1,560 @@ +# Entity Adapter Cookbook + +How partner plugins integrate with `2meet-data-optimizer v1.0.0+` to gain anti-EAV +benefits without modifying their existing data model. + +This is the practical guide. For the canonical vocabulary see `CONTEXT.md`; for the +two standing architectural decisions see `docs/adr-001-post-entity-source-of-truth.md` +and `docs/adr-002-dual-write-naming-collision.md`. + +--- + +## ⚡ TL;DR Decision Tree (read this first — v2.1.2 reordered) + +The most common mistake is picking Tier 1-3 when Tier 5 was the right answer. +Ask these questions in order: + +``` +Q1. Does the partner plugin have its OWN custom tables / lookup tables + that already provide anti-EAV? (HPOS, wc_product_meta_lookup, + BuddyPress activity tables, EDD payments, etc.) + + ├── YES → 🟢 TIER 5 (integrate, don't duplicate). STOP HERE. + │ Register awareness only. Do not migrate. + │ See: TMDO_WooCommerce reference. + │ + └── NO → continue to Q2. + +Q2. Is this a brand-new plugin you control end-to-end? + + ├── YES → 🟢 TIER 4 (greenfield, anti-EAV from day 1). + │ Use `wp tmdo register-stub ` for boilerplate. + │ See: 2meet-inquiries reference. + │ + └── NO (existing plugin with postmeta) → continue to Q3. + +Q3. Is the relevant postmeta key heavily queried (filter / sort / search)? + Benchmark: > 10k rows OR > 3-condition meta_query OR sort by meta_value. + + ├── YES → 🟢 TIER 1-3 (migrate to a Zone). + │ Tier 1 (5 min) for read-only optimization. + │ Tier 2 (30 min) for full dual-write. + │ Tier 3 (1-2 days) for new entity type. + │ + └── NO → 🟢 LEAVE AS POSTMETA. Premature optimization. + Re-evaluate when scale crosses Q3 thresholds. +``` + +**Why Tier 5 is FIRST**: at v2.1.2 audit time, every mature plugin we surveyed +(WC / BuddyPress potential / EDD potential / GravityForms) has its own anti-EAV. +Defaulting to Tier 1-3 risks the catastrophic "two sources of truth" failure +mode. See `docs/INTEGRATION_PATTERN_DECISION.md` for the principle. + +### How to tell if a partner plugin already has anti-EAV (Tier 5 candidate) + +Check these signals in order — any ONE is sufficient for Tier 5: + +| Signal | Where to look | Examples | +|--------|---------------|----------| +| **Lookup tables** with name pattern `*_lookup` / `*_meta_lookup` / `*_index` | `SHOW TABLES LIKE 'wp_{prefix}_%lookup%'` | `wp_wc_product_meta_lookup`, `wp_wc_customer_lookup` | +| **HPOS-style migration toggle** (custom table replaces postmeta) | Plugin's settings → "High Performance" or "Custom Tables" feature | WooCommerce HPOS, EDD 3.0 payments | +| **Dedicated columns instead of meta** in main entity table | `DESC wp_{plugin}_entities` shows `price`, `status` etc. as columns | BuddyPress activity table, MemberPress subscriptions | +| **Plugin's own search/filter API** that bypasses `meta_query` | `wc_get_products()`, `bp_activity_get()`, `edd_get_payments()` | WC, BuddyPress, EDD all have native APIs | +| **`*_stats` / `*_aggregate` tables** for analytics queries | `wp_wc_order_stats`, `wp_*_lookup` | WC analytics, MonsterInsights | +| **db_version option** that hits `dbDelta` migration on plugin update | `wp option get {plugin}_db_version` returns a non-trivial version | Indicates the plugin has its own schema migration story | + +If you see **2+ signals** → definitely Tier 5. +If you see **0 signals** but the plugin has heavy postmeta usage → Tier 1-3. +If you see **0 signals** and postmeta is light → leave it (Q3 = NO). + +**Quick command-line audit**: +```bash +# List custom tables for a plugin +wp db query "SHOW TABLES LIKE 'wp_{prefix}_%'" + +# Count postmeta keys the plugin owns (low = likely Tier 5; high = candidate Tier 1-3) +wp db query "SELECT COUNT(DISTINCT meta_key) FROM wp_postmeta WHERE meta_key LIKE '\\_{prefix}_%'" + +# If both numbers are non-trivial → Tier 5 is correct (plugin uses both, but +# its tables are the truth and postmeta is legacy/secondary). +``` + +--- + +## Three integration tiers + +Pick the one that matches your plugin's data model: + +### Tier 1 — `TMDO_API` facade(最少改動,5 分鐘) + +If your plugin reads/writes `*_meta()` directly today, swap to the facade. This +gives you future-proofing for free — the day the field migrates to a zone or +entity adapter, your plugin needs zero changes. + +**Before:** +```php +$token = get_post_meta( $vendor_id, 'tmeetic_ical_token', true ); +update_post_meta( $vendor_id, 'tmeetic_ical_token', $new_token ); +``` + +**After:** +```php +$token = class_exists( 'TMDO_API' ) + ? TMDO_API::get_field( $vendor_id, 'tmeetic_ical_token' ) + : get_post_meta( $vendor_id, 'tmeetic_ical_token', true ); + +if ( class_exists( 'TMDO_API' ) ) { + TMDO_API::set_field( $vendor_id, 'tmeetic_ical_token', $new_token ); +} else { + update_post_meta( $vendor_id, 'tmeetic_ical_token', $new_token ); +} +``` + +**Real example:** `2meet-courses/includes/class-2meetic-ical.php` (Wave 2 改造). + +**Cross-entity:** +```php +TMDO_API::get_entity( 'user', $user_id, 'points' ); +TMDO_API::get_entity( 'term', $term_id, 'usage_count' ); +TMDO_API::get_entity( 'comment', $comment_id, 'helpful_count' ); +TMDO_API::set_entity( 'user', $uid, 'points', 50 ); +``` + +**Helper:** +```php +TMDO_API::is_field_registered( 'post', 'hp_price' ); // bool +TMDO_API::trace_storage( 'post', 'hp_price', 'hp_listing' ); // 'zone_hot' | 'postmeta' | ... +``` + +### Tier 2 — Schema Registry 註冊(中等改動,30 分鐘) + +If your plugin owns specific meta_keys that benefit from Hot zone (search/filter), +Cold zone (display/JSON), or Warm zone (TTL counter) treatment. + +**Implementation:** create one integration class. + +```php +// my-plugin/includes/class-tmdo-myplugin.php +final class TMDO_MyPlugin { + public static function register(): void { + // Detect partner plugin (2meet-data-optimizer) — no-op when absent. + if ( ! class_exists( 'TMDO_Schema_Registry' ) ) { + return; + } + add_action( 'wpdo_register_fields', array( __CLASS__, 'register_fields' ) ); + add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_tables' ) ); + } + + public static function register_fields( TMDO_Schema_Registry $registry ): void { + $registry->register_many( 'my-plugin', array( + // Hot zone (search/filter): flat 1NF column with index. + array( + 'post_type' => 'my_post_type', + 'meta_key' => 'my_price', + 'zone' => 'hot', + 'data_type' => 'decimal(10,2) NOT NULL DEFAULT 0', + 'column' => 'my_price', + 'indexed' => true, + ), + // Cold zone (description / JSON / display). + array( + 'post_type' => 'my_post_type', + 'meta_key' => 'my_description', + 'zone' => 'cold', + 'cache_group' => 'wpdo_cold_my_post_type', + 'cache_ttl' => HOUR_IN_SECONDS, + ), + ) ); + } + + public static function register_tables( TMDO_Custom_Table_Registry $registry ): void { + $registry->register( 'my-plugin', array( + 'table_name' => 'my_custom_table', + 'primary_key' => 'id', + 'post_type_link' => 'my_post_type', + 'doctor_callback' => array( __CLASS__, 'doctor_my_table' ), + ) ); + } + + public static function doctor_my_table(): array { + global $wpdb; + $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$wpdb->prefix}my_custom_table`" ); + return array( 'ok' => true, 'message' => "rows: {$count}" ); + } +} + +// In your plugin's bootstrap: +add_action( 'plugins_loaded', array( 'TMDO_MyPlugin', 'register' ), 5 ); +``` + +**Real examples:** +- `2meet-data-optimizer/includes/integrations/class-tmdo-infocards.php` — 9 fields + 3 tables +- `2meet-data-optimizer/includes/integrations/class-tmdo-bookings.php` — 7 tables only + +### Tier 3 — Custom Entity Adapter(大改動,1-2 天) + +If you need cross-entity behaviour (e.g. counter that works on user / term / +comment uniformly), use the demo entity counter pattern. + +**Real example:** `2meet-data-optimizer/includes/integrations/class-tmdo-demo-entity-counter.php`. + +**Pattern:** +```php +final class My_Counter { + public const MODULE = 'entity_my_counter'; + public const TABLE = 'wpdo_my_counters'; + + public static function install_table(): void { + global $wpdb; + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta( "CREATE TABLE {$wpdb->prefix}" . self::TABLE . " ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + entity_type varchar(20) NOT NULL, + entity_id bigint(20) unsigned NOT NULL, + counter_key varchar(100) NOT NULL, + counter_value bigint(20) NOT NULL DEFAULT 0, + updated_at datetime NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY ui_entity_counter (entity_type, entity_id, counter_key), + KEY idx_lookup (entity_type, counter_key, counter_value) + ) {$wpdb->get_charset_collate()};" ); + } + + public static function set( string $entity_type, int $entity_id, string $key, int $value ): void { + // Always write native (durability anchor). + TMDO_API::set_entity( $entity_type, $entity_id, $key, $value ); + + // Conditional dual-write to zone table. + if ( TMDO_Feature_Flags::is_write_active( self::MODULE ) ) { + TMDO_DB::upsert( + $GLOBALS['wpdb']->prefix . self::TABLE, + array( + 'entity_type' => $entity_type, + 'entity_id' => $entity_id, + 'counter_key' => $key, + 'counter_value' => $value, + 'updated_at' => current_time( 'mysql' ), + ), + array( 'counter_value', 'updated_at' ), + array( 'entity_type', 'entity_id', 'counter_key' ), + array( '%s', '%d', '%s', '%d', '%s' ) + ); + } + } + + public static function get( string $entity_type, int $entity_id, string $key ): int { + if ( TMDO_Feature_Flags::is_read_custom( self::MODULE ) ) { + // Read from zone table; fallback to native if row missing. + $val = $GLOBALS['wpdb']->get_var( $GLOBALS['wpdb']->prepare( + "SELECT counter_value FROM `{$GLOBALS['wpdb']->prefix}" . self::TABLE . "` + WHERE entity_type = %s AND entity_id = %d AND counter_key = %s", + $entity_type, $entity_id, $key + ) ); + if ( null !== $val ) { + return (int) $val; + } + } + return (int) TMDO_API::get_entity( $entity_type, $entity_id, $key ); + } + + public static function top_n( string $entity_type, string $key, int $n = 10 ): array { + // Killer query that postmeta cannot do efficiently. + return $GLOBALS['wpdb']->get_results( $GLOBALS['wpdb']->prepare( + "SELECT entity_id, counter_value FROM `{$GLOBALS['wpdb']->prefix}" . self::TABLE . "` + WHERE entity_type = %s AND counter_key = %s + ORDER BY counter_value DESC LIMIT %d", + $entity_type, $key, $n + ), ARRAY_A ); + } +} +``` + +--- + +## FSM lifecycle reference + +For Tier 3 entity adapters, drive the 7+1 state machine via CLI: + +```bash +# 1. Install schema (one-time) +wp tmdo doctor # verify base tables + +# 2. Initial state: idle (do not register feature flag) +wp tmdo mode-audit | grep entity_my_counter # should show 'idle' + +# 3. Begin dual_write — both native + zone get writes +wp tmdo mode-set entity_my_counter dual_write + +# 4. Run backfill (if you have existing data) +# (custom script or wp tmdo migrate) + +# 5. Verify with shadow_read for 7 days +wp tmdo mode-set entity_my_counter verify +wp tmdo shadow-enable entity_my_counter + +# Check for diffs +wp eval 'echo (int) $GLOBALS["wpdb"]->get_var("SELECT COUNT(*) FROM {$GLOBALS[\"wpdb\"]->prefix}wpdo_shadow_diffs WHERE entity_type=\"user\"");' + +# 6. Cutover — reads switch to zone +wp tmdo shadow-disable entity_my_counter +wp tmdo mode-set entity_my_counter cutover + +# 7. After 7 more days, cleanup native rows +wp tmdo mode-set entity_my_counter cleanup + +# 8. Final state — no fallback, zone is source of truth +wp tmdo mode-set entity_my_counter complete + +# Rollback at any time +wp tmdo mode-set entity_my_counter idle +``` + +--- + +## Anti-EAV lint exemptions + +Some patterns require direct SQL by design (one-off migration scans, cron token +expiry checks). Mark them with `phpcs:ignore` so `wp tmdo lint --strict` accepts them: + +```php +// phpcs:ignore WPDO.AntiEAV.PostmetaScan -- Cron sweeps postmeta to find expiring IG tokens. +$ids = $wpdb->get_col( $wpdb->prepare( + "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = 'tmeetic_ig_token_expiry' AND CAST(meta_value AS UNSIGNED) < %d", + time() + 31 * DAY_IN_SECONDS +) ); +``` + +**Available rules:** +- `WPDO.AntiEAV.PostmetaScan` — cross-postmeta scan in migration / cron +- `WPDO.AntiEAV.UsermetaScan` — same, usermeta +- `WPDO.AntiEAV.PostmetaFallback` — fallback path during graceful degradation + +The exemption stays line-local. The lint cannot be silenced for an entire file. + +--- + +## Verification checklist (before merge) + +```bash +# 1. Lint passes strict +wp tmdo lint --plugin=$(pwd) --strict + +# 2. Conflict scan clean +wp tmdo conflict-scan + +# 3. Doctor check (your custom tables registered + healthy) +wp tmdo doctor + +# 4. Tests pass (if your plugin has them) +./vendor/bin/phpunit +``` + +--- + +## Real-world results (dev10, 2026-04-25) + +Production benchmarks measured with 50 hp_listing posts: + +| Path | Latency (n=100) | Speedup | +|---|---|---| +| Native `get_post_meta()` | 205ms | 1.0x baseline | +| `TMDO_Listing_Stats::get_view_count()` (Zone B Warm + fallback) | 81ms | **2.51x** | +| `TMDO_Demo_Entity_Counter::top_n()` (Zone via 1 LEFT JOIN) | 0.19ms / call | **postmeta cannot do efficiently** | + +The `top_n` example is the killer use case — sorting 1000s of users by point +count via postmeta requires a full meta_value scan + filesort. The zone table +serves it from a covering index in sub-millisecond. + +--- + +## Tier 4: Greenfield plugin — `2meet-inquiries` (v0.5.0) + +The cleanest case: a plugin written **from scratch** to be anti-EAV from day 1. +Use this as a template for all new 2meet-* plugins. + +### Why this is the gold standard + +| Pattern | What `2meet-inquiries` does | What `bookings`/`courses`/`infocards` had to retrofit | +|---|---|---| +| Large structured config | `wp_2mqi_forms.config_json LONGTEXT` (custom table) | Originally postmeta `_eh_inquiry_config` (anti-EAV violation) | +| Hot-zone meta on hp_vendor | `wpdo_register_fields` — `_tmqi_default_form_id` (bigint, indexed) | Some retrofitted via Schema_Registry Hot Wave 1; others still WP_Query-driven | +| Sensitive PII | `customer_email_enc BLOB` + `customer_email_hash CHAR(64)` for indexed lookup | Originally separate plugins each rolled own AES wrapper | +| Audit-trail rows | Dedicated `wp_2mqi_responses` table (1 row per submission) | Older plugins used `wp_postmeta` rows-per-field → EAV blow-up | +| Analytics events | Dedicated `wp_2mqi_analytics` (event_type, stage_index, session_id) | N/A — most plugins didn't have analytics, would've gone to postmeta if they did | +| Webhook config | `wp_options` per vendor (autoload=no, low cardinality) | Same | + +### Anatomy: 4 custom tables, 0 plugin-owned postmeta keys + +``` +wp_2mqi_forms — form definitions (config_json + counters + slug) +wp_2mqi_responses — submitted inquiries (encrypted PII + payload_json) +wp_2mqi_drafts — in-progress submissions (token + 14-day expire) +wp_2mqi_analytics — funnel events (view, stage_*, submit) +``` + +Plus 2 `hp_vendor` fields registered to Schema_Registry Hot zone: + +```php +$registry->register_many( '2meet-inquiries', array( + array( + 'post_type' => 'hp_vendor', + 'meta_key' => '_tmqi_default_form_id', + 'zone' => 'hot', + 'data_type' => 'bigint(20) NOT NULL DEFAULT 0', + 'column' => '_tmqi_default_form_id', + 'indexed' => true, + ), + array( + 'post_type' => 'hp_vendor', + 'meta_key' => '_tmqi_inquiries_enabled', + 'zone' => 'hot', + 'data_type' => 'tinyint(1) NOT NULL DEFAULT 0', + 'column' => '_tmqi_inquiries_enabled', + 'indexed' => true, + ), +) ); +``` + +### Bootstrap pattern (recommended) + +```php +final class TMQI_Plugin { + use TMDO_Anti_EAV_Aware; // ← strict contract; fails to load without it + + public function run(): void { + // Register tables on the canonical action. + add_action( 'wpdo_register_fields', array( __CLASS__, 'register_wpdo_fields' ) ); + add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ) ); + // ... + } + + public static function register_wpdo_fields(): void { /* hot-zone fields */ } + public static function register_custom_tables( $registry = null ): void { + TMQI_WPDO_Integration::register_tables( $registry ); + } +} +``` + +### Ground rules followed + +✅ **No `update_post_meta()` calls anywhere** — even for hp_vendor metas, we call `TMDO_API::set_field()` +✅ **No direct `SELECT FROM wp_postmeta`** — `wpdo lint --strict` exits 0 +✅ **All large JSON in custom tables** — `config_json` and `payload_json` columns, never postmeta +✅ **Sensitive data encrypted** — reuse `TMEETIC_Crypto::encrypt()` (don't roll your own) +✅ **Indexed search on encrypted columns** — store SHA-256 hash alongside ciphertext +✅ **Single `do_action( 'tmqi/submitted' )`** — downstream notifiers, analytics, webhook all hook here + +### Verification (run on dev10 right now) + +```bash +# Custom tables registered +wp eval 'echo count(TMDO_Custom_Table_Registry::instance()->for_provider("2meet-inquiries"));' +# → 4 + +# Strict lint passes +wp tmdo lint --plugin=$(wp plugin path 2meet-inquiries) --strict +# → Success: Anti-EAV lint passed + +# Conflict scan +wp tmdo conflict-scan +# → 0 conflicts detected +``` + +### Takeaway for Wave 2/3 retrofits + +When refactoring an existing plugin to be anti-EAV, the question is not "how do +we shoehorn this into postmeta less?" — it's "**what does the data look like if +we redesign it like 2meet-inquiries from day 1?**" Then plot a migration path. + +For most plugins the answer is: **replace one big postmeta key with one custom +table row, and register a small number of hot-zone hp_vendor fields for search.** + +--- + +## Tier 5: Integrating with a plugin that has its OWN anti-EAV — `TMDO_WooCommerce` (v2.1.0) + +The hardest case: WC core already has anti-EAV (HPOS for orders, `wp_wc_product_meta_lookup` +for products). WPDO's job is to **integrate, not duplicate**. + +### Why this is different from Tiers 1-4 + +| Aspect | Tier 1-4 (we own the data) | Tier 5 (WC owns it) | +|--------|---------------------------|----------------------| +| Custom tables | We define + create | WC defines + creates | +| Hot-zone fields | Migrated from postmeta to our Hot zone | Already in WC's lookup tables; we just **register awareness** | +| Doctor probes | We control existence + schema | We probe but don't fix | +| Schema drift | Our migration tooling | WC's update_db_*() handles | +| Conflict | None (single owner) | **Risk: shadow lookup tables** | + +### Anti-pattern: ❌ DON'T duplicate WC's lookup tables + +```php +// WRONG — creates a parallel system that drifts from WC's truth +$schema->register( 'woocommerce', array( + 'post_type' => 'product', + 'meta_key' => '_price', + 'zone' => 'hot', + // ... migrate _price into our wpdo_hot_product table +) ); +// Now `_price` lives in BOTH wp_wc_product_meta_lookup AND our hot zone. +// Updates touch one but not the other. Catastrophe. +``` + +### Right pattern: ✅ Register awareness, defer to WC's anti-EAV + +```php +// In TMDO_WooCommerce::register_custom_tables(): +foreach ( WC_CORE_TABLES as $name => $meta ) { + $registry->register( 'woocommerce', array( + 'table_name' => $name, // wp_wc_product_meta_lookup + 'description' => $meta['description'], + 'doctor_callback' => array( __CLASS__, 'doctor_check' ), + ) ); +} + +// In TMDO_WooCommerce::register_schema_fields(): +// Register postmeta keys WC STILL uses (the ones not yet migrated to lookups). +// When `_price` is also in wc_product_meta_lookup, registering doesn't migrate +// to OUR hot zone — it's just a hint to TMDO_API consumers about "this is hot". +$schema->register_many( 'woocommerce', array( + array( 'post_type' => 'product', 'meta_key' => '_price', 'zone' => 'hot', ... ), + // ... +) ); +``` + +### Decision tree for new partner integrations + +``` +Does the partner plugin store data in postmeta? +├── NO → already on custom tables → Tier 4 (register tables + done) +└── YES → does the partner have its own lookup/cache table? + ├── NO → Tier 1-3 (we manage migration) + └── YES → Tier 5: register awareness only, never duplicate +``` + +### Tier 5 checklist for `TMDO_WooCommerce` + +- [x] Register all 20 `wp_wc_*` tables to Custom_Table_Registry +- [x] Add `doctor_callback` that probes existence + row count (not schema diff) +- [x] Register hot-zone postmeta fields (legacy path only) — WC's lookup is the truth +- [x] Detect HPOS state via `OrderUtil::custom_orders_table_usage_is_enabled()` +- [x] When HPOS off → register order postmeta hot fields (`_order_total`, etc.) +- [x] When HPOS on → DON'T register order postmeta (would be stale) +- [x] Customer usermeta hot fields registered unconditionally (WC always uses usermeta for these) +- [x] Subscription product fields conditional on `WC_Subscriptions` OR `wc-linepay-subscription` +- [x] Own custom table (`wpdo_wc_commissions`) for vendor marketplace tracking +- [x] Admin notice recommends HPOS when legacy order count > threshold +- [x] Admin dashboard surfaces commission stats + table health + +### What this DOESN'T do (and shouldn't) + +- ❌ Migrate `_price` into our hot zone (WC already has wc_product_meta_lookup) +- ❌ Mirror `wc_orders` into our archive zone (WC handles its own archive) +- ❌ Intercept `update_post_meta` for product meta (interferes with WC's lookup sync) +- ❌ Create products / orders / customers (WC's domain) + +### When to revisit + +- WC drops a lookup table (unlikely but possible) → migrate that field path to Tier 3 +- HPOS becomes default-on → audit our order postmeta registrations and remove +- New WC subextension introduces meta keys we should register → add to `register_subscription_fields()` diff --git a/docs/INTEGRATION_PATTERN_DECISION.md b/docs/INTEGRATION_PATTERN_DECISION.md new file mode 100644 index 0000000..c6fa033 --- /dev/null +++ b/docs/INTEGRATION_PATTERN_DECISION.md @@ -0,0 +1,118 @@ +# Integration Pattern Decision — 2026-04-25 + +> **v1.0.0 後記(2026-07-31):本文結論已被架構拆分取代。** +> 當時的兩個選項是「集中在核心」vs「各外掛自帶 bridge」。v1.0.0 走的是第三條路: +> 每個夥伴外掛對應**一個獨立 AddOn 外掛**(`2meet-data-optimizer--addon`,共 11 個), +> 核心完全不認識夥伴外掛。下文的 `class-tmdo-.php` 一律已搬進對應 AddOn。 +> 本文保留是為了記錄「為什麼不選 per-plugin bridge 檔」這段推理 —— 該理由對 AddOn 邊界同樣適用。 + +**Trigger**: Step BB audit revealed two parallel patterns for partner plugin integration; need to commit to one. + +--- + +## What I found + +| Plugin | Centralized in 2meet-data-optimizer? | Own bridge file? | +|--------|-----------------------------------|------------------| +| 2meet-infocards | ✅ `class-tmdo-infocards.php` | ❌ | +| 2meet-bookings | ✅ `class-tmdo-bookings.php` | ❌ | +| 2meet-quotation | ✅ `class-tmdo-quotation.php` | ❌ | +| 2meet-events | ✅ `class-tmdo-events.php` | ✅ `class-tmevents-wpdo-bridge.php` (**duplicate!**) | +| 2meet-collab | ✅ `class-tmdo-collab.php` | ❌ | +| 2meet-mobile-bridge | ✅ `class-tmdo-mobile-bridge.php` | ❌ | +| 2meet-playlist | ✅ `class-tmdo-playlist.php` | ❌ | +| 2meet-courses | ❌ | ✅ `class-2meetic-courses-wpdo.php` (NEW today, P step) | +| 2meet-inquiries | ❌ | ✅ `class-tmqi-wpdo-integration.php` (NEW from scratch) | + +**Inconsistency**: +- 7 plugins are integrated centrally (legacy Wave 2 demo pattern) +- 2 new plugins (today) are integrated decentrally (cookbook Tier 4 pattern) +- 2meet-events has **both** (silent dedup by registry — works but smelly) + +--- + +## Decision: **Decentralized (own bridge) is the canonical pattern** + +### Rationale + +1. **Cookbook Tier 4 documents it as the standard** for new plugins (already published) +2. **Each plugin owns its own data contract** — no cross-plugin coupling in 2meet-data-optimizer +3. **Easier to ship** — partner plugin can update its registration without bumping 2meet-data-optimizer +4. **Simpler mental model** — "where do tables get registered? In the plugin that owns them" + +### Why we're NOT migrating today + +1. **Freeze**: Per `FREEZE_2026-04-25.md`, no risky refactors during freeze +2. **Working**: All 7 centralized integrations work; registry dedup handles the events double-pattern +3. **No vendor demand**: Nobody has reported confusion or bugs from the dual pattern +4. **Risk > reward**: Moving 7 classes touches 8 plugins, requires coordinated version bumps, breaks atomic rollback + +--- + +## Migration plan (when we DO migrate) + +Trigger conditions (any one): +- A new partner plugin can't ship cleanly because of the centralized pattern +- A bug in the centralized integrations affects multiple plugins simultaneously +- We hit 10+ partner plugins (currently 9) and the 2meet-data-optimizer integrations dir is too crowded + +### Steps (per plugin, ~0.5 day each) + +``` +1. Copy /2meet-data-optimizer/includes/integrations/class-tmdo-{slug}.php + to /{plugin-slug}/includes/integrations/class-{prefix}-wpdo.php + +2. Rename class: + - TMDO_Bookings → TMB_WPDO + - TMDO_Quotation → TMQUO_WPDO (etc.) + - Keep registration logic identical + +3. Wire in {plugin-slug} bootstrap (after main classes load): + require_once $dir . 'includes/integrations/class-{prefix}-wpdo.php'; + {prefix}_WPDO::register(); + +4. In 2meet-data-optimizer: + - Remove require_once line + - Remove class name from the partner array (line ~208 of main file) + - Bump WPDO version (patch) + +5. Verify: + wp eval 'echo count(TMDO_Custom_Table_Registry::instance()->for_provider("{slug}"));' + → should still match the original count + +6. Bump partner plugin version (minor, since it now has new dependency) + Update Requires Plugins header to mention 2meet-data-optimizer ≥ X.Y.Z +``` + +### Special case: 2meet-events double-pattern + +Already has both. Migration = remove the centralized `class-tmdo-events.php` (the bridge in 2meet-events stays). This is the **simplest first migration** because the partner plugin's own bridge is already proven. + +--- + +## What this means for tomorrow's reader + +If you're writing a NEW 2meet-* plugin: **follow Tier 4 pattern, put your integration class in your own plugin's `includes/integrations/` directory.** + +If you're maintaining an EXISTING centralized integration in 2meet-data-optimizer: **leave it alone unless one of the migration triggers fires.** + +If you see the dual pattern in 2meet-events and are confused: **it's intentional, registry dedups, will be cleaned up later.** + +--- + +## Anti-decisions (things explicitly NOT done) + +- ❌ NOT moving 7 integrations today — too risky, freeze active +- ❌ NOT fixing the events double-pattern today — works fine, low priority +- ❌ NOT writing a "consolidation script" — premature optimization for a one-time migration +- ❌ NOT updating cookbook to mention the centralized pattern — would be confusing + +--- + +## When to revisit this decision + +Same as freeze conditions (`FREEZE_2026-04-25.md`): +- vendor reports confusion / bug +- 30 days passed with no action needed +- New plugin (#10+) onboarded +- Production critical event involves the pattern diff --git a/docs/adr-001-post-entity-source-of-truth.md b/docs/adr-001-post-entity-source-of-truth.md new file mode 100644 index 0000000..2e69768 --- /dev/null +++ b/docs/adr-001-post-entity-source-of-truth.md @@ -0,0 +1,85 @@ +# ADR-001: Post Entity Source-of-Truth Contract + +**Status:** Accepted +**Date:** 2026-05-15 +**Deciders:** wpdev + +--- + +## Context + +Two code paths can intercept `update_post_metadata` / `add_post_metadata`: + +1. **TMDO_Sync_Bridge** — the original Zone interceptor that dual-writes to + Hot (Zone A) and Cold (Zone C) flat tables. +2. **TMDO_Hook_Bus** — the Entity Bridge write path added in v2.9.x that writes + to `wp_wpdo_post_*` flat tables via Entity Registry groups. + +When both are active without a clear contract, a single `update_post_meta()` call +can fan out to three distinct write paths (wp_postmeta + Zone table + entity flat +table), producing divergent row counts and confusing `TMDO_API::trace_storage()` +output. + +The defensive patch `TMDO_Sync_Bridge::is_owned_by_entity_bridge()` (v2.9.2) +was added to prevent double-writes but left the authoritative contract undocumented. + +--- + +## Decision + +**When `post` entity mode is `dual_write`, `shadow_read`, or `aeav_only`, +Entity Bridge (TMDO_Hook_Bus) is the sole source of truth for keys registered +in TMDO_Entity_Registry under the `post` entity type.** + +Sync_Bridge defers to Entity Bridge for those keys via `is_owned_by_entity_bridge()`: + +```php +// TMDO_Sync_Bridge — intercept_update() guard: +if ( self::is_owned_by_entity_bridge( $meta_key ) ) { + return $check; // pass-through — Entity Bridge owns this key +} +``` + +`is_owned_by_entity_bridge()` returns `true` when both conditions hold: +- `TMDO_Mode_Manager::writes_to_flat('post')` — mode is at least dual_write +- `TMDO_Entity_Registry::get_field('post', $meta_key)` — key is registered + +Keys **not** in Entity Registry continue to be owned by Sync_Bridge (Zone path). + +### Invariants + +| Condition | Owner | +|-----------|-------| +| post mode = `disabled` or `idle` | wp_postmeta (no interception) | +| post mode ≥ `dual_write` AND key in Entity Registry | **Entity Bridge** | +| post mode ≥ `dual_write` AND key not in Entity Registry | **Sync_Bridge** (Zone) | +| post mode = `aeav_only` | Entity Bridge for registered keys; unregistered keys fallthrough to wp_postmeta | + +--- + +## Consequences + +**Good:** +- Developers adding a new post meta key can determine its owner in O(1): check + whether the key is in `wpdo_register_fields` under `post` entity. If yes → + Entity Bridge owns it; if no → Zone Sync_Bridge handles it. +- `TMDO_API::trace_storage()` output reflects this: Entity Bridge keys show the + flat entity table; Zone keys show the zone table. + +**Bad / Watch out for:** +- A key registered in **both** Schema_Registry (Zone) and Entity_Registry (Entity + Bridge groups) will be captured by Entity Bridge and silently dropped by + Sync_Bridge. The duplicate registration is a misconfiguration — caught by + `TMDO_Sync_Bridge` guard and validated by `wp tmdo conflict-scan`. +- If `TMDO_Mode_Manager` or `TMDO_Entity_Registry` are unavailable (e.g. very + early bootstrap), `is_owned_by_entity_bridge()` returns `false` and all writes + fall through to the Zone path — safe degradation. + +--- + +## Related + +- `includes/interceptors/class-tmdo-sync-bridge.php` — `is_owned_by_entity_bridge()` (v2.9.2) +- `includes/engine/class-tmdo-hook-bus.php` — Entity Bridge write path +- `TMDO_API::trace_storage()` — human-readable storage path diagnostics +- `wp tmdo conflict-scan` — detects keys registered in both paths diff --git a/docs/adr-002-dual-write-naming-collision.md b/docs/adr-002-dual-write-naming-collision.md new file mode 100644 index 0000000..48c1f38 --- /dev/null +++ b/docs/adr-002-dual-write-naming-collision.md @@ -0,0 +1,65 @@ +# ADR-002: Acknowledge 'dual_write' naming collision between Mode_Manager and Feature_Flags FSMs + +**Status:** Accepted — deferred rename +**Date:** 2026-05-15 +**Deciders:** wpdev + +--- + +## Context + +Two distinct FSMs in the codebase both use the string `'dual_write'`: + +| FSM | Class | Constant | Stored in | Semantics | +|-----|-------|----------|-----------|-----------| +| Entity Bridge | `TMDO_Mode_Manager` | `MODE_DUAL_WRITE` | `wpdo_bridge_modes` | Writes go to both UAE flat table AND `wp_*meta` | +| Zone Migration | `TMDO_Feature_Flags` | `STATUS_DUAL_WRITE` | `wpdo_features` | Writes go to both Zone A/B/C table AND `wp_postmeta` | + +The two FSMs are orthogonal — a post type can be in Zone `dual_write` +(actively migrating) while the Entity Bridge is in `aeav_only` mode, or vice +versa. The collision was introduced when the Entity Bridge FSM was added in +v2.5.x alongside the pre-existing Zone Migration FSM. + +--- + +## Decision + +**Defer the rename. Document the collision instead.** + +Renaming either constant (e.g. Mode_Manager → `bridge_dual`) would require: + +1. Updating ~85 call sites across 30+ files. +2. Writing a DB migration to translate stored option values (`'dual_write'` → + `'bridge_dual'` in `wp_options['wpdo_bridge_modes']`). +3. Updating all WP-CLI commands that accept mode strings as user input. +4. Updating all admin UI dropdowns and confirmation messages. +5. Handling sites that run the old code against a DB that has already been + migrated (or the reverse — new code on an un-migrated DB). + +The risk of introducing bugs via a mechanical rename outweighs the naming +improvement at the current stage of the project. + +The collision is mitigated by: +- A disambiguating docblock in `class-tmdo-mode-manager.php` (added 2026-05-15) +- This ADR, which explains the overlap to future developers +- The two FSMs operating on different option keys and being unreachable from + each other's code paths + +--- + +## Consequences + +- **Future rename path**: When a DB migration is warranted (e.g. alongside + another schema change), rename `MODE_DUAL_WRITE → 'bridge_dual'` and add a + migration in `TMDO_Installer::maybe_upgrade()` that rewrites the stored string. +- **Linter**: If PHPStan or a custom rule ever flags string literal comparisons + across FSMs, this ADR is the canonical explanation for why the overlap is + intentional. + +--- + +## Related + +- `includes/engine/class-tmdo-mode-manager.php` — naming note in class docblock +- `includes/class-tmdo-feature-flags.php` — Zone FSM (7 states) +- P1-10 from full-review report (2026-05-15) diff --git a/docs/anti-eav-lint.yml.template b/docs/anti-eav-lint.yml.template new file mode 100644 index 0000000..8860e71 --- /dev/null +++ b/docs/anti-eav-lint.yml.template @@ -0,0 +1,63 @@ +name: Anti-EAV Strict Lint + +# Drop this file into each partner plugin's `.github/workflows/anti-eav-lint.yml`. +# Required: `2meet-data-optimizer` v1.0.0+ available either via: +# (a) Composer dev dependency on the plugin repo, OR +# (b) Side-by-side checkout in the same workspace. + +on: + pull_request: + branches: [ main, master, develop ] + push: + branches: [ main, master ] + +jobs: + anti-eav-lint: + name: Anti-EAV Strict Lint (wp tmdo lint --strict) + runs-on: ubuntu-latest + + steps: + - name: Checkout this plugin + uses: actions/checkout@v4 + with: + path: this-plugin + + - name: Checkout 2meet-data-optimizer + uses: actions/checkout@v4 + with: + repository: wpdev/2meet-data-optimizer + path: 2meet-data-optimizer + ref: v1.0.0 + + - name: Setup PHP 8.3 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer:v2, wp-cli + + - name: Install 2meet-data-optimizer dependencies + working-directory: 2meet-data-optimizer + run: composer install --no-interaction --no-dev --prefer-dist + + - name: Bootstrap minimal WP for wp-cli + run: | + # Install a throwaway WordPress so wp-cli has runtime context. + wp core download --path=/tmp/wp --skip-content + wp config create --path=/tmp/wp --dbname=wp_lint --dbuser=root --dbpass=root --dbhost=127.0.0.1 + # Symlink 2meet-data-optimizer into the wp-content/plugins dir so its CLI loads. + mkdir -p /tmp/wp/wp-content/plugins + ln -s "$GITHUB_WORKSPACE/2meet-data-optimizer" /tmp/wp/wp-content/plugins/2meet-data-optimizer + + - name: Run wp tmdo lint --strict + run: | + cd /tmp/wp + wp --skip-themes --skip-plugins=all tmdo lint \ + --plugin="$GITHUB_WORKSPACE/this-plugin" \ + --strict \ + --max-autoload=30 + # Exit non-zero on: + # - Direct SELECT FROM wp_postmeta / wp_usermeta / wp_termmeta / wp_commentmeta + # - update_post_meta() on a field already registered to the TMDO Schema Registry + # - autoload=yes options exceeding --max-autoload (default 30) + # - meta_query with ≥3 conditions but no wpdo_register_fields + # Bypass: phpcs:ignore WPDO.AntiEAV. -- diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..a1d9fd0 --- /dev/null +++ b/readme.txt @@ -0,0 +1,339 @@ +=== 2meet Data Optimizer === +Contributors: 2meetio +Tags: performance, database, postmeta, optimization, hivepress +Requires at least: 6.0 +Tested up to: 6.9 +Requires PHP: 8.1 +Stable tag: 1.0.0 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Four-zone postmeta optimization for WordPress. Migrates wp_postmeta to dedicated custom tables (Hot/Warm/Cold/Archive) for dramatically faster queries. + +== Description == + +**2meet Data Optimizer** replaces the slow, generic `wp_postmeta` EAV table with four purpose-built storage zones, each optimized for a specific access pattern: + +| Zone | Name | Best For | Speed Gain | +|------|------|----------|-----------| +| A | Hot | Search / filter fields (price, category, status) | 3–12× faster | +| B | Warm | TTL counters (view counts, temporary flags) | N/A | +| C | Cold | Display / description fields (JSON blob + Object Cache) | 2–4× faster | +| D | Archive | Expired data (gzip compressed, restorable) | N/A | + += Key Features = + +* **Zero-downtime migration** — 7-state machine (idle → dual_write → backfill → verify → cutover → cleanup → complete) with rollback support +* **Fully replaces HP Custom Tables (HPCT)** — one-command import of all HPCT flags and migration records +* **HivePress integration** — automatic field detection for hp_listing, hp_vendor, and 17+ HivePress extensions +* **LatePoint integration** — booking meta interception +* **WooCommerce Orders interception** — order meta routing +* **MySQL / MariaDB + SQLite dual-engine** — works with both the standard MySQL stack and the SQLite drop-in +* **REST API** — `/wp-json/wpdo/v1/` endpoints for listings, stats, and status monitoring +* **WP-CLI** — 13 commands: status, install, doctor, analyze, migrate, verify, cutover, rollback, enable, disable, import-hpct, benchmark, cleanup +* **Rate limiting** — IP transient + cookie-based dedup for POST /view; Admin Dashboard shows Top 10 rate-limited posts +* **Object Cache integration** — Zone C bulk prefetch on `loop_start` + `save_post` cache warming +* **Admin Dashboard** — 4-group 20-tab UI: Dashboard / Zones / Migration / Classifier / Entity Bridge / User-Term-Comment-Post migration wizards / Stress Testers / Logs / HPCT Import / REST API / HivePress / Settings +* **Zone Classifier** — analyzes existing postmeta and suggests optimal zone placement (transient-cached, 1h TTL) +* **Multisite** — auto-installs tables on new site creation +* **i18n ready** — `.pot` template + Traditional Chinese (zh_TW) 100% complete + += Real-World Performance (MariaDB 11.8.2, n=200) = + +* **hp_listing single-field read**: ~633ms → ~56ms = **12× faster** (Zone A benchmark) +* **hp_vendor single-field read**: ~334ms → ~108ms = **3.1× faster** (Zone A benchmark) +* **hp_vendor JSON blob read**: ~270ms → ~87ms = **3.1× faster** (Zone C benchmark) +* **Query Router T2 full-row get_row**: **28.1× faster** +* **Query Router T7 multi-field write set_many**: **95.3× faster** + += Requirements = + +* WordPress 6.0+ +* PHP 8.1+ +* MySQL 5.7+ / MariaDB 10.4+ (or SQLite via drop-in) + +== Installation == + +1. Upload the `2meet-data-optimizer` folder to `/wp-content/plugins/` +2. Activate the plugin through the **Plugins** screen in WordPress +3. Navigate to **2meet Data Optimizer** in the admin menu +4. Run `wp tmdo doctor` (WP-CLI) or click **Run Doctor** in the Dashboard tab to verify installation +5. Use `wp tmdo analyze {post_type}` to see zone placement recommendations +6. Use `wp tmdo migrate {module}` to start migrating a module + += Migrating from HP Custom Tables = + +1. Keep HP Custom Tables active +2. Run `wp tmdo import-hpct` to copy all HPCT state +3. Verify with `wp tmdo doctor` +4. Deactivate HP Custom Tables + +== Frequently Asked Questions == + += Will my data be lost if I deactivate the plugin? = + +No. During the `dual_write` and `backfill` phases, all data continues to be written to `wp_postmeta`. Data in Zone tables is only the primary source after `cutover`. You can `rollback` any module before reaching `complete` state. + += Does this work without WP-CLI? = + +Yes. All migration operations are available through the Admin UI under **2meet Data Optimizer → Migration**. WP-CLI is optional but recommended for batch operations. + += Is SQLite supported? = + +Yes. The plugin automatically detects the SQLite drop-in (`WP_SQLite_DB` / `WP_SQLite_Translator`) and applies compatibility patches. Zone D gzip archive and Zone A flat columns both work on SQLite. + += Will this conflict with HivePress or HP Custom Tables? = + +2meet Data Optimizer loads at `plugins_loaded` priority 4 (before HPCT at priority 5) and includes compatibility logic to prevent duplicate hook registration. Running alongside HPCT is fully supported during the transition period. + += How do I roll back a migration? = + +``` +wp tmdo rollback {module} +``` + +This reverts to `dual_write` state. All reads return to `wp_postmeta` immediately. You can re-attempt the migration at any time. + += What is the Zone Classifier? = + +The Classifier tab analyzes your existing `wp_postmeta` data (access frequency, value cardinality, field length) and suggests which zone each meta key belongs in. Results are cached for 1 hour per post type. + +== Screenshots == + +1. Dashboard — overview of all modules, test counts, and rate-limit statistics +2. Migration — 7-state progress bar per module with migrate/verify/cutover/rollback actions +3. Zone Classifier — postmeta analysis with zone recommendations +4. REST API Tab — endpoint documentation with cURL examples and JavaScript SDK + +== Changelog == + += 1.0.0 = +* Supersedes `wp-data-optimizer` v3.4.6 — that plugin is retired; this one is the single unified engine +* Version numbering restarts at 1.0.0; entries below 3.4.6 are the predecessor's history, kept for archaeology +* Security: 7 × Logger::error() arity fatals fixed; REST field allowlist; 13 destructive admin actions moved GET → POST; DDL type whitelist; mass column clear is now opt-in via `wpdo_allow_mass_column_clear` +* Concurrency: warm table UNIQUE KEY, JSON_MERGE_PATCH / JSON_REMOVE cold writes, atomic Zone_Warm::increment() +* Architecture: TMDO_Zone_Router + TMDO_Routing_Predicate, Migration Phase Strategy (11 phase classes), TMDO_Standard_Post_Interceptor +* Compat: bidirectional `wpdo_*` ⇄ `tmdo_*` hook bridge, 57 AddOn class aliases, `wp tmdo` namespace for all 38 CLI subcommands +* ABI: `doctor_callback` is now 1-arg; `wpdo_capture_before_value` defaults to false +* Integration layers (HivePress / WooCommerce / LatePoint / 2meet-*) live in 11 separate AddOn plugins + += 3.3.2 = +* Tests: 21 new unit tests for Diagnose, Install_Schema, Backfill_Bulk, Backfill_Unserialize, Verify_Sample migration phases (609 total, 0 failures) +* New: TMDO_Crypto::is_key_derivable() public helper; wp tmdo doctor [WARN] when AUTH_KEY/SECURE_AUTH_SALT absent +* Fix: phpcs:disable file-level suppressors reduced 37 → 19 via phpcs.xml directory-level exclude-pattern rules +* Docs: README.md + readme.txt Stable tag aligned to v3.3.1 in prior release; CHANGELOG.md backfilled for v3.1.5–v3.3.1 + += 3.3.1 = +* Security: 13 admin GET state-changing operations converted to POST form (nonce-protected) — prevents nonce leakage via Referer header (CVSS 4.3) +* Security: REST endpoints pre-filter Zone A/C fields via show_in_rest registry (CVSS 5.3) +* Fix: phpcs:disable file-level suppressors reduced from 37 to 19 — directory-level exclusions in phpcs.xml replace broad per-file disables +* Fix: TMDO_Crypto::is_key_derivable() public helper; wp tmdo doctor reports WARN when AUTH_KEY/SECURE_AUTH_SALT absent +* Tests: 21 new unit tests for Diagnose, Install_Schema, Backfill_Bulk, Backfill_Unserialize, Verify_Sample phases + += 3.3.0 = +* Refactor: TMDO_Migration_Orchestrator removes final; 18 self::ENTITY_TYPE → static::ENTITY_TYPE for late-static-binding subclass override +* Tests: HookBusIntegrationTest (14 tests) covers intercept_update/get/delete against real MariaDB; tearDownAfterClass clears Schema_Manager table_exists cache + += 3.2.0 = +* Quality: declare(strict_types=1) added to all 181 production PHP files (includes/, admin/, cli/) + += 3.1.9 = +* CI: PHPCS step in test.yml is now a blocking gate (removed || true); release.yml requires full test-gate (lint + phpcs + unit + integration) before publishing +* Security: wp tmdo doctor adds HTTP accessibility probe for backup directory; nginx snippet surfaced when dir returns 200 + += 3.1.8 = +* Security: M-AUTH-1 fix — draft/private/trash posts return 403 to anonymous users in REST get_listing; regression test added +* Fix: TMDO_Conflict_Monitor catch block surfaces errors via TMDO_Logger::warning instead of swallowing silently +* Docs: ADR-001 (post entity source-of-truth contract); ADR-002 (dual_write naming conflict between FSM and Entity Bridge) +* Tests: Migration Phase unit tests for Cleanup RuntimeException guards + 4-phase lifecycle; Mode_Manager cache stubs in bootstrap + += 3.1.7 = +* Perf: Zone Cold save_blob() refactored from 2-query SELECT+INSERT/UPDATE to single INSERT...ON DUPLICATE KEY UPDATE (TMDO_DB::upsert) +* Fix: TMDO_DB::upsert() MySQL branch migrated from deprecated VALUES() to row-alias syntax (INSERT ... AS new_vals ON DUPLICATE KEY UPDATE col = new_vals.col) +* Fix: Same row-alias migration for points_manager, term_comment_misc_bucket, hivepress requests/messages adapters +* Docs: README.md version updated to 3.1.7; Admin UI section updated to reflect 4-group 20-tab layout +* Docs: readme.txt Stable tag updated to 3.1.7; changelogs added for v3.0.1–v3.1.6 + += 3.1.6 = +* Fixed: TMDO_Logger::error() call-site arity — all catch-block callers updated to 3-argument form (module, hook, message) +* Fixed: WP_DATA_OPTIMIZER_VERSION constant now delegates to TMDO_VERSION instead of hardcoded '3.0.0' +* New: Gitea CI now runs 477 integration tests against MariaDB 11.4 on every push +* New: release.yml requires test-gate before publishing a GitHub/Gitea release +* Perf: TMDO_DB::upsert() MySQL branch migrated from deprecated VALUES() to row-alias syntax (MySQL 8.0.20+ / MariaDB 10.3.3+) +* Perf: Zone Warm set() refactored to single-query upsert; new atomic increment() method +* Perf: Zone Cold save_blob() refactored from 2-query select+insert/update to single upsert +* Perf: TMDO_Schema_Manager::table_exists() caches results per request to avoid repeated SHOW TABLES +* Perf: cache_orchestrator L1 eviction O(n) → O(1); ORDER BY RAND() eliminated from verify() +* Security: wpdo_rest_listing_visible_fields filter for field-level REST exposure control +* Security: wpdo_allow_mass_column_clear filter gates delete_all (default false) +* Security: TMDO_Crypto derived_key() returns empty string when salts absent; encrypt/decrypt short-circuit safely +* Chore: phpunit.xml failOnWarning=true; composer.json production classmap autoload + += 3.1.5 = +* Security: Gitea release.yml rewritten — release job now requires test-gate (unit + lint) to pass first +* Chore: capture_before_value filter default changed false to eliminate 2 extra DB queries per managed write + += 3.1.4 = +* New: User EAV ratio optimization — admin_prefs group +14 WP admin UI keys; commerce group +3 WC runtime stats; hp_user group +hp_verified +* Perf: dev10 ratio improved 1:1.73 → 1:1.42; dev20 1:2.09 → 1:1.07; dev21 (Dokan Pro 1.8M vendors) 1:1.00 + += 3.1.3 = +* Fixed: Last hardcoded inline style removed (conflict-detector.php → wpdo-list-disc class) +* New: scripts/install-local-dev.sh rsync deployment script +* Improved: .wpdo-mode-badge CSS transition added + += 3.1.2 = +* Fixed: ci-package.sh now uses rsync staging so ZIP top-level directory is correct 2meet-data-optimizer/ +* Fixed: phpunit-integration.xml excluded from ZIP + += 3.1.1 = +* Fixed: Version header alignment (3.1.0 → 3.1.1) and Gitea DB_PASS secret for integration tests + += 3.1.0 = +* New: scripts/ci-package.sh + .gitea/workflows/release.yml (push v* tag → auto-package + Gitea release) + += 3.0.3 = +* Fixed: wp tmdo doctor partner plugin doctor_callback invocation (call_user_func($cb) → call_user_func($cb, $tbl_raw)); eliminates WooCommerce "Too few arguments" warnings + += 3.0.2 = +* UX: 18 admin files — all inline style= replaced with CSS utility classes +* New: Settings tab split into 7 independent save-section cards with AJAX save +* New: wpdo-admin.css +36 utility classes + += 3.0.1 = +* Architecture: Strategy + Template Method + Zone Router + Partner Registry patterns +* New: 11 injectable Migration Phase objects; TMDO_Standard_Post_Interceptor abstract base; TMDO_Zone_Router dispatch layer +* Fixed: PHPCS 0 errors / 0 warnings project-wide + += 3.0.0 = +* New: HivePress family integration covering 13 official addons (core / blocks / bookings / favorites / marketplace / memberships / messages / requests / reviews / seo / social-links / statistics / tags). Auto-detect, zero overhead when HivePress absent. +* New: 6-command CLI namespace `wp tmdo hivepress {detect,score,doctor,migrate,rollback,benchmark}`. +* New: REST endpoints `GET /wpdo/v1/hivepress/{status,score,health}` (manage_options gated, read-only). +* New: Admin tab `Tools → 2meet Data Optimizer → HivePress 整合`. +* New: 8-dimension anti-EAV suitability scorer with per-adapter + aggregate reporting. +* New: Comment query router (rewrites hp_message recipient lookup, hp_favorite UNIQUE check, etc. — feature flag gated). +* New: Cron optimizer replacing HivePress hourly listing-expiry full scan (feature flag gated). +* New: Conflict guard against legacy hp-custom-tables / hp-info-cards plugins. +* Schema: 8 new zone/shadow tables (hot_hp_request, hot_hp_membership[_plan], hot_hp_booking, comment_hp_{message,favorite,offer}, term_hp_listing_tag). +* Refactor: legacy `class-tmdo-hivepress.php` (242 lines) re-cast as deprecated stub delegating to new core adapter. +* Tests: +144 unit tests + 7 integration tests; full suite 1013 tests / 2469 assertions / 0 failures. +* Security: SQL injection / XSS / capability / ABSPATH guards all audited PASS. + += 2.6.10 = +* Fixed: 壓力測試「取消」無法中斷 — 原本 cancel() 寫入的 cancelled status 會被 in-flight run_batch() 結尾的 update_option() 覆寫回 running(race condition) +* Added: `CANCEL_FLAG` transient — cancel() / cleanup() 設旗標;run_batch() 開頭、run_batch_realistic() 每個 user 迭代之前都檢查;read-modify-write 結尾保留 cancelled status +* Added: 前端 cancel 按鈕點擊立即 UI 反饋(disabled + 「取消中…」),避免使用者再點一次 + += 2.6.9 = +* Fixed: 啟動壓力測試 504 Gateway Timeout — `start()` 不再同步執行 batch(之前 batch=500 + realistic mode 會塞 30 分鐘以上) +* Added: `BATCH_DEADLINE_SEC=8s` wall-clock 上限 — 每個 pump batch 跑滿 8 秒就 yield,下一次 polling 接手;遠低於 nginx 60s timeout +* Added: PHP `set_time_limit()` 在 pump 內動態設定,確保 batch 有足夠時間完成 +* Improved: Realistic mode 預設批次提示 — 前端偵測 batch>10 時提示確認(每 user ~3 秒,batch>10 撞 deadline) +* Improved: Admin UI 啟動後立即顯示進度卡片,不必等第一次 polling + += 2.6.8 = +* Fixed: 壓力測試進度條不更新 — 不再依賴 wp-cron 自動觸發;`get_progress()` 內建 pump 機制,每次 polling 主動推進一個 batch(用 transient lock 防併發) +* Fixed: 啟動測試後立刻同步執行第一個 batch,使用者第一次 polling 即可看到進度 +* Fixed: `finalize()` 完成時清除殘留 cron event +* Fixed: 兩種模式 (fast / realistic) 端到端驗證 — 進度即時推進、Hook Bus 正確攔截、密碼 PassWord2026! 可登入 + += 2.6.7 = +* Added: User Entity 壓力測試 + Benchmark 工具(Tools → 2meet Data Optimizer → 壓力測試)— 設定要建立的測試使用者數,一鍵自動填滿所有 user 相關 flat tables(hot/cold/membership/activity/profile/sso/points_ledger) +* Added: 兩種寫入模式 — `fast`(直接 bulk INSERT,~5-10K users/秒)/ `realistic`(走 wp_insert_user + Hook Bus,測試生產路徑) +* Added: 完整 Benchmark 報告 — 寫入速率 / 批次延遲 / DB 容量(每張表 data_length + index_length)/ 6 種查詢效能(含原生 EAV baseline 對比) +* Added: WP Cron 驅動的批次任務 + 即時進度條(2 秒 polling,顯示 users/sec、ETA、PHP peak memory) +* Added: REST API — `/wpdo/v1/stress-test/{status|start|cancel|cleanup|benchmark}`(皆需 manage_options) +* Added: Admin 新 Tab「壓力測試」附警告 banner,一鍵清除所有 test_* 使用者及其 flat table 資料 + += 2.6.6 = +* Added: Entity Bridge tab in admin (Tools → 2meet Data Optimizer → Entity Bridge) with health cards per entity (user/term/comment) — shows mode, pipeline visualization, per-group coverage bars, shadow diff count, auto-promote status, and guided recommendation +* Added: Async cron-driven Backfill engine (migrate_group_batch) — REST POST /entity-bridge/backfill triggers WP Cron, self-reschedules every 5 s until all EAV rows are migrated to flat table +* Added: REST API endpoints (requires manage_options) — GET/POST /wpdo/v1/entity-bridge/health, /backfill, /promote, /demote +* Added: TMDO_Entity_Health class — aggregates coverage pct, migration checkpoint, shadow diffs, auto-promote eligibility per entity +* Added: wpdo-entity-bridge.js — real-time polling (5 s) during active backfill, inline promote/demote with confirmation dialogs +* Verified: 356 unit / 181 integration / 0 failures; PHPCS 0 errors / 0 warnings + += 2.6.5 = +* Security: A-4 cleanup — removed all plaintext id_token persistence from Spoke (update_user_meta _tmso_last_id_token writes removed); logout now calls delete_user_meta to progressively clean stale values +* Fixed: Integration test isolation — ZoneHotIntegrationTest, ZoneWarmIntegrationTest, WarmArchiveIntegrationTest, SyncBridgeIntegrationTest setUpBeforeClass() now uses DROP TABLE IF EXISTS + CREATE TABLE to guarantee clean schema even after interrupted prior runs; eliminates 65 spurious failures +* Verified: 356 unit / 181 integration / 0 failures; PHPCS 0/0; stress-tested with pre-polluted tables + += 2.6.4 = +* Security: H-4 — AES-256-CBC encryption for Slack/Discord/Telegram webhook secrets via new TMDO_Crypto class; key derived from AUTH_KEY + SECURE_AUTH_SALT; stored as enc:v1:; backward-compat with existing plaintext values +* Security: A-4 — SLO hash migration: Hub stores sha256(id_token) per user+client on token issuance; /oauth/logout accepts id_token_hash_hint with hash_equals() timing-safe comparison; Spoke prefers hash path with legacy JWT fallback +* Admin: webhook inputs use type=password + autocomplete=new-password; empty submission preserves existing encrypted value + += 2.6.3 = +* Security: H-1/H-2 — TMDO_Snapshot_Reader::apply() verifies sha256+size before executing SQL; is_safe_name() enforces $wpdb->prefix +* Security: H-3/M-2 — wpdo_rl_stats capped at 100 entries to prevent unbounded wp_options growth +* Security: M-3 — nginx backup-directory protection notice in admin + README-NGINX.txt with location block +* Security: L-1 — TMDO_DB::table() sanitizes input with sanitize_key() + += 2.6.2 = +* Added: wp tmdo benchmark --custom-tables flag benchmarks all 117 registered custom tables +* Added: TMDO_Site_Metrics_Collector — daily 05:00 UTC cron writes 11 site-wide metrics to wpdo_site_metrics (90-day retention) +* Added: wp tmdo site-metrics CLI command with --collect, --history, --days, --format options + += 2.6.1 = +* Added: E2E test suite T-A01–T-A20 (21 tests): Snapshots tab, Setup Wizard 5-step, Export health+snapshots + += 2.6.0 = +* Added: wp tmdo doctor integrates Custom Table Registry — partner tables shown by provider with status/row-count/doctor_callback +* Added: wp tmdo bridge-status shows dual_write progress: flat table rows vs EAV distinct IDs, progress_% + += 1.3.31 = +* Fixed: PHP WASM / WP Playground compatibility — merged multiple PHP tags on line 604 of admin class to avoid Parse error in PHP WASM 8.3 + += 1.3.30 = +* Added: Zone A covering indexes — TMDO_Installer::add_covering_indexes() auto-generates compound indexes based on column types +* Added: WP-CLI wp tmdo add-indexes — applies covering indexes to existing hot tables as an upgrade path +* Added: EXPLAIN-verified index usage: hp_featured+price filter scans 52 rows vs 477 full table scan +* Benchmark: hp_listing Zone A 3.6x, Zone B 3.6x, Zone C 2.4x (n=200, MariaDB 11.8.2) + += 1.3.29 = +* Fixed: WordPress Coding Standards (WPCS) — zero violations (phpcs exit 0) +* Fixed: All output properly escaped with esc_html(); REMOTE_ADDR sanitized with wp_unslash() + sanitize_text_field() +* Fixed: PreparedSQL phpcs:ignore annotations for validated table names; Yoda conditions corrected +* Added: phpcs.xml ruleset; complete docblocks (@param/@return/@var/@throws) across all classes +* Verified: 291 tests / 492 assertions / 0 failures (PHP 8.1–8.3, MariaDB 10.11–11.4) + += 1.3.28 = +* Added: `readme.txt` for WordPress.org submission (Description, Installation, FAQ, Changelog, Screenshots) +* Verified: 291 tests / 492 assertions / 0 failures (PHP 8.1–8.3, MariaDB 10.11–11.4) + += 1.3.27 = +* Added: SyncBridgeTest (14 unit tests) — full guard condition coverage for intercept_get/update/add/cleanup_post +* Added: CacheLayerTest (9 unit tests) — prefetch(), warm_post(), get_stats() coverage +* Added: Admin Dashboard rate-limit stats card — total 429 events, Top 10 rate-limited posts, reset button (nonce-protected) +* Performance: Zone A hp_vendor n=200: 2.5×; Zone C hp_vendor n=200: 3.1× + += 1.3.26 = +* Added: ZoneArchiveTest (20 unit tests) — archive, archive_batch, get with gzip decompression, restore, delete, stats +* Added: Unit test coverage for RestApi (+5), ZoneCold (+3), ZoneHot (+2), ZoneWarm (+3) +* Added: Cookie-based dedup for POST /view rate limiting (Set-Cookie header, SameSite=Strict) +* Added: Rate-limit stats tracking (wpdo_rl_stats option) in get_status() response +* Added: CLI benchmark — Zone B (Warm) and Zone C (Cold) support added to `wp tmdo benchmark` + += 1.3.25 = +* Added: ZoneHotIntegrationTest, ZoneWarmIntegrationTest (MariaDB), SyncBridgeIntegrationTest, QueryRouterIntegrationTest +* Added: CI GitHub Actions workflow — PHP 8.1/8.2/8.3 × MariaDB 10.11/11.4 with TZ: UTC +* Added: Zone C integration tests (ZoneColdIntegrationTest, 15 tests) +* Added: REST API rate limiting (IP transient, 1 hit/IP/post/hour, HTTP 429) +* Performance: Zone A hp_listing benchmark n=200: 12×; hp_vendor: 3.1× + +== Upgrade Notice == + += 1.3.31 = +Bug fix for PHP WASM / WP Playground environments. Recommended for all users. + += 1.3.30 = +Adds covering indexes for Zone A hot tables. Run `wp tmdo add-indexes` on existing installations to apply. Required for optimal multi-field filter performance. + += 1.3.29 = +WPCS compliance release — zero phpcs violations. Security hardening (escape, sanitize). Upgrade recommended before WordPress.org submission. + += 1.3.28 = +Maintenance release — WordPress.org submission preparation. No functional changes. + += 1.3.27 = +Adds Admin Dashboard rate-limit statistics card. Upgrade recommended for sites using the POST /view endpoint.