b63ab46f54
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 一律保留 —— 這是資料層 零遷移的前提。
325 lines
15 KiB
Markdown
325 lines
15 KiB
Markdown
# 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`.
|