Files
2meet-data-optimizer/CONTEXT.md
T
wpdev 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
docs: 移植 readme.txt / CONTEXT.md / docs(backport A v3.4.6)
- 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 一律保留 —— 這是資料層
零遷移的前提。
2026-07-31 10:06:03 +08:00

325 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.