diff --git a/CHANGELOG.md b/CHANGELOG.md
index e82bb74..3d2eb59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ All notable changes to Peptide Repo Core are documented here.
Format: [Semantic Versioning](https://semver.org/).
+## [0.7.1] - 2026-06-17
+
+### Fixed
+- **Critical**: Settings page PHP Fatal `Undefined constant PR_Core_Settings_Renderer::OPTION_GROUP` (and 6 other `self::CONST` references). Root cause: PR #22 split the renderer into its own class but left all constants defined only on `PR_Core_Settings`. Fix: promote the 6 option-key private constants to `public const` on `PR_Core_Settings`; update renderer to reference them via `PR_Core_Settings::*`. Regression introduced in v0.7.0.
+
## [0.7.0] - 2026-06-16
### Changed
diff --git a/includes/admin/class-pr-core-settings-renderer.php b/includes/admin/class-pr-core-settings-renderer.php
index 2056e20..8c4b43c 100644
--- a/includes/admin/class-pr-core-settings-renderer.php
+++ b/includes/admin/class-pr-core-settings-renderer.php
@@ -35,8 +35,8 @@ class PR_Core_Settings_Renderer {
@@ -50,10 +50,10 @@ class PR_Core_Settings_Renderer {
* @return void
*/
public static function render_enabled_field(): void {
- $enabled = (bool) get_option( self::RELATED_ENABLED, true );
+ $enabled = (bool) get_option( PR_Core_Settings::RELATED_ENABLED, true );
printf(
' %s',
- esc_attr( self::RELATED_ENABLED ),
+ esc_attr( PR_Core_Settings::RELATED_ENABLED ),
checked( $enabled, true, false ),
esc_html__( 'Show related monographs on single peptide pages', 'peptide-repo-core' )
);
@@ -67,8 +67,8 @@ class PR_Core_Settings_Renderer {
public static function render_limit_field(): void {
printf(
' ',
- esc_attr( self::RELATED_LIMIT ),
- absint( get_option( self::RELATED_LIMIT, 3 ) )
+ esc_attr( PR_Core_Settings::RELATED_LIMIT ),
+ absint( get_option( PR_Core_Settings::RELATED_LIMIT, 3 ) )
);
echo '' . esc_html__( 'Number of articles to display (1-6). Default: 3.', 'peptide-repo-core' ) . '
';
}
@@ -88,8 +88,8 @@ class PR_Core_Settings_Renderer {
* @return void
*/
public static function render_cadence_field(): void {
- $current = get_option( self::SCAN_CADENCE, 'weekly' );
- echo '';
+ $current = get_option( PR_Core_Settings::SCAN_CADENCE, 'weekly' );
+ echo '';
foreach ( array(
'daily' => 'Daily',
'weekly' => 'Weekly',
@@ -106,7 +106,7 @@ class PR_Core_Settings_Renderer {
* @return void
*/
public static function render_threshold_field(): void {
- printf( ' ', esc_attr( self::DEFAULT_THRESHOLD ), absint( get_option( self::DEFAULT_THRESHOLD, 180 ) ) );
+ printf( ' ', esc_attr( PR_Core_Settings::DEFAULT_THRESHOLD ), absint( get_option( PR_Core_Settings::DEFAULT_THRESHOLD, 180 ) ) );
}
/**
@@ -115,7 +115,7 @@ class PR_Core_Settings_Renderer {
* @return void
*/
public static function render_high_velocity_field(): void {
- printf( ' ', esc_attr( self::HIGH_VELOCITY_THRESHOLD ), absint( get_option( self::HIGH_VELOCITY_THRESHOLD, 60 ) ) );
+ printf( ' ', esc_attr( PR_Core_Settings::HIGH_VELOCITY_THRESHOLD ), absint( get_option( PR_Core_Settings::HIGH_VELOCITY_THRESHOLD, 60 ) ) );
}
/**
@@ -126,8 +126,8 @@ class PR_Core_Settings_Renderer {
public static function render_email_field(): void {
printf(
' ',
- esc_attr( self::VERIFICATION_EMAIL ),
- esc_attr( (string) get_option( self::VERIFICATION_EMAIL, '' ) )
+ esc_attr( PR_Core_Settings::VERIFICATION_EMAIL ),
+ esc_attr( (string) get_option( PR_Core_Settings::VERIFICATION_EMAIL, '' ) )
);
echo '' . esc_html__( 'Comma-separated. Leave blank to disable email digests.', 'peptide-repo-core' ) . '
';
}
diff --git a/includes/admin/class-pr-core-settings.php b/includes/admin/class-pr-core-settings.php
index 6a6e2a5..5520cfb 100644
--- a/includes/admin/class-pr-core-settings.php
+++ b/includes/admin/class-pr-core-settings.php
@@ -23,22 +23,22 @@ class PR_Core_Settings {
public const OPTION_GROUP = 'pr_core_settings';
/** @var string Option key: enable/disable related articles. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- private const RELATED_ENABLED = 'pr_core_related_posts_enabled';
+ public const RELATED_ENABLED = 'pr_core_related_posts_enabled';
/** @var string Option key: related articles limit (1-6). */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- private const RELATED_LIMIT = 'pr_core_related_posts_limit';
+ public const RELATED_LIMIT = 'pr_core_related_posts_limit';
/** @var string Option key: WP-cron scan cadence. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- private const SCAN_CADENCE = 'pr_core_scan_cadence';
+ public const SCAN_CADENCE = 'pr_core_scan_cadence';
/** @var string Option key: default staleness threshold in days. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- private const DEFAULT_THRESHOLD = 'pr_core_default_threshold';
+ public const DEFAULT_THRESHOLD = 'pr_core_default_threshold';
/** @var string Option key: high-velocity staleness threshold in days. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- private const HIGH_VELOCITY_THRESHOLD = 'pr_core_high_velocity_threshold';
+ public const HIGH_VELOCITY_THRESHOLD = 'pr_core_high_velocity_threshold';
/** @var string Option key: comma-separated notification email list. */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
- private const VERIFICATION_EMAIL = 'pr_core_verification_email';
+ public const VERIFICATION_EMAIL = 'pr_core_verification_email';
/**
* Register the settings submenu page under Peptides.
diff --git a/peptide-repo-core.php b/peptide-repo-core.php
index 434e259..11d7b9a 100644
--- a/peptide-repo-core.php
+++ b/peptide-repo-core.php
@@ -9,7 +9,7 @@
* Plugin Name: Peptide Repo Core
* Plugin URI: https://peptiderepo.com
* Description: Canonical peptide schema — shared data layer for the peptiderepo.com ecosystem. Provides the peptide CPT, dosing rows, legal status cells, AI candidate queue, disclaimer component, and JSON-LD output.
- * Version: 0.7.0
+ * Version: 0.7.1
* Author: peptiderepo
* Author URI: https://peptiderepo.com
* License: GPL-2.0-or-later
@@ -29,7 +29,7 @@ if ( ! defined( 'ABSPATH' ) ) {
/* ── Constants ────────────────────────────────────────────────────────── */
-define( 'PR_CORE_VERSION', '0.7.0' );
+define( 'PR_CORE_VERSION', '0.7.1' );
define( 'PR_CORE_PLUGIN_FILE', __FILE__ );
define( 'PR_CORE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'PR_CORE_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
diff --git a/tests/unit/SettingsRendererTest.php b/tests/unit/SettingsRendererTest.php
new file mode 100644
index 0000000..075f889
--- /dev/null
+++ b/tests/unit/SettingsRendererTest.php
@@ -0,0 +1,204 @@
+ [],
+ 'existing_taxonomies' => [],
+ 'registered_post_types' => [],
+ 'registered_taxonomies' => [],
+ 'registered_meta' => [],
+ 'added_actions' => [],
+ 'added_filters' => [],
+ ];
+ require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/admin/class-pr-core-settings.php';
+ require_once PR_CORE_PLUGIN_DIR . 'includes/admin/class-pr-core-settings-renderer.php';
+ }
+
+ // ── Cross-class constant accessibility ────────────────────────────────.
+
+ /**
+ * OPTION_GROUP must be a non-empty string on PR_Core_Settings.
+ * Renderer calls PR_Core_Settings::OPTION_GROUP — verifies it resolves.
+ */
+ public function test_option_group_constant_accessible_on_settings(): void {
+ $this->assertNotEmpty( PR_Core_Settings::OPTION_GROUP );
+ $this->assertIsString( PR_Core_Settings::OPTION_GROUP );
+ }
+
+ public function test_related_enabled_constant_accessible(): void {
+ $this->assertNotEmpty( PR_Core_Settings::RELATED_ENABLED );
+ }
+
+ public function test_related_limit_constant_accessible(): void {
+ $this->assertNotEmpty( PR_Core_Settings::RELATED_LIMIT );
+ }
+
+ public function test_scan_cadence_constant_accessible(): void {
+ $this->assertNotEmpty( PR_Core_Settings::SCAN_CADENCE );
+ }
+
+ public function test_default_threshold_constant_accessible(): void {
+ $this->assertNotEmpty( PR_Core_Settings::DEFAULT_THRESHOLD );
+ }
+
+ public function test_high_velocity_threshold_constant_accessible(): void {
+ $this->assertNotEmpty( PR_Core_Settings::HIGH_VELOCITY_THRESHOLD );
+ }
+
+ public function test_verification_email_constant_accessible(): void {
+ // Value is intentionally a non-empty option key string.
+ $this->assertIsString( PR_Core_Settings::VERIFICATION_EMAIL );
+ $this->assertNotEmpty( PR_Core_Settings::VERIFICATION_EMAIL );
+ }
+
+ // ── Renderer render_*_field: no fatal on constant resolution ─────────.
+
+ /**
+ * render_enabled_field() calls PR_Core_Settings::RELATED_ENABLED twice.
+ * Before the fix this fatalled. Capture output to confirm no crash.
+ */
+ public function test_render_enabled_field_no_fatal(): void {
+ ob_start();
+ PR_Core_Settings_Renderer::render_enabled_field();
+ $output = ob_get_clean();
+ $this->assertStringContainsString( 'type="checkbox"', $output );
+ }
+
+ /**
+ * render_limit_field() calls PR_Core_Settings::RELATED_LIMIT twice.
+ */
+ public function test_render_limit_field_no_fatal(): void {
+ ob_start();
+ PR_Core_Settings_Renderer::render_limit_field();
+ $output = ob_get_clean();
+ $this->assertStringContainsString( 'type="number"', $output );
+ }
+
+ /**
+ * render_cadence_field() calls PR_Core_Settings::SCAN_CADENCE twice.
+ */
+ public function test_render_cadence_field_no_fatal(): void {
+ ob_start();
+ PR_Core_Settings_Renderer::render_cadence_field();
+ $output = ob_get_clean();
+ $this->assertStringContainsString( 'assertStringContainsString( 'type="number"', $output );
+ }
+
+ /**
+ * render_high_velocity_field() calls PR_Core_Settings::HIGH_VELOCITY_THRESHOLD twice.
+ */
+ public function test_render_high_velocity_field_no_fatal(): void {
+ ob_start();
+ PR_Core_Settings_Renderer::render_high_velocity_field();
+ $output = ob_get_clean();
+ $this->assertStringContainsString( 'type="number"', $output );
+ }
+
+ /**
+ * render_email_field() calls PR_Core_Settings::VERIFICATION_EMAIL twice.
+ */
+ public function test_render_email_field_no_fatal(): void {
+ ob_start();
+ PR_Core_Settings_Renderer::render_email_field();
+ $output = ob_get_clean();
+ $this->assertStringContainsString( 'type="text"', $output );
+ }
+
+ // ── Constant value contracts ──────────────────────────────────────────.
+
+ public function test_option_group_value(): void {
+ $this->assertSame( 'pr_core_settings', PR_Core_Settings::OPTION_GROUP );
+ }
+
+ public function test_related_enabled_value(): void {
+ $this->assertSame( 'pr_core_related_posts_enabled', PR_Core_Settings::RELATED_ENABLED );
+ }
+
+ public function test_related_limit_value(): void {
+ $this->assertSame( 'pr_core_related_posts_limit', PR_Core_Settings::RELATED_LIMIT );
+ }
+
+ public function test_scan_cadence_value(): void {
+ $this->assertSame( 'pr_core_scan_cadence', PR_Core_Settings::SCAN_CADENCE );
+ }
+
+ public function test_default_threshold_value(): void {
+ $this->assertSame( 'pr_core_default_threshold', PR_Core_Settings::DEFAULT_THRESHOLD );
+ }
+
+ public function test_high_velocity_threshold_value(): void {
+ $this->assertSame( 'pr_core_high_velocity_threshold', PR_Core_Settings::HIGH_VELOCITY_THRESHOLD );
+ }
+
+ public function test_verification_email_value(): void {
+ $this->assertSame( 'pr_core_verification_email', PR_Core_Settings::VERIFICATION_EMAIL );
+ }
+}