# Architecture ## Why one plugin Three plugins shipped the same product surface under three admin menus and shared two cron hooks. Users installed, updated and deactivated them independently while the code assumed all three were present. Consolidating into one plugin makes the dependency explicit and lets one activation hook set up a consistent state. The trade-off is that three independent `add_menu_page` calls, three sets of admin assets and three activation routines now share a single namespace. Every one of those collided, and each collision is recorded in `KNOWN-ISSUES.md`. ## Layers ``` umbra-bookshelf.php (bootstrap) ├── constants one UMBRA_BOOKSHELF_* set ├── requires 21 class files, three modules ├── plugins_loaded construct modules ├── init text domain └── admin_menu(9) single top-level menu │ ┌──────────────────────────┼──────────────────────────┐ │ │ │ includes/books includes/library includes/email Umbra_Database *owns bd_books/bd_series* Umbra_Simple_Email Umbra_Book_Manager …_Helper_Admin …_Admin Umbra_Authors …_Helper_Install …_Install Umbra_Series Umbra_Email_Sender Umbra_Retail_Sites Umbra_Rating_Sync Umbra_Shortcodes Umbra_SEO assets: admin/, public/, library/, email/ Umbra_GDPR assets/ Umbra_Goodreads_Scraper (only scraper) Umbra_Admin Umbra_Migration Umbra_Theme_Compatibility -> class_alias BD_BookShelfHelper includes/class-umbra-schema-migration.php reconciles pre-consolidation schemas ``` ## Design rules **One owner per name.** Every class, constant, asset handle, menu slug and AJAX action is owned by exactly one module. The bootstrap owns the top-level menu and the shared constants; modules own their submenus and assets. **Persistence is not part of the identity.** Anything written to the database or embedded in published content keeps its historical name. The rename covers headers, text domains, classes, constants, handles, slugs and JS globals — never table names, option keys, meta keys, cron hooks, nonces, shortcodes, post types, taxonomies or cookies. See §2.2 of the specification for the full list. **Registration order is explicit.** The top-level menu registers at `admin_menu` priority 9 so the parent exists before modules append submenus at the default 10. Modules construct on `plugins_loaded` so their own `init` callbacks are registered before `init` fires. **Compatibility shims are explicit.** `Umbra_Theme_Compatibility` exposes the static API the unmodified theme calls, and re-exports the legacy `BD_BookShelfHelper` name via `class_alias()`. **One owner per table.** `bd_books` and `bd_series` belong to the catalogue. The library module deliberately no longer creates them. Where an installation was built on the old library schema, `Umbra_Schema_Migration` renames the old table aside, creates the canonical one, and copies rows across preserving IDs. **Schema work happens before `dbDelta`, never after.** `dbDelta` adds columns but never drops them, so pointing it at a table of the wrong shape produces a table that satisfies neither schema. Reconciliation therefore runs first, and the migration is what guarantees the invariant. ## Still to reconcile Making the catalogue schema canonical removed the *table* conflict. The two modules still model a book differently — the catalogue as a row in `bd_books`, the library as a `book` custom post type with per-post meta — and nothing keeps them in step. See `TASKS.md` §5.