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:
@@ -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.
|
||||
|
||||
37
CONTEXT.md
37
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.
|
||||
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>';
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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?)',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>';
|
||||
|
||||
@@ -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"' : '';
|
||||
|
||||
Reference in New Issue
Block a user