v4.5.0: drop peptide CPT registration; add PR Core dependency check (#15)

Drops PSA registration of peptide CPT + peptide_category taxonomy (PR Core >= 0.2.0 now owns them per 2026-04-22 consolidation). Adds hard dependency check on PR Core 0.2.0+ — PSA self-deactivates with an admin notice if PR Core is missing or below min version.

CI note: PHPUnit failures (PSA_AI_Content, PSA_Directory_API not found) and WPCS failure are pre-existing on main at HEAD 65f973f (verified: same 5 errors in run 24530495421). PHP Lint green on 7.4/8.1/8.3. No regressions introduced by this PR.

QA: qa-reviews/2026-04-22-2609c66.md PASS.
This commit is contained in:
peptiderepo
2026-04-22 17:12:34 +07:00
committed by GitHub
parent 65f973fea4
commit 868d31cfb3
8 changed files with 480 additions and 292 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,81 @@
<?php
/**
* Dependency check for Peptide Repo Core (PR Core).
*
* What: Verifies PR Core >= 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( '<div class="notice notice-error"><p>%s</p></div>', $msg ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $msg already escaped via esc_html__.
}
}

View File

@@ -1,16 +1,26 @@
<?php
/**
* Registers the Peptide custom post type, taxonomy, and admin columns.
* PSA's consumer-side integration with the `peptide` custom post type.
*
* What: CPT registration, taxonomy registration, default category seeding, admin columns.
* Who calls it: Main plugin file via psa_init() and psa_admin_init() hooks.
* Dependencies: WordPress CPT API, taxonomy API.
* As of v4.5.0 the `peptide` CPT and `peptide_category` taxonomy are owned by
* Peptide Repo Core (PR Core) >= 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.
*

View File

@@ -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();
}

View File

@@ -0,0 +1,200 @@
<?php
/**
* Tests for PSA_Dependency_Check.
*
* Covers:
* - is_satisfied() returns false when PR_CORE_VERSION is undefined.
* - is_satisfied() returns false when PR_CORE_VERSION < REQUIRED_PR_CORE_VERSION.
* - is_satisfied() returns true when PR_CORE_VERSION == REQUIRED_PR_CORE_VERSION.
* - is_satisfied() returns true when PR_CORE_VERSION > 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' ) );
}
}

View File

@@ -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' => '<input />',
'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' => '<input />',
'date' => 'Date',
);
$result = PSA_Post_Type::add_admin_columns( $columns );
$this->assertSame( $columns, $result );
$this->assertArrayNotHasKey( 'psa_source', $result );
}
}

View File

@@ -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)