- 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 一律保留 —— 這是資料層 零遷移的前提。
15 KiB
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-optimizerv3.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/v1REST 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 viaadd_filter('hivepress/v1/extensions', …)). SeeTMDO_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 labelubuntu-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'sstate,log, andmetrics— 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:
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.
['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:
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.