diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7528141..49624df 100755 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -10,8 +10,8 @@ Peptide Search AI is a WordPress plugin that transforms sites into intelligent, ``` peptide-search-ai/ -├── peptide-search-ai.php Main plugin file; defines constants, hooks init/admin_init -├── uninstall.php Cleans up all peptide posts, meta, options, transients +├── peptide-search-ai.php Main plugin file; defines constants, hooks init/admin_init, gates CPT boot paths on PSA_Dependency_Check +├── uninstall.php Cleans up PSA-owned options, transients, and the `wp_psa_api_logs` table (v4.5.0+ does NOT touch peptide posts/terms — owned by PR Core) ├── readme.txt Plugin directory listing (WordPress.org metadata) ├── composer.json PHP dependencies; PSR-4 autoloader for includes/ ├── .phpcs.xml.dist WordPress Coding Standards (WPCS) configuration @@ -20,7 +20,8 @@ peptide-search-ai/ ├── includes/ │ ├── class-psa-config.php Configuration constants (rate limits, timeouts, tokens, TTL) │ ├── class-psa-encryption.php AES-256-CBC encryption for API keys at rest -│ ├── class-psa-post-type.php CPT registration, peptide_category taxonomy, meta field definitions +│ ├── class-psa-dependency-check.php PR Core >= 0.2.0 dependency gate + admin notice (v4.5.0) +│ ├── class-psa-post-type.php Admin columns + `psa_*` meta key definitions on the `peptide` CPT owned by PR Core │ ├── class-psa-search.php AJAX search, REST API route, shortcode rendering, cache mgmt │ ├── class-psa-directory.php Browsable directory: [peptide_directory] shortcode + /v1/compounds REST endpoint │ ├── class-psa-ai-generator.php AI pipeline orchestrator (validation, generation, category assignment, meta save) @@ -214,6 +215,9 @@ graph TD ### 1. Custom Post Type + Post Meta vs. Custom Tables **Decision:** Use CPT (`post_type='peptide'`) with post meta fields (`psa_*`). +Registration moved from PSA to Peptide Repo Core (PR Core) in PSA v4.5.0 — +see the "Post types" section above. The CPT-vs-custom-tables choice itself +still holds; only ownership of the registration moved. **Rationale:** - **WordPress integration:** Leverage native post, user, revision, and permission systems. @@ -274,6 +278,23 @@ graph TD - PubChem cache: 7 days for hits, 12 hours for misses - Rate limit counters: 1 hour per request window +## Post types + +PSA no longer registers any post type. The `peptide` CPT and `peptide_category` +taxonomy are owned by Peptide Repo Core (PR Core). PSA depends on PR Core ≥ 0.2.0 +for these to be available. + +PSA still owns: +- Meta boxes attached to the `peptide` CPT (psa_peptide_data, psa_extended_data) +- The `psa_*` meta key namespace on peptide posts +- Search index, directory widget, KB renderer, search REST endpoints + +Dependency is enforced at boot by `PSA_Dependency_Check::is_satisfied()` — when +PR Core < 0.2.0 or is absent, PSA renders an admin notice and all CPT-bound +surfaces (meta boxes, admin columns, directory, KB renderer, search, single- +peptide template) skip registration. Non-CPT surfaces (settings page, upgrade +driver) still boot so the admin can read the notice and configure the plugin. + ## Data Storage ### Post Meta Fields (PSA_Post_Type::META_FIELDS) @@ -307,13 +328,19 @@ All stored on peptide posts as post meta: | `psa_cycle_parameters` | Cycle/protocol phase info | Text | AI (v4.3.0) | | `psa_amino_acid_count` | Number of amino acids | Integer | Derived from sequence (v4.3.0) | -### Taxonomy: peptide_category (v4.3.0) +### Taxonomy: peptide_category -Hierarchical taxonomy registered on the `peptide` CPT. Pre-populated terms: -Tissue Repair, Lipid Metabolism, Aging Research, Dermatological, Metabolic, Growth Hormone, Immunology, Endocrine. +Hierarchical taxonomy on the `peptide` CPT. **Registered by PR Core** (>= 0.2.0) +as of PSA v4.5.0 — PSA no longer calls `register_taxonomy( 'peptide_category', … )`. +The 8 terms seeded by earlier PSA versions (Tissue Repair, Lipid Metabolism, Aging +Research, Dermatological, Metabolic, Growth Hormone, Immunology, Endocrine) stay +intact because `wp_term_taxonomy` is keyed on the taxonomy name, which is +unchanged. -Auto-assigned during AI generation via `PSA_AI_Generator::assign_category_term()`. -Migration for existing peptides via admin "Migrate Existing Peptides to Categories" button. +PSA continues to *read* and *write* term assignments on peptide posts — any +plugin can operate on a public taxonomy. `PSA_AI_Generator::assign_category_term()` +still auto-assigns during AI generation; the admin "Migrate Existing Peptides +to Categories" button still runs. ### Integration Hooks (v4.3.0) @@ -434,40 +461,4 @@ The plugin stores the schema version it last migrated to in a separate option, ` ## Performance Optimizations -- **Post caching:** `_prime_post_caches()` and `update_meta_cache()` batch-load posts and meta (lines 458–459 in class-psa-search.php). -- **Transient caching:** Search results cached 6 hours; invalidated on post save. -- **Conditional asset loading:** CSS/JS only loaded on relevant pages. -- **Lazy PubChem:** Optional, can be disabled via setting. -- **Daily generation cap:** 50/day to limit API costs. -- **Separate validation model:** Fast/cheap model for upfront validation to reduce full-generation calls. - -## Testing - -**CI Pipeline:** `.github/workflows/ci.yml` -- PHPUnit tests across PHP 7.4, 8.1, 8.3 -- PHP syntax checks across PHP 7.4, 8.1, 8.3 -- WordPress Coding Standards (PHPCS) validation -- JavaScript syntax checks (Node.js) - -**Post-Deploy:** `.github/workflows/deploy.yml` -- HTTP health check verifies site returns 200 -- REST API smoke test verifies `/wp-json/peptides/v1/search?q=BPC-157` responds - -**Manual Testing Checklist:** -- Search for existing peptide → results appear -- Search for new peptide (valid) → "pending" message → check back → content generated -- Search for invalid peptide → "not recognized" message -- Test rate limits (10/hour AJAX, 20/hour REST API) -- Verify PubChem data merges correctly -- Test auto-publish on/off -- Verify shortcode works multiple times on same page -- Test on mobile (600px breakpoint) - -## Deployment - -See `.github/workflows/deploy.yml`: -1. Push to main triggers CI tests. -2. If tests pass, validate deployment readiness (required files, no sensitive data). -3. Sync clean files to orphan `deploy` branch. -4. SSH to Hostinger, git pull `deploy` branch into plugin directory. -5. Verify deployment (check version, required files exist). +- **Post caching:** `_prime_post_caches()` and `update_meta_cache()` batch-load posts and meta (lines 458–4 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 55fbaa4..f62606a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ All notable changes to the Peptide Search AI plugin will be documented in this f The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.5.0] - 2026-04-22 + +### Changed (BREAKING) +- Dropped registration of the `peptide` custom post type and `peptide_category` taxonomy. These are now owned by Peptide Repo Core (PR Core) v0.2.0+, the canonical peptide data layer. Existing posts and term relationships are unaffected — they continue to work with PR Core's registration. +- PSA now has a hard dependency on PR Core ≥ 0.2.0. An admin notice appears if the dependency is not met; PSA features that operate on the `peptide` CPT (meta boxes, admin columns, directory widget, KB renderer, search widget, single-peptide template) degrade gracefully until PR Core is updated. +- `psa_activate()` no longer seeds default `peptide_category` terms — PR Core inherits the 8 existing terms via taxonomy-name keying in `wp_term_taxonomy`. +- `uninstall.php` no longer deletes `peptide` posts or `peptide_category` terms. Those entities are owned by PR Core as of this release; removing them on PSA uninstall would break cross-plugin contracts. PSA uninstall now only removes PSA's own options, transients, and the `wp_psa_api_logs` table. + +### Added +- `PSA_Dependency_Check` class (`includes/class-psa-dependency-check.php`): verifies `PR_CORE_VERSION >= 0.2.0`, renders an admin notice when unmet, and is used as a boot-path gate by CPT-dependent PSA classes. +- PHPUnit coverage for `PSA_Dependency_Check::is_satisfied()` across undefined / 0.1.1 / 0.2.0 / 0.3.0 version scenarios plus a regression assertion that `PSA_Post_Type` no longer exposes `register_post_type` or `register_taxonomy` call paths. + +### Removed +- `PSA_Post_Type::register_peptide_post_type()` method. +- `PSA_Post_Type::register_taxonomy()` method. +- `PSA_Post_Type::populate_default_categories()` method and its activation hook (seeding is a data-layer concern now owned by PR Core; the 8 existing terms remain in `wp_term_taxonomy`). +- Note: the `PSA_Post_Type::DEFAULT_CATEGORIES` constant is **retained** — PSA still consumes it as a slug→name reference list in `PSA_AI_Content::assign_category_term()` and the admin "Migrate Existing Peptides to Categories" button. Its role changed from "seed list" to "consumer lookup"; the value is unchanged. + +### Unchanged +- Meta boxes (`psa_peptide_data`, `psa_extended_data`) and all `psa_*` meta keys on peptide posts. +- Directory shortcode `[peptide_directory]` and its `/wp-json/peptide-search-ai/v1/compounds` REST endpoint. +- Single-peptide template (`PSA_Template`) and KB article renderer (`PSA_KB_Builder`). +- Search widget, `/wp-json/peptides/v1/search` REST endpoint, and AJAX search handler. +- Cost tracker, API logs table schema, and the `psa_db_version` option. + +### Migration notes +- No data migration required. The existing `peptide` posts and all `peptide_category` term assignments remain intact and functional — PR Core registers the same post-type name (`peptide`) and taxonomy name (`peptide_category`), so `wp_posts.post_type` and `wp_term_taxonomy.taxonomy` continue to resolve. +- After upgrading, ensure PR Core is at 0.2.0 or later — otherwise PSA surfaces an admin notice and degrades the peptide-dependent features until PR Core is updated. +- Deploy order (coordinated release): PR Core v0.2.0 ships first and restores the peptide detail pages on its own (PSA v4.4.3's existing `post_type_exists( 'peptide' )` guard no-ops its registration once PR Core claims the CPT). PSA v4.5.0 ships second as cleanup — no user-visible change at that point. + ## [4.4.3] - 2026-04-14 ### Fixed @@ -63,77 +93,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `PSA_AI_Generator` now delegates to `PSA_OpenRouter` and `PSA_KB_Builder` (reduced from 827 to ~470 lines). - Updated test suite to call `PSA_OpenRouter::parse_response()` instead of removed private method. -- Updated `ARCHITECTURE.md` and `CONVENTIONS.md` to reflect new class structure. -- Bumped minimum PHP recommendation to 7.4+ with strict type enforcement. - -### Removed -- Deprecated `PSA_Error` class and `class-psa-error.php` (unused, no callers). - -## [4.1.0] - 2026-04-10 - -### Added -- AES-256-CBC encryption for API keys stored in the database (`PSA_Encryption` class). -- Cost/token tracking with `wp_psa_api_logs` custom table (`PSA_Cost_Tracker` class). -- Monthly budget system with hard-stop enforcement. -- Dry-run mode for cost estimation before API calls. -- Admin UI for API usage summary and recent call logs. -- Monthly budget configuration field in settings. - -### Fixed -- PHPCS `PreparedSQL` errors in cost tracker queries. - -## [4.0.0] - 2026-04-10 - -### Security -- **SEC-001 CRITICAL:** Removed spoofable `X-Forwarded-For` / `X-Real-IP` from IP detection; now trusts only `CF-Connecting-IP` + `REMOTE_ADDR`. -- **SEC-004 HIGH:** Added peptide name input validation with character whitelist and prompt injection blocking. -- **SEC-006 HIGH:** Added missing `return` after `wp_send_json_success` in pending retry max-retries block. -- **SEC-007 MEDIUM:** KB articles now respect the `auto_publish` setting instead of always publishing. -- **SEC-009 MEDIUM:** Replaced hardcoded page ID 73 with configurable `kb_page_id` setting. -- **SEC-010 MEDIUM:** Fixed cache invalidation transient name pattern (added underscore prefix). -- **SEC-011 MEDIUM:** Skipped `sleep()` during WP-Cron to avoid blocking PHP workers on rate limit retry. -- **SEC-012 MEDIUM:** Added global daily generation cap (50/day) to prevent cost overruns. -- **SEC-013 LOW:** Added `@deprecated` notice to unused `PSA_Error` class. - -### Added -- PHPUnit test infrastructure with `bootstrap.php` and 20 unit tests across 2 test files. -- `ARCHITECTURE.md` and `CONVENTIONS.md` documentation. - -### Fixed -- JavaScript typos breaking form submit and network error handling. -- CSS missing semicolons. -- PHPCS compliance: short ternaries, array formatting, Yoda conditions, tab indentation. - -## [3.0.0] - 2026-04-09 - -### Added -- Dual-layer rate limiting (transient + object cache) for AJAX and REST endpoints. -- Cache generation counter for O(1) cache invalidation instead of expensive DELETE queries. -- PubChem data enrichment with molecular weight, formula, SMILES, and InChI. -- Background generation via WP-Cron (non-blocking). -- Retry logic with configurable max attempts for stale pending peptides. -- REST API endpoint (`/wp-json/peptides/v1/search`) for headless access. -- Multiple shortcode instances on the same page via `data-psa-instance` attribute. - -### Changed -- Search results cached with generation counter for fast invalidation. -- Batch post/meta priming to avoid N+1 queries in search results. - -## [2.0.0] - 2026-04-08 - -### Added -- AI-powered peptide name validation (lightweight model before full generation). -- OpenRouter API integration for multi-model support. -- Custom Post Type (`peptide`) with 15+ meta fields. -- Admin settings page for API key, model selection, and behavior configuration. -- Single peptide page template with quick-facts table and source badges. -- Echo Knowledge Base article auto-creation from generated content. -- CSS design system with `.psa-*` namespace and BEM-inspired structure. - -## [1.0.0] - 2026-04-07 - -### Added -- Initial release. -- Basic peptide search shortcode. -- AJAX-powered search with debounced input. -- Responsive mobile layout (600px breakpoint). +- Updated \ No newline at end of file diff --git a/includes/class-psa-dependency-check.php b/includes/class-psa-dependency-check.php new file mode 100644 index 0000000..f3d4450 --- /dev/null +++ b/includes/class-psa-dependency-check.php @@ -0,0 +1,81 @@ += REQUIRED_PR_CORE_VERSION is active so PSA can rely + * on the `peptide` CPT + `peptide_category` taxonomy being registered. Renders + * an admin notice when the dependency is unmet. Serves as a boot-path gate: + * PSA surfaces that operate on the `peptide` CPT (meta boxes, admin columns, + * directory widget, KB renderer, search widget, template) call + * is_satisfied() and degrade gracefully — they do not fatal — when PR Core is + * missing or out of date. + * + * Who calls it: Main plugin bootstrap (`psa_init` + `psa_admin_init`) and each + * CPT-dependent class's `init()` method before hooking into WordPress. + * Dependencies: PR Core defines the PR_CORE_VERSION constant in its main + * plugin file; absence of the constant is treated as "not installed." + * + * @package PeptideSearchAI + * @since 4.5.0 + * @see includes/class-psa-post-type.php — Gated via init_admin(). + * @see includes/class-psa-directory.php — Gated via init(). + * @see includes/class-psa-template.php — Gated via init(). + * @see includes/class-psa-search.php — Gated via init(). + */ +declare( strict_types=1 ); + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +class PSA_Dependency_Check { + + /** + * Minimum PR Core version that registers the `peptide` CPT + `peptide_category` + * taxonomy in the shape PSA depends on. Coordinated release: see + * `convo/peptidesearch/threads/2026-04-peptide-cpt-consolidation/`. + */ + const REQUIRED_PR_CORE_VERSION = '0.2.0'; + + /** Register the admin notice hook. Safe to call multiple times. */ + public static function init(): void { + add_action( 'admin_notices', array( __CLASS__, 'maybe_render_notice' ) ); + } + + /** + * Whether PR Core is installed, active, and at a version PSA accepts. + * + * Pure function — no side effects. Safe to call from any boot-path gate. + * + * @return bool True when PR_CORE_VERSION is defined and >= REQUIRED_PR_CORE_VERSION. + */ + public static function is_satisfied(): bool { + if ( ! defined( 'PR_CORE_VERSION' ) ) { + return false; + } + return version_compare( (string) constant( 'PR_CORE_VERSION' ), self::REQUIRED_PR_CORE_VERSION, '>=' ); + } + + /** + * Render the admin notice when the dependency is unmet. + * + * Hooked to `admin_notices`. Skips rendering when the dependency is + * satisfied and when the current user lacks the `activate_plugins` + * capability (the only role that can act on the notice). + */ + public static function maybe_render_notice(): void { + if ( self::is_satisfied() ) { + return; + } + if ( ! function_exists( 'current_user_can' ) || ! current_user_can( 'activate_plugins' ) ) { + return; + } + + $msg = esc_html__( + 'Peptide Search AI requires Peptide Repo Core 0.2.0 or newer for peptide data (custom post type and taxonomy). Please install or update Peptide Repo Core — until then, peptide meta boxes, the directory widget, and the search widget are disabled.', + 'peptide-search-ai' + ); + + printf( '

%s

', $msg ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $msg already escaped via esc_html__. + } +} diff --git a/includes/class-psa-post-type.php b/includes/class-psa-post-type.php index 37dd354..6cc00b4 100755 --- a/includes/class-psa-post-type.php +++ b/includes/class-psa-post-type.php @@ -1,16 +1,26 @@ = 0.2.0 — PSA no longer registers them. This + * class now wires PSA-owned admin surfaces (meta boxes, admin columns) onto + * the CPT that PR Core registers, and exposes the `psa_*` meta key namespace + * via constants for the rest of PSA to consume. + * + * What: Admin columns, meta key definitions, get_all_meta_keys() helper, and + * backward-compatible proxies to PSA_Post_Type_Meta. + * Who calls it: `psa_admin_init()` in the main plugin file, gated by + * PSA_Dependency_Check::is_satisfied() so admin surfaces don't bind against + * a missing CPT when PR Core is absent. + * Dependencies: PR Core >= 0.2.0 for the CPT itself (runtime); WordPress + * meta-box + admin-column APIs; PSA_Post_Type_Meta for meta box rendering. * * @package PeptideSearchAI - * @since 1.0.0 - * @see includes/class-psa-post-type-meta.php — Meta box rendering and save handlers. - * @see includes/class-psa-ai-content.php — Uses DEFAULT_CATEGORIES for category assignment. - * @see includes/class-psa-directory.php — Reads taxonomy for directory filtering. + * @since 1.0.0 (CPT registration removed in 4.5.0) + * @see includes/class-psa-post-type-meta.php — Meta box rendering and save handlers. + * @see includes/class-psa-dependency-check.php — PR Core gate used by init_admin(). + * @see includes/class-psa-directory.php — Reads `peptide_category` terms for directory filtering. */ declare( strict_types=1 ); @@ -20,7 +30,16 @@ if ( ! defined( 'ABSPATH' ) ) { class PSA_Post_Type { - /** Default peptide category terms pre-populated on activation. slug => display name. */ + /** + * Canonical slug → display-name map for the peptide categories PSA's + * AI and admin tools know how to assign terms to. This is a consumer + * reference list (used by PSA_AI_Content::assign_category_term() and + * the "Migrate Existing Peptides to Categories" admin button) — not a + * seed list. As of PSA v4.5.0 PR Core >= 0.2.0 owns the + * `peptide_category` taxonomy; the 8 terms below were seeded by PSA + * in pre-v4.5.0 releases and remain intact in `wp_term_taxonomy`. PSA + * no longer seeds them on activation. + */ const DEFAULT_CATEGORIES = array( 'tissue-repair' => 'Tissue Repair', 'lipid-metabolism' => 'Lipid Metabolism', @@ -60,7 +79,14 @@ class PSA_Post_Type { 'psa_amino_acid_count' => 'Amino Acid Count', ); - /** Register admin-only hooks (meta boxes, save, admin columns). */ + /** + * Register admin-only hooks (meta boxes, save, admin columns). + * + * Caller (`psa_admin_init`) gates this behind + * PSA_Dependency_Check::is_satisfied() so meta boxes + admin columns + * don't attempt to bind against a `peptide` CPT that PR Core has not + * yet registered. + */ public static function init_admin(): void { add_action( 'add_meta_boxes', array( 'PSA_Post_Type_Meta', 'add_meta_boxes' ) ); add_action( 'save_post_peptide', array( 'PSA_Post_Type_Meta', 'save_meta' ) ); @@ -68,72 +94,6 @@ class PSA_Post_Type { add_action( 'manage_peptide_posts_custom_column', array( __CLASS__, 'render_admin_column' ), 10, 2 ); } - /** Register the peptide CPT. */ - public static function register_peptide_post_type(): void { - if ( post_type_exists( 'peptide' ) ) { - return; - } - - register_post_type( 'peptide', array( - 'labels' => array( - 'name' => 'Peptides', - 'singular_name' => 'Peptide', - 'menu_name' => 'Peptides', - 'add_new' => 'Add Peptide', - 'add_new_item' => 'Add New Peptide', - 'edit_item' => 'Edit Peptide', - 'new_item' => 'New Peptide', - 'view_item' => 'View Peptide', - 'search_items' => 'Search Peptides', - 'not_found' => 'No peptides found', - 'not_found_in_trash' => 'No peptides found in Trash', - ), - 'public' => true, - 'has_archive' => true, - 'rewrite' => array( 'slug' => 'peptides' ), - 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ), - 'menu_icon' => 'dashicons-database', - 'show_in_rest' => true, - 'capability_type' => 'post', - 'hierarchical' => false, - ) ); - } - - /** Register the peptide_category taxonomy on the peptide CPT. */ - public static function register_taxonomy(): void { - if ( taxonomy_exists( 'peptide_category' ) ) { - return; - } - - register_taxonomy( 'peptide_category', 'peptide', array( - 'labels' => array( - 'name' => __( 'Peptide Categories', 'peptide-search-ai' ), - 'singular_name' => __( 'Peptide Category', 'peptide-search-ai' ), - 'search_items' => __( 'Search Categories', 'peptide-search-ai' ), - 'all_items' => __( 'All Categories', 'peptide-search-ai' ), - 'edit_item' => __( 'Edit Category', 'peptide-search-ai' ), - 'update_item' => __( 'Update Category', 'peptide-search-ai' ), - 'add_new_item' => __( 'Add New Category', 'peptide-search-ai' ), - 'new_item_name' => __( 'New Category Name', 'peptide-search-ai' ), - 'menu_name' => __( 'Categories', 'peptide-search-ai' ), - ), - 'hierarchical' => true, - 'public' => true, - 'show_in_rest' => true, - 'show_admin_column' => true, - 'rewrite' => array( 'slug' => 'peptide-category' ), - ) ); - } - - /** Pre-populate default category terms on activation. Idempotent. */ - public static function populate_default_categories(): void { - foreach ( self::DEFAULT_CATEGORIES as $slug => $name ) { - if ( ! term_exists( $slug, 'peptide_category' ) ) { - wp_insert_term( $name, 'peptide_category', array( 'slug' => $slug ) ); - } - } - } - /** * Add custom columns to the peptide admin list. * diff --git a/peptide-search-ai.php b/peptide-search-ai.php index 4acb4c5..229d42d 100755 --- a/peptide-search-ai.php +++ b/peptide-search-ai.php @@ -3,7 +3,7 @@ * Plugin Name: Peptide Search AI * Plugin URI: https://example.com/peptide-search-ai * Description: Searchable peptide database with AI-powered auto-population and browsable directory. - * Version: 4.4.3 + * Version: 4.5.0 * Author: Terence * License: GPL v2 or later * Text Domain: peptide-search-ai @@ -15,13 +15,14 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } -define( 'PSA_VERSION', '4.4.3' ); +define( 'PSA_VERSION', '4.5.0' ); define( 'PSA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PSA_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'PSA_PLUGIN_FILE', __FILE__ ); require_once PSA_PLUGIN_DIR . 'includes/class-psa-config.php'; require_once PSA_PLUGIN_DIR . 'includes/class-psa-encryption.php'; +require_once PSA_PLUGIN_DIR . 'includes/class-psa-dependency-check.php'; require_once PSA_PLUGIN_DIR . 'includes/class-psa-post-type.php'; require_once PSA_PLUGIN_DIR . 'includes/class-psa-post-type-meta.php'; require_once PSA_PLUGIN_DIR . 'includes/class-psa-search-handler.php'; @@ -42,11 +43,20 @@ require_once PSA_PLUGIN_DIR . 'includes/class-psa-directory.php'; require_once PSA_PLUGIN_DIR . 'includes/class-psa-upgrade.php'; function psa_init() { - PSA_Post_Type::register_peptide_post_type(); - PSA_Post_Type::register_taxonomy(); + // Settings + dependency notice always boot so the admin can see plugin + // state and act on a missing PR Core dependency. + PSA_Admin::init(); + PSA_Dependency_Check::init(); + + // PR Core >= 0.2.0 owns `peptide` CPT + `peptide_category` taxonomy as + // of v4.5.0. Gate CPT-dependent surfaces so PSA degrades gracefully + // instead of binding hooks against an unregistered post type. + if ( ! PSA_Dependency_Check::is_satisfied() ) { + return; + } + PSA_Search::init(); PSA_AI_Generator::init(); - PSA_Admin::init(); PSA_Batch_Enrichment::init(); PSA_Template::init(); PSA_Directory::init(); @@ -54,8 +64,15 @@ function psa_init() { add_action( 'init', 'psa_init' ); function psa_admin_init() { - PSA_Post_Type::init_admin(); PSA_Upgrade::maybe_run(); + + // Meta boxes + admin columns bind to the `peptide` CPT; only register + // when PR Core has provided it. + if ( ! PSA_Dependency_Check::is_satisfied() ) { + return; + } + + PSA_Post_Type::init_admin(); } add_action( 'admin_init', 'psa_admin_init' ); @@ -233,9 +250,13 @@ function psa_replace_kb_search() { add_action( 'wp_footer', 'psa_replace_kb_search' ); function psa_activate() { - PSA_Post_Type::register_peptide_post_type(); - PSA_Post_Type::register_taxonomy(); - PSA_Post_Type::populate_default_categories(); + // As of PSA v4.5.0, PR Core (>= 0.2.0) owns the `peptide` CPT and + // `peptide_category` taxonomy — PSA no longer registers them here. The + // 8 default category terms were seeded on earlier PSA versions and + // remain in wp_term_taxonomy; PR Core inherits them by registering the + // same taxonomy name, so no re-seed is required. PSA only owns its + // own cost-tracker table and still needs to flush rewrite rules on + // activation so the `peptide` CPT permalinks resolve on this install. PSA_Cost_Tracker::create_table(); flush_rewrite_rules(); } diff --git a/tests/unit/DependencyCheckTest.php b/tests/unit/DependencyCheckTest.php new file mode 100644 index 0000000..fdc229e --- /dev/null +++ b/tests/unit/DependencyCheckTest.php @@ -0,0 +1,200 @@ + REQUIRED_PR_CORE_VERSION. + * - Regression guard: PSA_Post_Type no longer declares + * register_peptide_post_type / register_taxonomy / populate_default_categories. + * + * PHP constants are defined process-wide and cannot be redefined once set, so + * each version-value variant runs in its own separate process via + * @runInSeparateProcess + @preserveGlobalState disabled. That gives each case + * a clean state to define (or not define) PR_CORE_VERSION. + * + * @package PeptideSearchAI + */ + +declare( strict_types=1 ); + +// Stub WordPress admin-notice helpers needed by maybe_render_notice() and the +// class autoloader. Pure stubs; no assertions run against them here — the +// maybe_render_notice() path is exercised only indirectly via is_satisfied(). +if ( ! function_exists( 'add_action' ) ) { + function add_action() {} +} +if ( ! function_exists( 'current_user_can' ) ) { + function current_user_can( $cap ) { return true; } +} +if ( ! function_exists( 'esc_html__' ) ) { + function esc_html__( $text, $domain = 'default' ) { + return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' ); + } +} + +require_once dirname( __DIR__ ) . '/bootstrap.php'; +require_once ABSPATH . 'includes/class-psa-dependency-check.php'; + +class DependencyCheckTest extends \PHPUnit\Framework\TestCase { + + /** + * REQUIRED_PR_CORE_VERSION is the contract — changing it is a breaking + * coordination event with PR Core. Assert it here so an accidental bump + * trips CI. + */ + public function test_required_pr_core_version_constant(): void { + $this->assertSame( + '0.2.0', + PSA_Dependency_Check::REQUIRED_PR_CORE_VERSION, + 'REQUIRED_PR_CORE_VERSION is a contract with PR Core; changing it must be a coordinated PR across both plugins.' + ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_is_satisfied_false_when_pr_core_version_undefined(): void { + require_once dirname( __DIR__, 2 ) . '/includes/class-psa-dependency-check.php'; + + $this->assertFalse( + defined( 'PR_CORE_VERSION' ), + 'Precondition: PR_CORE_VERSION must not be defined in this process.' + ); + $this->assertFalse( PSA_Dependency_Check::is_satisfied() ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_is_satisfied_false_when_pr_core_version_below_required(): void { + define( 'PR_CORE_VERSION', '0.1.1' ); + require_once dirname( __DIR__, 2 ) . '/includes/class-psa-dependency-check.php'; + + $this->assertFalse( PSA_Dependency_Check::is_satisfied() ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_is_satisfied_true_when_pr_core_version_exactly_required(): void { + define( 'PR_CORE_VERSION', '0.2.0' ); + require_once dirname( __DIR__, 2 ) . '/includes/class-psa-dependency-check.php'; + + $this->assertTrue( PSA_Dependency_Check::is_satisfied() ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_is_satisfied_true_when_pr_core_version_above_required(): void { + define( 'PR_CORE_VERSION', '0.3.0' ); + require_once dirname( __DIR__, 2 ) . '/includes/class-psa-dependency-check.php'; + + $this->assertTrue( PSA_Dependency_Check::is_satisfied() ); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_is_satisfied_false_for_pre_release_below_required(): void { + // Defensive: PHP's version_compare treats "0.2.0-alpha" as < "0.2.0", + // which is the desired behaviour here. Regression guard against the + // constant silently accepting pre-release PR Core builds. + define( 'PR_CORE_VERSION', '0.2.0-alpha' ); + require_once dirname( __DIR__, 2 ) . '/includes/class-psa-dependency-check.php'; + + $this->assertFalse( PSA_Dependency_Check::is_satisfied() ); + } + + /** + * Regression guard for the v4.5.0 cleanup: PSA_Post_Type must no longer + * expose the three removed registration methods. A future refactor that + * re-introduces any of them would re-create the CPT collision with PR + * Core and break the production site. + */ + public function test_psa_post_type_registration_methods_removed(): void { + // Stub the minimum WP surface PSA_Post_Type touches at class-load + // time so we can reflect on it here without pulling the full bootstrap. + if ( ! function_exists( 'post_type_exists' ) ) { + function post_type_exists( $post_type ) { return false; } + } + if ( ! function_exists( 'register_post_type' ) ) { + function register_post_type( $post_type, $args = array() ) { return null; } + } + if ( ! function_exists( 'taxonomy_exists' ) ) { + function taxonomy_exists( $taxonomy ) { return false; } + } + if ( ! function_exists( 'register_taxonomy' ) ) { + function register_taxonomy( $taxonomy, $object_type, $args = array() ) { return null; } + } + if ( ! function_exists( 'add_filter' ) ) { + function add_filter() {} + } + if ( ! function_exists( '__' ) ) { + function __( $text, $domain = 'default' ) { return $text; } + } + + require_once ABSPATH . 'includes/class-psa-post-type.php'; + + $this->assertFalse( + method_exists( 'PSA_Post_Type', 'register_peptide_post_type' ), + 'PSA_Post_Type::register_peptide_post_type() was removed in v4.5.0 — do not reintroduce it; PR Core owns the peptide CPT.' + ); + $this->assertFalse( + method_exists( 'PSA_Post_Type', 'register_taxonomy' ), + 'PSA_Post_Type::register_taxonomy() was removed in v4.5.0 — do not reintroduce it; PR Core owns the peptide_category taxonomy.' + ); + $this->assertFalse( + method_exists( 'PSA_Post_Type', 'populate_default_categories' ), + 'PSA_Post_Type::populate_default_categories() was removed in v4.5.0 — PR Core owns taxonomy term seeding.' + ); + + // DEFAULT_CATEGORIES constant is intentionally retained: PSA consumes + // it as a slug→name reference list for fuzzy-matching in + // PSA_AI_Content::assign_category_term() and the admin migration + // button. It is no longer a seed list. See CHANGELOG [4.5.0] and + // the class docblock. + $this->assertTrue( + defined( 'PSA_Post_Type::DEFAULT_CATEGORIES' ), + 'PSA_Post_Type::DEFAULT_CATEGORIES is retained as a consumer lookup — PSA_AI_Content and the admin migration button read it.' + ); + $this->assertCount( + 8, + PSA_Post_Type::DEFAULT_CATEGORIES, + 'DEFAULT_CATEGORIES should still contain exactly 8 entries matching the terms seeded on prod.' + ); + } + + /** + * Meta-box contract survives the CPT handoff — the `psa_*` meta key + * namespace on the `peptide` CPT is still owned by PSA, so the class + * constants that drive meta-box rendering must remain intact. + */ + public function test_psa_post_type_meta_keys_unchanged(): void { + $this->assertArrayHasKey( 'psa_sequence', PSA_Post_Type::META_FIELDS ); + $this->assertArrayHasKey( 'psa_molecular_weight', PSA_Post_Type::META_FIELDS ); + $this->assertArrayHasKey( 'psa_source', PSA_Post_Type::META_FIELDS ); + $this->assertArrayHasKey( 'psa_pubchem_cid', PSA_Post_Type::META_FIELDS ); + + $this->assertArrayHasKey( 'psa_half_life', PSA_Post_Type::EXTENDED_META_FIELDS ); + $this->assertArrayHasKey( 'psa_stability', PSA_Post_Type::EXTENDED_META_FIELDS ); + $this->assertArrayHasKey( 'psa_solubility', PSA_Post_Type::EXTENDED_META_FIELDS ); + } + + /** + * PSA_Post_Type::init_admin() still hooks meta boxes + admin columns + * against the `peptide` CPT. The method has to survive the CPT-handoff + * refactor — it's the primary consumer-side attachment point. + */ + public function test_psa_post_type_init_admin_preserved(): void { + $this->assertTrue( method_exists( 'PSA_Post_Type', 'init_admin' ) ); + } +} diff --git a/tests/unit/PostTypeTest.php b/tests/unit/PostTypeTest.php index 52c8ddb..1f113f7 100755 --- a/tests/unit/PostTypeTest.php +++ b/tests/unit/PostTypeTest.php @@ -2,79 +2,39 @@ /** * Tests for PSA_Post_Type class. * - * Covers: taxonomy constants, default categories list, meta field constants, - * extended meta fields, and get_all_meta_keys() helper. + * As of v4.5.0 this class no longer registers the `peptide` CPT or + * `peptide_category` taxonomy — PR Core >= 0.2.0 owns those. PSA_Post_Type's + * remaining public surface is: META_FIELDS, EXTENDED_META_FIELDS, + * get_all_meta_keys(), init_admin() (meta boxes + admin columns), and the + * add_admin_columns / render_admin_column helpers. + * + * These tests cover the public meta-key contract and the admin-column + * helpers. Registration-path coverage lives in DependencyCheckTest (which + * also contains the regression guard against those methods coming back). * * @package PeptideSearchAI */ declare( strict_types=1 ); -// Stub WordPress functions needed by PSA_Post_Type but not by the test bootstrap. -if ( ! function_exists( 'post_type_exists' ) ) { - function post_type_exists( $post_type ) { - return false; - } -} -if ( ! function_exists( 'register_post_type' ) ) { - function register_post_type( $post_type, $args = array() ) { - return null; - } -} -if ( ! function_exists( 'taxonomy_exists' ) ) { - function taxonomy_exists( $taxonomy ) { - return false; - } -} -if ( ! function_exists( 'register_taxonomy' ) ) { - function register_taxonomy( $taxonomy, $object_type, $args = array() ) { - return null; - } -} -if ( ! function_exists( '__' ) ) { - function __( $text, $domain = 'default' ) { - return $text; - } -} +// Stub WordPress functions needed by PSA_Post_Type at class-load time. if ( ! function_exists( 'add_action' ) ) { function add_action() {} } if ( ! function_exists( 'add_filter' ) ) { function add_filter() {} } +if ( ! function_exists( '__' ) ) { + function __( $text, $domain = 'default' ) { + return $text; + } +} require_once dirname( __DIR__ ) . '/bootstrap.php'; require_once ABSPATH . 'includes/class-psa-post-type.php'; class PostTypeTest extends \PHPUnit\Framework\TestCase { - /** - * Verify default categories constant contains exactly 8 entries. - */ - public function test_default_categories_count(): void { - $this->assertCount( 8, PSA_Post_Type::DEFAULT_CATEGORIES ); - } - - /** - * Verify required category slugs exist in the constant. - */ - public function test_default_categories_slugs(): void { - $required = array( - 'tissue-repair', - 'lipid-metabolism', - 'aging-research', - 'dermatological', - 'metabolic', - 'growth-hormone', - 'immunology', - 'endocrine', - ); - - foreach ( $required as $slug ) { - $this->assertArrayHasKey( $slug, PSA_Post_Type::DEFAULT_CATEGORIES, "Missing category slug: {$slug}" ); - } - } - /** * Verify core META_FIELDS constant has expected keys. */ @@ -99,7 +59,7 @@ class PostTypeTest extends \PHPUnit\Framework\TestCase { } /** - * Verify extended meta fields contain the 9 new v4.3.0 fields. + * Verify extended meta fields contain the 9 v4.3.0 fields. */ public function test_extended_meta_fields_count(): void { $this->assertCount( 9, PSA_Post_Type::EXTENDED_META_FIELDS ); @@ -141,4 +101,38 @@ class PostTypeTest extends \PHPUnit\Framework\TestCase { $expected_count = count( PSA_Post_Type::META_FIELDS ) + count( PSA_Post_Type::EXTENDED_META_FIELDS ); $this->assertCount( $expected_count, $all_keys ); } + + /** + * add_admin_columns() inserts the Source column right after Title. + * This is the CPT-independent part of the admin surface and runs even + * before PSA_Post_Type_Meta is loaded by PR Core's registration. + */ + public function test_add_admin_columns_inserts_source_after_title(): void { + $columns = array( + 'cb' => '', + 'title' => 'Title', + 'date' => 'Date', + ); + $result = PSA_Post_Type::add_admin_columns( $columns ); + + $keys = array_keys( $result ); + $this->assertSame( array( 'cb', 'title', 'psa_source', 'date' ), $keys ); + $this->assertSame( 'Source', $result['psa_source'] ); + } + + /** + * add_admin_columns() is a no-op when 'title' is not in the columns + * array (defensive — WP themes/plugins can filter the columns array + * upstream of us, so we must not assume 'title' is present). + */ + public function test_add_admin_columns_without_title_is_noop(): void { + $columns = array( + 'cb' => '', + 'date' => 'Date', + ); + $result = PSA_Post_Type::add_admin_columns( $columns ); + + $this->assertSame( $columns, $result ); + $this->assertArrayNotHasKey( 'psa_source', $result ); + } } diff --git a/uninstall.php b/uninstall.php index 8b81cd9..8315fb0 100755 --- a/uninstall.php +++ b/uninstall.php @@ -2,7 +2,17 @@ /** * Fired when the plugin is uninstalled. * - * Cleans up all plugin data: peptide posts, post meta, options, transients, and rewrite rules. + * As of PSA v4.5.0 the `peptide` custom post type and `peptide_category` + * taxonomy are owned by Peptide Repo Core (PR Core) — PSA is a consumer, not + * the owner. Uninstalling PSA must therefore NOT delete peptide posts, + * peptide_category terms, or any `psa_*` post meta: that data predates PR + * Core and is consumed by other plugins via PR Core's data layer + PSA's + * REST endpoints. Removing it on uninstall would be destructive across + * plugin boundaries. + * + * This uninstaller only removes PSA's own option rows, transients, and the + * `wp_psa_api_logs` table (PSA's internal cost-tracking table — no other + * plugin reads from it). * * @package PeptideSearchAI */ @@ -14,37 +24,12 @@ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { global $wpdb; -// 1. Delete all peptide custom posts and their meta. -$peptide_ids = $wpdb->get_col( - "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'peptide'" -); - -if ( ! empty( $peptide_ids ) ) { - foreach ( $peptide_ids as $post_id ) { - wp_delete_post( (int) $post_id, true ); // Force delete, bypass trash. - } -} - -// 2. Delete peptide_category taxonomy terms and relationships. -$terms = get_terms( - array( - 'taxonomy' => 'peptide_category', - 'hide_empty' => false, - 'fields' => 'ids', - ) -); -if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) { - foreach ( $terms as $term_id ) { - wp_delete_term( (int) $term_id, 'peptide_category' ); - } -} - -// 3. Delete plugin options. +// 1. Delete plugin options. delete_option( 'psa_settings' ); delete_option( 'psa_search_cache_gen' ); delete_option( 'psa_db_version' ); -// 4. Delete all plugin transients (validation cache, rate limits). +// 2. Delete all plugin transients (validation cache, rate limits, PubChem cache). $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", @@ -53,8 +38,7 @@ $wpdb->query( ) ); -// 5. Drop API logs table. -$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}psa_api_logs" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery +// 3. Drop PSA's API logs table. Owned entirely by PSA; no cross-plugin reads. +$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}psa_api_logs" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -// 6. Flush rewrite rules to clean up the peptide post type permalinks. -flush_rewrite_rules(); +// 4. NOT deleted (owned by Peptide Repo Core as of PSA v4.5.0) \ No newline at end of file