PR Vision v0.2.0: clear CI blockers (P1-A/B/C/D/E + P2 CONTEXT)

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
This commit is contained in:
peptiderepo
2026-06-14 07:10:35 +00:00
parent 8274d54a5e
commit a09bda0181
10 changed files with 136 additions and 38 deletions

View File

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

View File

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

View File

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

View File

@@ -75,12 +75,12 @@ class PRV_Model_Manager_Table {
echo '<tr class="' . esc_attr( $row_class ) . '">';
// Provider.
echo '<td>' . $prov . '</td>';
echo '<td>' . esc_html( $prov ) . '</td>';
// Slug (monospace).
echo '<td><code style="background:var(--prv-surface3);padding:1px 5px;border-radius:3px;font-size:12px;' . ( $retired ? 'text-decoration:line-through;' : '' ) . '">' . $slug . '</code>';
echo '<td><code style="background:var(--prv-surface3);padding:1px 5px;border-radius:3px;font-size:12px;' . ( $retired ? 'text-decoration:line-through;' : '' ) . '">' . esc_html( $slug ) . '</code>';
if ( $note ) {
echo '<br><span style="color:var(--prv-text-muted);font-size:11px">' . $note . '</span>';
echo '<br><span style="color:var(--prv-text-muted);font-size:11px">' . esc_html( $note ) . '</span>';
}
echo '</td>';
@@ -89,13 +89,14 @@ class PRV_Model_Manager_Table {
$form_name = 'prv_enable_' . $id;
echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline">';
echo '<input type="hidden" name="action" value="prv_model_update">';
echo '<input type="hidden" name="prv_model_id" value="' . $id . '">';
echo '<input type="hidden" name="prv_model_id" value="' . esc_attr( $id ) . '">';
echo '<input type="hidden" name="prv_model_slug" value="' . esc_attr( (string) ( $m['slug'] ?? '' ) ) . '">';
echo '<input type="hidden" name="prv_model_note" value="' . esc_attr( (string) ( $m['note'] ?? '' ) ) . '">';
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 '<input type="checkbox" name="prv_model_enabled" onchange="this.form.submit()" ' . $checked_attr . $toggle_attrs . ' title="' . ( $enabled ? 'Disable' : 'Enable' ) . '">';
echo '</form>';
if ( $retired ) {
@@ -104,12 +105,13 @@ class PRV_Model_Manager_Table {
echo '</td>';
// Run health.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- render_health_badge() returns pre-escaped HTML markup.
echo '<td>' . $this->render_health_badge( $m ) . '</td>';
// Test button + chip.
$chip_id = 'prv-test-chip-' . $id;
echo '<td>';
echo '<button type="button" class="prv-btn prv-btn-ghost prv-test-btn" data-model-id="' . $id . '">';
echo '<button type="button" class="prv-btn prv-btn-ghost prv-test-btn" data-model-id="' . esc_attr( $id ) . '">';
echo esc_html__( 'Test', 'pr-vision' );
echo '</button>';
echo ' <span id="' . esc_attr( $chip_id ) . '" class="prv-badge prv-badge-unknown" role="status" aria-live="polite"></span>';
@@ -119,7 +121,7 @@ class PRV_Model_Manager_Table {
echo '<td>';
echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline" onsubmit="return confirm(\'Remove this model?\');">';
echo '<input type="hidden" name="action" value="prv_model_remove">';
echo '<input type="hidden" name="prv_model_id" value="' . $id . '">';
echo '<input type="hidden" name="prv_model_id" value="' . esc_attr( $id ) . '">';
wp_nonce_field( PRV_Settings_Page::NONCE_MODEL, 'prv_nonce' );
echo '<button type="submit" class="prv-btn prv-btn-danger">' . esc_html__( 'Remove', 'pr-vision' ) . '</button>';
echo '</form>';

View File

@@ -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<string, array{probed: int, errors: int}> $outcomes Per-slug outcome.
* @param string $run_id UUID of completed run.
* @param array<string, array{probed: int, errors: int}> $outcomes Per-slug outcome.
*
* @return void
*/

View File

@@ -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?)',
)
);
}
}
}

View File

@@ -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<string, mixed> $counts Run counts.
* @param array<string, mixed> $counts Run counts.
* @param array<string, array{probed: int, errors: int}> $model_outcomes Model outcomes.
*
* @return void

View File

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

View File

@@ -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 '<div class="prv-field"><label class="prv-label" for="prv_cadence">' . esc_html__( 'Probe cadence', 'pr-vision' ) . '</label>';
echo '<select class="prv-input" id="prv_cadence" name="prv_cadence" data-scoring-relevant="1" style="max-width:200px">';
foreach ( array( 'weekly' => 'Weekly', 'twicedaily' => 'Twice daily', 'daily' => 'Daily' ) as $v => $l ) {
$cadence_options = array(
'weekly' => 'Weekly',
'twicedaily' => 'Twice daily',
'daily' => 'Daily',
);
foreach ( $cadence_options as $v => $l ) {
echo '<option value="' . esc_attr( $v ) . '"' . selected( $cadence, $v, false ) . '>' . esc_html( $l ) . '</option>';
}
echo '</select>';

View File

@@ -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"' : '';