From a09bda0181d6ad083b089ac10db78898c954055d Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Sun, 14 Jun 2026 07:10:35 +0000 Subject: [PATCH] PR Vision v0.2.0: clear CI blockers (P1-A/B/C/D/E + P2 CONTEXT) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1-A: Remove defined()/constant() declarations from tests/bootstrap.php - Both shadow PHP built-ins → 'Cannot redeclare' fatal on PHP 8.x - Killed PHP Lint (8.2) job and cancelled 8.1/8.3 jobs - Full bootstrap audit: no other PHP built-in shadows remain P1-B: Re-escape pre-escaped vars at echo site (class-prv-model-manager-table) - $prov, $slug, $note: esc_html() at output (was only at assignment) - $id: esc_attr() at every output site (3 places) - $checked_attr/$toggle_attrs: phpcs:ignore -- hard-coded attribute strings - render_health_badge(): phpcs:ignore -- returns pre-escaped HTML markup P1-C: phpcs:disable/enable NonceVerification.Missing (class-prv-settings-page) - Wraps all four handle_*() POST blocks - Justification inline: nonce verified via require_admin_nonce() private helper P1-D/E: Auto-fixable PHPCS violations (array formatting + style) - class-prv-config.php: expand 12 inline associative peptide arrays to multi-line - class-prv-probe-runner.php: expand inline {probed,errors} array; align param docs - class-prv-model-test-ajax.php: multi-line wp_send_json_success/error calls - class-prv-settings-renderer.php: extract cadence options array to variable - class-prv-model-registry.php: capitalise docblock first word; align param spacing P2: CONTEXT.md v0.2.0 -- add 7 new domain terms - Config Version, Run Lock, Model Registry, Run Health, Projected Cost, Config-version Stamp, Truncation State Agent-Session: pr-vision admin v0.2.0 CI fix --- CHANGELOG.md | 7 +++ CONTEXT.md | 37 +++++++++++- includes/core/class-prv-config.php | 60 +++++++++++++++---- .../core/class-prv-model-manager-table.php | 14 +++-- includes/core/class-prv-model-registry.php | 6 +- includes/core/class-prv-model-test-ajax.php | 26 ++++---- includes/core/class-prv-probe-runner.php | 7 ++- includes/core/class-prv-settings-page.php | 8 +++ includes/core/class-prv-settings-renderer.php | 7 ++- tests/bootstrap.php | 2 - 10 files changed, 136 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d4211..a533f7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,3 +94,10 @@ Admin/Settings interface + correctness fixes. All adversarial-QA must-fixes addr - `PRV_Plugin::init()` boots `PRV_Upgrader::run()` first and registers `PRV_Settings_Page`. - `uninstall.php` also purges `prv_` transients (run-lock etc.). - Version bumped to **0.2.0**. + +### CI + +- **P1-A:** Removed `function defined()` and `function constant()` declarations from `tests/bootstrap.php` — they shadowed PHP built-ins and caused `Cannot redeclare` fatal on PHP 8.x, blocking all unit-test CI steps. +- **P1-B:** Re-escaped pre-escaped variables at echo site in `class-prv-model-manager-table.php`; added targeted `phpcs:ignore` with justification for `render_health_badge()` (returns pre-escaped HTML). +- **P1-C:** Added `phpcs:disable/enable WordPress.Security.NonceVerification.Missing` around `$_POST` blocks in all four `handle_*()` methods in `class-prv-settings-page.php`; nonce is verified via `require_admin_nonce()` private helper. +- **P1-D/E:** Expanded inline associative arrays to multi-line in `class-prv-config.php`, `class-prv-probe-runner.php`, `class-prv-settings-renderer.php`, `class-prv-model-test-ajax.php`; fixed param-comment spacing in `class-prv-model-registry.php`; capitalised docblock long-description first word. diff --git a/CONTEXT.md b/CONTEXT.md index 1cc26d1..a1fb770 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,6 +1,6 @@ # Context — Domain Glossary (PR Vision) -**Version:** 0.1.1 | **Last updated:** 2026-06-13 +**Version:** 0.2.0 | **Last updated:** 2026-06-14 This file is the ubiquitous-language glossary for `pr-vision`. Every term below has a precise meaning within this codebase. AI agents and human contributors should use this terminology consistently. @@ -79,6 +79,41 @@ The extensibility mechanism: `PRV_Data_Collector` produces data; `PRV_Dashboard_ **Code identifiers:** `interface-prv-data-collector.php`, `interface-prv-dashboard-panel.php`, `PRV_Collector_Registry`. +### Config Version +A monotonically incrementing integer (stored in the `prv_config_versions` option) that is bumped whenever the active probe configuration changes (models list, peptides list, or prompt intents). Each row in `prv_ai_visibility` is stamped with the `config_version` at the time it was recorded. Rows from a different config version are excluded from scoring to ensure comparability. + +**Code identifiers:** `PRV_Config_Version`, `bump_version_if_changed()`, `config_version` column, `prv_config_versions` option. + +### Run Lock +A transient-backed mutex (`prv_run_lock`) that prevents concurrent probe runs. Acquired at the start of a run and released in a `finally` block. TTL is 1 hour to self-clear on fatal crash. Both the cron tick and the "Run now" button check `is_locked()` before dispatching. + +**Code identifiers:** `PRV_Run_Lock`, `acquire()`, `release()`, `is_locked()`, transient `prv_run_lock`. + +### Model Registry +A versioned option (`prv_models`) storing the list of LLM models to probe. Each entry is a rich object with `{id, provider, slug, enabled, note, health_status, health_probed, health_errors, health_run_id}`. The registry migrates v0.1.x flat-string arrays to v2 schema on upgrade. CRUD is exposed on the Settings page. + +**Code identifiers:** `PRV_Model_Registry`, `run_migration_v2()`, `prv_models` option, `PRV_SCHEMA_VERSION` constant. + +### Run Health +A per-model health status derived from the most recent probe run outcomes. Values: `healthy` (probes succeeded), `retired` (zero rows, errors occurred), `disabled` (model not enabled), `unknown` (not yet run). Displayed as a badge in the Model Manager table. Updated by `PRV_Model_Registry::update_health()` at end of each run. + +**Code identifiers:** `health_status`, `health_probed`, `health_errors`, `health_run_id` fields; `PRV_Model_Registry::update_health()`; `render_health_badge()`. + +### Projected Cost +The estimated monthly spend (in USD) based on the current configuration: `enabled_models × peptides × intents × $0.005/probe × runs_per_month`. Displayed on the Settings page as a cost inset. The `over_cap` flag is shown with `aria-live="polite"` when the projected cost would exceed the monthly budget. + +**Code identifiers:** `PRV_Config::get_projected_cost()`, `per_run_usd`, `per_month_usd`, `probe_count`, `over_cap` return keys. + +### Config-version Stamp +The act of recording the active `config_version` integer on each `prv_ai_visibility` row at insert time. Enables retrospective filtering: only rows with `config_version = active_version` are included in current-period scoring, preventing cross-config score pollution. + +**Code identifiers:** `config_version` column in `prv_ai_visibility`, `PRV_Config_Version::get_active_version()`. + +### Truncation State +When the monthly budget cap is reached mid-run, `prv_last_run_truncated` is set to `1`. This surfaces on the Settings page as a notice: "Last run was truncated by the monthly budget cap." The flag is cleared on the next successful full run. Probes after the cap are skipped (not errored) and counted in `skipped_budget`. + +**Code identifiers:** `prv_last_run_truncated` option, `PRV_Cost_Ledger::can_afford()`, `skipped_budget` count, `truncated` flag in run counts. + ### GEO Generative Engine Optimization — the practice of improving a brand's visibility in AI-generated responses. This plugin is the measurement instrument for peptiderepo.com's GEO program. diff --git a/includes/core/class-prv-config.php b/includes/core/class-prv-config.php index 0837113..90d324f 100644 --- a/includes/core/class-prv-config.php +++ b/includes/core/class-prv-config.php @@ -144,18 +144,54 @@ class PRV_Config { */ private static function default_peptides(): array { return array( - array( 'slug' => 'bpc-157', 'label' => 'BPC-157' ), - array( 'slug' => 'tb-500', 'label' => 'TB-500' ), - array( 'slug' => 'mk-677', 'label' => 'MK-677' ), - array( 'slug' => 'cjc-1295', 'label' => 'CJC-1295' ), - array( 'slug' => 'ghrp-6', 'label' => 'GHRP-6' ), - array( 'slug' => 'ipamorelin', 'label' => 'Ipamorelin' ), - array( 'slug' => 'semaglutide', 'label' => 'Semaglutide' ), - array( 'slug' => 'tirzepatide', 'label' => 'Tirzepatide' ), - array( 'slug' => 'selank', 'label' => 'Selank' ), - array( 'slug' => 'semax', 'label' => 'Semax' ), - array( 'slug' => 'nad-plus', 'label' => 'NAD+' ), - array( 'slug' => 'aod-9604', 'label' => 'AOD-9604' ), + array( + 'slug' => 'bpc-157', + 'label' => 'BPC-157', + ), + array( + 'slug' => 'tb-500', + 'label' => 'TB-500', + ), + array( + 'slug' => 'mk-677', + 'label' => 'MK-677', + ), + array( + 'slug' => 'cjc-1295', + 'label' => 'CJC-1295', + ), + array( + 'slug' => 'ghrp-6', + 'label' => 'GHRP-6', + ), + array( + 'slug' => 'ipamorelin', + 'label' => 'Ipamorelin', + ), + array( + 'slug' => 'semaglutide', + 'label' => 'Semaglutide', + ), + array( + 'slug' => 'tirzepatide', + 'label' => 'Tirzepatide', + ), + array( + 'slug' => 'selank', + 'label' => 'Selank', + ), + array( + 'slug' => 'semax', + 'label' => 'Semax', + ), + array( + 'slug' => 'nad-plus', + 'label' => 'NAD+', + ), + array( + 'slug' => 'aod-9604', + 'label' => 'AOD-9604', + ), ); } diff --git a/includes/core/class-prv-model-manager-table.php b/includes/core/class-prv-model-manager-table.php index f1cf9a1..4318846 100644 --- a/includes/core/class-prv-model-manager-table.php +++ b/includes/core/class-prv-model-manager-table.php @@ -75,12 +75,12 @@ class PRV_Model_Manager_Table { echo ''; // Provider. - echo '' . $prov . ''; + echo '' . esc_html( $prov ) . ''; // Slug (monospace). - echo '' . $slug . ''; + echo '' . esc_html( $slug ) . ''; if ( $note ) { - echo '
' . $note . ''; + echo '
' . esc_html( $note ) . ''; } echo ''; @@ -89,13 +89,14 @@ class PRV_Model_Manager_Table { $form_name = 'prv_enable_' . $id; echo '
'; echo ''; - echo ''; + echo ''; echo ''; echo ''; wp_nonce_field( PRV_Settings_Page::NONCE_MODEL, 'prv_nonce' ); // Disabled toggle for retired models. $toggle_attrs = $retired ? ' disabled aria-disabled="true"' : ''; $checked_attr = $enabled ? ' checked' : ''; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $checked_attr and $toggle_attrs are hard-coded attribute strings; $enabled is a boolean cast. echo ''; echo '
'; if ( $retired ) { @@ -104,12 +105,13 @@ class PRV_Model_Manager_Table { echo ''; // Run health. + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_health_badge() returns pre-escaped HTML markup. echo '' . $this->render_health_badge( $m ) . ''; // Test button + chip. $chip_id = 'prv-test-chip-' . $id; echo ''; - echo ''; echo ' '; @@ -119,7 +121,7 @@ class PRV_Model_Manager_Table { echo ''; echo '
'; echo ''; - echo ''; + echo ''; wp_nonce_field( PRV_Settings_Page::NONCE_MODEL, 'prv_nonce' ); echo ''; echo '
'; diff --git a/includes/core/class-prv-model-registry.php b/includes/core/class-prv-model-registry.php index 91c0ac8..a0c0b99 100644 --- a/includes/core/class-prv-model-registry.php +++ b/includes/core/class-prv-model-registry.php @@ -10,7 +10,7 @@ declare(strict_types=1); /** * Manages prv_models: v0.1.x→v0.2.0 migration, CRUD, per-model run-health. * - * v2 schema: [{id, provider, slug, enabled, note, health_status, health_probed, + * V2 schema: [{id, provider, slug, enabled, note, health_status, health_probed, * health_errors, health_run_id}]. v1 legacy: flat string array. * * Who triggers: PRV_Upgrader::run() (upgrade path); PRV_Settings_Page (CRUD). @@ -196,8 +196,8 @@ class PRV_Model_Registry { * * Side effects: Updates prv_models option. * - * @param string $run_id UUID of completed run. - * @param array $outcomes Per-slug outcome. + * @param string $run_id UUID of completed run. + * @param array $outcomes Per-slug outcome. * * @return void */ diff --git a/includes/core/class-prv-model-test-ajax.php b/includes/core/class-prv-model-test-ajax.php index b488824..d72f60f 100644 --- a/includes/core/class-prv-model-test-ajax.php +++ b/includes/core/class-prv-model-test-ajax.php @@ -74,18 +74,22 @@ class PRV_Model_Test_Ajax { try { $provider->probe( 'what is BPC-157' ); $ms = (int) round( ( microtime( true ) - $start ) * 1000 ); - wp_send_json_success( array( - 'resolved' => true, - 'latency_ms' => $ms, - 'slug' => $slug, - 'message' => sprintf( 'Resolved · %d ms', $ms ), - ) ); + wp_send_json_success( + array( + 'resolved' => true, + 'latency_ms' => $ms, + 'slug' => $slug, + 'message' => sprintf( 'Resolved · %d ms', $ms ), + ) + ); } catch ( \Exception $e ) { - wp_send_json_error( array( - 'resolved' => false, - 'slug' => $slug, - 'message' => 'Not resolved -- provider returned error (slug retired?)', - ) ); + wp_send_json_error( + array( + 'resolved' => false, + 'slug' => $slug, + 'message' => 'Not resolved -- provider returned error (slug retired?)', + ) + ); } } } diff --git a/includes/core/class-prv-probe-runner.php b/includes/core/class-prv-probe-runner.php index 3e4cfcd..0475d02 100644 --- a/includes/core/class-prv-probe-runner.php +++ b/includes/core/class-prv-probe-runner.php @@ -97,7 +97,10 @@ class PRV_Probe_Runner { $model_outcomes = array(); // Per-slug: {probed, errors}. foreach ( $models as $slug ) { - $model_outcomes[ $slug ] = array( 'probed' => 0, 'errors' => 0 ); + $model_outcomes[ $slug ] = array( + 'probed' => 0, + 'errors' => 0, + ); } $counts = array( @@ -233,7 +236,7 @@ class PRV_Probe_Runner { /** * Derive + persist the API-key status from run outcomes. * - * @param array $counts Run counts. + * @param array $counts Run counts. * @param array $model_outcomes Model outcomes. * * @return void diff --git a/includes/core/class-prv-settings-page.php b/includes/core/class-prv-settings-page.php index 62fd83d..c890fa2 100644 --- a/includes/core/class-prv-settings-page.php +++ b/includes/core/class-prv-settings-page.php @@ -115,6 +115,7 @@ class PRV_Settings_Page { $this->require_admin_nonce( self::NONCE_SAVE ); $old_cadence = PRV_Config::get_cadence(); + // phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce verified above via require_admin_nonce(). $budget = isset( $_POST['prv_monthly_budget_usd'] ) ? max( 0.01, (float) sanitize_text_field( wp_unslash( (string) $_POST['prv_monthly_budget_usd'] ) ) ) : PRV_DEFAULT_MONTHLY_BUDGET_USD; @@ -148,6 +149,7 @@ class PRV_Settings_Page { $intent_lines = array_filter( array_map( 'trim', explode( "\n", $raw_intents ) ) ); update_option( 'prv_prompt_intents', array_values( $intent_lines ) ); } + // phpcs:enable WordPress.Security.NonceVerification.Missing PRV_Config_Version::bump_version_if_changed(); @@ -168,10 +170,12 @@ class PRV_Settings_Page { */ public function handle_model_add(): void { $this->require_admin_nonce( self::NONCE_MODEL ); + // phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce verified above via require_admin_nonce(). $slug = sanitize_text_field( wp_unslash( (string) ( $_POST['prv_model_slug'] ?? '' ) ) ); $provider = sanitize_text_field( wp_unslash( (string) ( $_POST['prv_model_provider'] ?? 'openrouter' ) ) ); $note = sanitize_text_field( wp_unslash( (string) ( $_POST['prv_model_note'] ?? '' ) ) ); $enabled = ! empty( $_POST['prv_model_enabled'] ); + // phpcs:enable WordPress.Security.NonceVerification.Missing if ( '' !== $slug ) { PRV_Model_Registry::add( $slug, $provider, $enabled, $note ); PRV_Config_Version::bump_version_if_changed(); @@ -189,10 +193,12 @@ class PRV_Settings_Page { */ public function handle_model_update(): void { $this->require_admin_nonce( self::NONCE_MODEL ); + // phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce verified above via require_admin_nonce(). $id = sanitize_text_field( wp_unslash( (string) ( $_POST['prv_model_id'] ?? '' ) ) ); $slug = sanitize_text_field( wp_unslash( (string) ( $_POST['prv_model_slug'] ?? '' ) ) ); $enabled = ! empty( $_POST['prv_model_enabled'] ); $note = sanitize_text_field( wp_unslash( (string) ( $_POST['prv_model_note'] ?? '' ) ) ); + // phpcs:enable WordPress.Security.NonceVerification.Missing if ( '' !== $id && '' !== $slug ) { PRV_Model_Registry::update( $id, array( 'slug' => $slug, 'enabled' => $enabled, 'note' => $note ) ); PRV_Config_Version::bump_version_if_changed(); @@ -210,7 +216,9 @@ class PRV_Settings_Page { */ public function handle_model_remove(): void { $this->require_admin_nonce( self::NONCE_MODEL ); + // phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce verified above via require_admin_nonce(). $id = sanitize_text_field( wp_unslash( (string) ( $_POST['prv_model_id'] ?? '' ) ) ); + // phpcs:enable WordPress.Security.NonceVerification.Missing if ( '' !== $id ) { PRV_Model_Registry::remove( $id ); PRV_Config_Version::bump_version_if_changed(); diff --git a/includes/core/class-prv-settings-renderer.php b/includes/core/class-prv-settings-renderer.php index 34e7b5f..48a3331 100644 --- a/includes/core/class-prv-settings-renderer.php +++ b/includes/core/class-prv-settings-renderer.php @@ -169,7 +169,12 @@ class PRV_Settings_Renderer { $next_str = $next_ts ? gmdate( 'Y-m-d H:i T', (int) $next_ts ) : 'Not scheduled'; echo '
'; echo ''; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 31d4804..fd87f77 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -174,8 +174,6 @@ function wp_send_json_success( $data = null ): void { throw new \RuntimeExceptio function wp_send_json_error( $data = null ): void { throw new \RuntimeException( 'json_error:' . json_encode( $data ) ); } function is_wp_error( $thing ): bool { return $thing instanceof WP_Error; } -function defined( string $constant ): bool { return \defined( $constant ); } -function constant( string $constant ) { return \constant( $constant ); } function selected( $selected, $current = true, bool $echo = true ): string { $str = ( (string) $selected === (string) $current ) ? ' selected="selected"' : '';