docs: 移植 readme.txt / CONTEXT.md / docs(backport A v3.4.6)
Tests / Integration Tests (push) Successful in 1m11s
Tests / Unit Tests (push) Failing after 11m56s
Anti-EAV Lint + Quality Gate / anti-eav-lint (push) Failing after 12m7s
Tests / PHPStan (push) Failing after 14m37s
Tests / PHPCS (push) Failing after 14m46s
Tests / PHP Lint (push) Failing after 14m57s

- 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 一律保留 —— 這是資料層
零遷移的前提。
This commit is contained in:
2026-07-31 10:06:03 +08:00
parent 2203bc471c
commit b63ab46f54
8 changed files with 1564 additions and 0 deletions
+324
View File
@@ -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 L6baseline 710)· gitea CI 6 job 全綠。
> v1.0.0 取代 `wp-data-optimizer` v3.4.6,該外掛已退休(本機目錄改名 `.retired`,git 歷史留在 gitea)。版號自 1.0.0 重啟。
> **本外掛只有通用引擎**4 entitypost / 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 containerintegration 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` | 0N | WP authentication reads sessions directly; Hook Bus cannot intercept session creation safely |
| `_application_passwords` | 01 | 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`.