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