diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 765921f..c52fbf0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -69,7 +69,11 @@ peptide-repo-core/ │ ├── class-pr-core-seed-data.php # Dev fixture: 3 peptides, 10 dosing rows, 5 legal cells │ │ │ ├── cpt/ -│ │ └── class-pr-core-peptide-cpt.php # CPT + peptide_category registration (guarded), meta fields, sanitizers +│ │ ├── class-pr-core-peptide-cpt.php # CPT + peptide_category registration (guarded), meta fields, sanitizers +│ │ └── class-pr-core-repo-daily-cpt.php # Repo Daily CPT registration for editorial content +│ │ +│ ├── taxonomies/ +│ │ └── class-pr-core-repo-daily-taxonomy.php # repo_daily_category taxonomy + term seeding │ │ │ ├── migrations/ │ │ ├── class-pr-core-migration-runner.php # Sequential migration engine @@ -90,10 +94,11 @@ peptide-repo-core/ │ │ └── class-pr-core-candidate-queue-repository.php # CRUD + approve/reject for queue │ │ │ ├── admin/ -│ │ ├── class-pr-core-admin.php # Admin initialization, menu, styles -│ │ ├── class-pr-core-peptide-metaboxes.php # Identifiers, dosing, legal meta boxes -│ │ ├── class-pr-core-admin-columns.php # Evidence, editorial, dosing count columns -│ │ └── class-pr-core-candidate-queue-page.php # AI candidate review admin screen +│ │ ├── class-pr-core-admin.php # Admin initialization, menu, styles +│ │ ├── class-pr-core-peptide-metaboxes.php # Identifiers, dosing, legal meta boxes +│ │ ├── class-pr-core-repo-daily-metaboxes.php # Author byline and clinical review flag meta boxes +│ │ ├── class-pr-core-admin-columns.php # Evidence, editorial, dosing count columns +│ │ └── class-pr-core-candidate-queue-page.php # AI candidate review admin screen │ │ │ ├── frontend/ │ │ ├── class-pr-core-disclaimer.php # Shortcode + static API for surface disclaimers @@ -110,6 +115,17 @@ peptide-repo-core/ --- +## Content Types + +| CPT | Slug | Taxonomy | Purpose | Registrant | +|-----|------|----------|---------|-----------| +| `peptide` | `/peptides/[slug]/` | `peptide_category` | Canonical peptide monographs with dosing, legal, evidence metadata | PR Core v0.2.0 (consolidated from PSA) | +| `repo_daily` | `/daily/[slug]/` | `repo_daily_category` | Editorial publications (articles, guides, comparisons, news) generated by PRAutoBlogger, published under Boo Sheeran byline | PR Core v0.5.0 | + +**Verification scanner scope:** The verification scanner (class-pr-core-verification-scanner.php) queries `post_type=peptide` only. `repo_daily` posts are excluded automatically by CPT separation and do not appear in the overdue queue. + +--- + ## Custom Tables ### `{prefix}pr_dosing_rows` diff --git a/CHANGELOG.md b/CHANGELOG.md index 21620fe..48789b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,29 @@ All notable changes to Peptide Repo Core are documented here. Format: [Semantic Versioning](https://semver.org/). +## [0.5.0] — 2026-04-27 + +### Added +- **Repo Daily CPT**: New `repo_daily` custom post type at `/daily/[slug]/` for editorial publications (articles, guides, comparisons, news) generated by PRAutoBlogger and published under the Boo Sheeran byline. +- **Repo Daily Category Taxonomy**: Non-hierarchical `repo_daily_category` taxonomy with four seeded terms (article, guide, comparison, news) for editorial classification. +- **Repo Daily Meta Fields**: + - `_repo_daily_author` (string, default "Boo Sheeran") — byline displayed in theme templates. Never hardcoded; always read from meta. + - `_repo_daily_clinical_review_required` (bool, stored as '1'/'') — editorial flag marking posts that require PR Clinical Review before publication. +- **Repo Daily Admin Meta Box**: "Article Settings" meta box on repo_daily edit screen with author byline field and clinical review flag checkbox. +- **Bulk Author Migration**: On activation, sets `_repo_daily_author = 'Boo Sheeran'` on all post_type=post posts bearing the psa_source meta key (autoblogged articles). Idempotent; existing meta values preserved. Posts remain post_type=post; forward-only migration for byline retroactivity. + +### Changed +- `PR_Core_Admin::register_hooks()` now registers repo_daily metaboxes alongside peptide metaboxes. + +### Technical +- New classes: `PR_Core_Repo_Daily_CPT`, `PR_Core_Repo_Daily_Taxonomy`, `PR_Core_Repo_Daily_Metaboxes`. +- New method: `PR_Core_Activator::migrate_autoblogged_author_meta()` — idempotent bulk meta setter. +- Taxonomy seeding: `PR_Core_Repo_Daily_Taxonomy::seed_terms()` called on activation. + +### Documentation +- ARCHITECTURE.md: Added repo_daily CPT to content-type table; documented scanner exclusion by CPT separation. +- CONVENTIONS.md: Added "How to add a new post meta field to repo_daily posts" extension pattern. + ## [0.4.0] — 2026-04-27 ### Added diff --git a/CONVENTIONS.md b/CONVENTIONS.md index 0169889..7defd34 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -14,7 +14,7 @@ Autoloader convention: `PR_Core_Foo_Bar` maps to `class-pr-core-foo-bar.php`. Files live in `includes/` or one level of subdirectory under `includes/`. -## How To: Add a New Post-Meta Field +## How To: Add a New Post-Meta Field to Peptide Posts 1. Add the field to `PR_Core_Peptide_CPT::get_meta_fields()` in `cpt/class-pr-core-peptide-cpt.php`. 2. Add the field to `PR_Core_Peptide_DTO` constructor and `to_array()` in `dto/class-pr-core-peptide-dto.php`. @@ -22,6 +22,13 @@ Files live in `includes/` or one level of subdirectory under `includes/`. 4. Add a form input in `PR_Core_Peptide_Metaboxes::render_identifiers_box()` in `admin/class-pr-core-peptide-metaboxes.php`. 5. The `save_meta()` method reads from the same field list — no changes needed unless custom save logic applies. +## How To: Add a New Post-Meta Field to Repo Daily Posts + +1. Define the meta key (start with `_repo_daily_`) and sanitization logic (use `sanitize_text_field()` for strings, cast to bool for checkboxes). +2. Create a new metabox or extend `PR_Core_Repo_Daily_Metaboxes::render_meta_box()` in `admin/class-pr-core-repo-daily-metaboxes.php`. +3. In `PR_Core_Repo_Daily_Metaboxes::save_meta()`, handle the field: read from `$_POST`, sanitize, and call `update_post_meta()`. +4. Example: The `_repo_daily_clinical_review_required` checkbox checks for `isset( $_POST['key'] )` and stores `'1'` or `''`; `get_post_meta()` is type-safe because the value is always one of two strings, never a bool. + ## How To: Add a New Migration 1. Create `includes/migrations/class-pr-core-migration-NNNN-description.php`. diff --git a/includes/admin/class-pr-core-admin.php b/includes/admin/class-pr-core-admin.php index 060b292..ad54619 100644 --- a/includes/admin/class-pr-core-admin.php +++ b/includes/admin/class-pr-core-admin.php @@ -34,6 +34,11 @@ class PR_Core_Admin { add_filter( 'manage_' . PR_Core_Peptide_CPT::POST_TYPE . '_posts_columns', [ $columns, 'add_columns' ] ); add_action( 'manage_' . PR_Core_Peptide_CPT::POST_TYPE . '_posts_custom_column', [ $columns, 'render_column' ], 10, 2 ); + // Repo Daily meta boxes. + $repo_daily_metaboxes = new PR_Core_Repo_Daily_Metaboxes(); + add_action( 'add_meta_boxes', [ $repo_daily_metaboxes, 'add_meta_boxes' ] ); + add_action( 'save_post_' . PR_Core_Repo_Daily_CPT::POST_TYPE, [ $repo_daily_metaboxes, 'save_meta' ], 10, 2 ); + add_action( 'admin_menu', [ $this, 'add_admin_pages' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_styles' ] ); } diff --git a/includes/admin/class-pr-core-repo-daily-metaboxes.php b/includes/admin/class-pr-core-repo-daily-metaboxes.php new file mode 100755 index 0000000..0393d46 --- /dev/null +++ b/includes/admin/class-pr-core-repo-daily-metaboxes.php @@ -0,0 +1,113 @@ +ID, '_repo_daily_author', true ) ?: self::DEFAULT_AUTHOR + ); + $requires_review = get_post_meta( $post->ID, '_repo_daily_clinical_review_required', true ); + + echo ''; + + // Author field. + printf( + '', + esc_html__( 'Author / Byline', 'peptide-repo-core' ), + esc_attr( $author ) + ); + + // Clinical review flag. + printf( + '', + esc_html__( 'Clinical Review Required', 'peptide-repo-core' ), + checked( $requires_review, '1', false ), + esc_html__( 'Flag for PR Clinical Review before publish', 'peptide-repo-core' ) + ); + + echo '
%s
'; + } + + /** + * Save repo_daily meta fields on post save. + * + * Side effects: updates post meta. + * + * @param int $post_id Post ID. + * @param \WP_Post $post Post object. + * @return void + */ + public function save_meta( int $post_id, \WP_Post $post ): void { + if ( ! isset( $_POST['pr_core_repo_daily_nonce'] ) ) { + return; + } + + if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pr_core_repo_daily_nonce'] ) ), 'pr_core_repo_daily_meta' ) ) { + return; + } + + if ( ! current_user_can( 'edit_posts' ) ) { + return; + } + + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + return; + } + + // Save author field. + if ( isset( $_POST['pr_core_repo_daily_author'] ) ) { + $author = sanitize_text_field( wp_unslash( $_POST['pr_core_repo_daily_author'] ) ); + // Use default if empty. + if ( empty( $author ) ) { + $author = self::DEFAULT_AUTHOR; + } + update_post_meta( $post_id, '_repo_daily_author', $author ); + } + + // Save clinical review flag. + $requires_review = isset( $_POST['pr_core_repo_daily_clinical_review_required'] ) ? '1' : ''; + update_post_meta( $post_id, '_repo_daily_clinical_review_required', $requires_review ); + } +} diff --git a/includes/class-pr-core-activator.php b/includes/class-pr-core-activator.php index 23797a3..43efb35 100644 --- a/includes/class-pr-core-activator.php +++ b/includes/class-pr-core-activator.php @@ -37,9 +37,17 @@ class PR_Core_Activator { PR_Core_Peptide_CPT::register_peptide_post_type(); PR_Core_Peptide_CPT::register_taxonomies(); + // Register repo_daily CPT and taxonomy + seed terms. + PR_Core_Repo_Daily_CPT::register_post_type(); + PR_Core_Repo_Daily_Taxonomy::register_taxonomy(); + PR_Core_Repo_Daily_Taxonomy::seed_terms(); + // Grant manage_peptide_content to administrators and editors. self::add_capabilities(); + // Bulk-set _repo_daily_author on existing posts with psa_source meta. + self::migrate_autoblogged_author_meta(); + // Flush rewrite rules for the new CPT. flush_rewrite_rules( false ); @@ -94,4 +102,58 @@ class PR_Core_Activator { } } } + + /** + * Bulk-set _repo_daily_author on autoblogged posts. + * + * Sets `_repo_daily_author = 'Boo Sheeran'` on all post_type=post posts + * that have the psa_source meta key present (identifying them as autoblogged). + * Idempotent: skips posts that already have the meta set. + * + * Side effects: updates post meta in database. + * + * @return void + */ + private static function migrate_autoblogged_author_meta(): void { + global $wpdb; + + // Query all posts of type 'post' that have the psa_source meta key. + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery + // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + $post_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT DISTINCT pm.post_id + FROM {$wpdb->postmeta} pm + WHERE pm.meta_key = %s + AND pm.post_id IN ( + SELECT ID FROM {$wpdb->posts} + WHERE post_type = %s + )", + 'psa_source', + 'post' + ) + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery + // phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching + + if ( empty( $post_ids ) ) { + return; + } + + $author_value = 'Boo Sheeran'; + + foreach ( $post_ids as $post_id ) { + $post_id = (int) $post_id; + + // Skip if already set (idempotent). + $existing = get_post_meta( $post_id, '_repo_daily_author', true ); + if ( ! empty( $existing ) ) { + continue; + } + + // Set the author meta. + update_post_meta( $post_id, '_repo_daily_author', $author_value ); + } + } } + diff --git a/includes/class-pr-core.php b/includes/class-pr-core.php index 8080789..f7b2315 100644 --- a/includes/class-pr-core.php +++ b/includes/class-pr-core.php @@ -35,6 +35,14 @@ class PR_Core { $topic_tax = new PR_Core_Topic_Taxonomy(); $topic_tax->register_hooks(); + // Register repo_daily CPT (fires on init). + $repo_daily_cpt = new PR_Core_Repo_Daily_CPT(); + $repo_daily_cpt->register_hooks(); + + // Register repo_daily_category taxonomy (fires on init). + $repo_daily_tax = new PR_Core_Repo_Daily_Taxonomy(); + $repo_daily_tax->register_hooks(); + // One-shot rewrite flush on in-place version bumps. Runs at the very // end of init (priority 999) so all CPTs/taxonomies — ours and // anyone else's — are registered first. Handles updates deployed diff --git a/includes/cpt/class-pr-core-repo-daily-cpt.php b/includes/cpt/class-pr-core-repo-daily-cpt.php new file mode 100755 index 0000000..2382520 --- /dev/null +++ b/includes/cpt/class-pr-core-repo-daily-cpt.php @@ -0,0 +1,80 @@ + __( 'Repo Daily', 'peptide-repo-core' ), + 'singular_name' => __( 'Repo Daily', 'peptide-repo-core' ), + 'add_new_item' => __( 'Add New Article', 'peptide-repo-core' ), + 'edit_item' => __( 'Edit Article', 'peptide-repo-core' ), + 'new_item' => __( 'New Article', 'peptide-repo-core' ), + 'view_item' => __( 'View Article', 'peptide-repo-core' ), + 'search_items' => __( 'Search Articles', 'peptide-repo-core' ), + 'not_found' => __( 'No articles found', 'peptide-repo-core' ), + 'not_found_in_trash' => __( 'No articles found in trash', 'peptide-repo-core' ), + 'all_items' => __( 'All Articles', 'peptide-repo-core' ), + 'menu_name' => __( 'Repo Daily', 'peptide-repo-core' ), + ]; + + $args = [ + 'labels' => $labels, + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'show_in_nav_menus' => true, + 'show_in_rest' => true, + 'rest_base' => 'repo_daily', + 'menu_position' => 6, + 'menu_icon' => 'dashicons-edit-page', + 'capability_type' => 'post', + 'map_meta_cap' => true, + 'hierarchical' => false, + 'has_archive' => true, + 'rewrite' => [ 'slug' => 'daily', 'with_front' => false ], + 'supports' => [ 'title', 'editor', 'excerpt', 'thumbnail', 'custom-fields' ], + ]; + + register_post_type( self::POST_TYPE, $args ); + } +} diff --git a/includes/taxonomies/class-pr-core-repo-daily-taxonomy.php b/includes/taxonomies/class-pr-core-repo-daily-taxonomy.php new file mode 100755 index 0000000..c7397d5 --- /dev/null +++ b/includes/taxonomies/class-pr-core-repo-daily-taxonomy.php @@ -0,0 +1,78 @@ + [ + 'name' => __( 'Category', 'peptide-repo-core' ), + 'singular_name' => __( 'Category', 'peptide-repo-core' ), + ], + 'public' => true, + 'publicly_queryable' => true, + 'show_in_rest' => true, + 'show_ui' => true, + 'show_admin_column' => true, + 'hierarchical' => false, + 'rewrite' => [ 'slug' => 'daily/category', 'with_front' => false ], + ] ); + } + + /** + * Seed default category terms on activation. + * + * Idempotent: checks for existence of each term before inserting. + * Called by PR_Core_Activator during plugin activation. + * + * Side effects: may insert terms into wp_terms and wp_term_taxonomy. + * + * @return void + */ + public static function seed_terms(): void { + $terms = [ 'article', 'guide', 'comparison', 'news' ]; + + foreach ( $terms as $term ) { + if ( ! term_exists( $term, self::TAX_CATEGORY ) ) { + wp_insert_term( $term, self::TAX_CATEGORY ); + } + } + } +} diff --git a/peptide-repo-core.php b/peptide-repo-core.php index 8fbb3bc..8cd3ab5 100644 --- a/peptide-repo-core.php +++ b/peptide-repo-core.php @@ -22,7 +22,7 @@ if ( ! defined( 'ABSPATH' ) ) { /* ── Constants ────────────────────────────────────────────────────────── */ -define( 'PR_CORE_VERSION', '0.4.0' ); +define( 'PR_CORE_VERSION', '0.5.0' ); define( 'PR_CORE_PLUGIN_FILE', __FILE__ ); define( 'PR_CORE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PR_CORE_PLUGIN_URL', plugin_dir_url( __FILE__ ) );