PR Vision v0.1.1: rename from peptide-geo-monitor + P2 fixes

- Exhaustive rename: PGM_ -> PRV_, pgm_ -> prv_, text domain and
  slug peptide-geo-monitor -> pr-vision, admin menu GEO Monitor ->
  PR Vision, main file peptide-geo-monitor.php -> pr-vision.php,
  DB table pgm_ai_visibility -> prv_ai_visibility.
- P2-A: replace ltrim($host,'www.') character-mask with
  str_starts_with/substr prefix strip in PRV_Citation_Detector.
  Adds test coverage for wikipedia.org, webmd.com survival.
- P2-B: drop continue-on-error: true from PHPCS CI step;
  WordPress Coding Standards now gates the build.
- P2-C: remove dead in-run cache from PRV_Probe_Runner::run();
  each (peptide, intent, model) triplet is visited once per run,
  cache hit is structurally impossible.
- Version bump 0.1.0 -> 0.1.1 + CHANGELOG [0.1.1] entry.

Agent-Session: pr-vision rename v0.1.1
This commit is contained in:
peptiderepo
2026-06-13 09:32:56 +00:00
parent 46fda89e18
commit 60ae52663c
39 changed files with 852 additions and 830 deletions

View File

@@ -8,13 +8,13 @@ declare(strict_types=1);
* without duplication. Also parses raw citation arrays/URLs into a clean
* domain list.
*
* Who triggers: PGM_Perplexity_Provider, PGM_OpenRouter_Provider.
* Who triggers: PRV_Perplexity_Provider, PRV_OpenRouter_Provider.
* Dependencies: None (pure PHP).
*
* @see CONTEXT.md — "cited", "our_position", "source_domains".
* @package PeptideGeoMonitor
* @package PrVision
*/
class PGM_Citation_Detector {
class PRV_Citation_Detector {
/**
* Parse raw citation data from an API response into a clean domain list.
@@ -43,7 +43,8 @@ class PGM_Citation_Detector {
$host = (string) wp_parse_url( $url, PHP_URL_HOST );
if ( '' !== $host ) {
$domains[] = strtolower( ltrim( $host, 'www.' ) );
$bare = str_starts_with( $host, 'www.' ) ? substr( $host, 4 ) : $host;
$domains[] = strtolower( $bare );
}
}
@@ -51,18 +52,18 @@ class PGM_Citation_Detector {
}
/**
* Detect whether PGM_TARGET_DOMAIN appears in a domain list.
* Detect whether PRV_TARGET_DOMAIN appears in a domain list.
*
* @param string[] $domains Parsed domain list from parse_domains().
*
* @return bool
*/
public function is_cited( array $domains ): bool {
return in_array( PGM_TARGET_DOMAIN, $domains, true );
return in_array( PRV_TARGET_DOMAIN, $domains, true );
}
/**
* Find the 1-based position of PGM_TARGET_DOMAIN in a domain list.
* Find the 1-based position of PRV_TARGET_DOMAIN in a domain list.
*
* Returns null when the target domain is not found.
*
@@ -71,7 +72,7 @@ class PGM_Citation_Detector {
* @return int|null
*/
public function get_our_position( array $domains ): ?int {
$pos = array_search( PGM_TARGET_DOMAIN, $domains, true );
$pos = array_search( PRV_TARGET_DOMAIN, $domains, true );
if ( false === $pos ) {
return null;
}

View File

@@ -13,19 +13,19 @@ declare(strict_types=1);
* https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openrouter
*
* Required WP-config constants (documented in README.md):
* PGM_CF_ACCOUNT_ID — Cloudflare account ID
* PGM_CF_GATEWAY_ID — AI Gateway name (e.g. "peptiderepo-prod")
* PGM_OPENROUTER_API_KEY — OpenRouter API key (sk-or-…)
* PRV_CF_ACCOUNT_ID — Cloudflare account ID
* PRV_CF_GATEWAY_ID — AI Gateway name (e.g. "peptiderepo-prod")
* PRV_OPENROUTER_API_KEY — OpenRouter API key (sk-or-…)
*
* Who triggers: PGM_Perplexity_Provider, PGM_OpenRouter_Provider.
* Who triggers: PRV_Perplexity_Provider, PRV_OpenRouter_Provider.
* Dependencies: wp_remote_post(), add_action(), curl_setopt().
*
* @see class-pgm-perplexity-provider.php — Uses post_to_gateway().
* @see class-pgm-openrouter-provider.php — Uses post_to_gateway().
* @see class-prv-perplexity-provider.php — Uses post_to_gateway().
* @see class-prv-openrouter-provider.php — Uses post_to_gateway().
* @see ARCHITECTURE.md — §External API integrations.
* @package PeptideGeoMonitor
* @package PrVision
*/
class PGM_Gateway_Client {
class PRV_Gateway_Client {
/**
* Base URL for the Cloudflare AI Gateway (without trailing slash).
@@ -63,7 +63,7 @@ class PGM_Gateway_Client {
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
'HTTP-Referer' => home_url(),
'X-Title' => 'Peptide GEO Monitor',
'X-Title' => 'PR Vision',
);
// Belt-and-suspenders cURL injection — some hosts strip Authorization.
@@ -72,11 +72,11 @@ class PGM_Gateway_Client {
$last_error = '';
try {
for ( $attempt = 1; $attempt <= PGM_MAX_RETRIES; $attempt++ ) {
for ( $attempt = 1; $attempt <= PRV_MAX_RETRIES; $attempt++ ) {
$response = wp_remote_post(
$url,
array(
'timeout' => PGM_API_TIMEOUT_SECONDS,
'timeout' => PRV_API_TIMEOUT_SECONDS,
'headers' => $headers,
'body' => wp_json_encode( $body ),
)
@@ -84,8 +84,8 @@ class PGM_Gateway_Client {
if ( is_wp_error( $response ) ) {
$last_error = $response->get_error_message();
if ( $attempt < PGM_MAX_RETRIES ) {
sleep( PGM_RETRY_BASE_DELAY_SECONDS * (int) pow( 2, $attempt - 1 ) );
if ( $attempt < PRV_MAX_RETRIES ) {
sleep( PRV_RETRY_BASE_DELAY_SECONDS * (int) pow( 2, $attempt - 1 ) );
}
continue;
}
@@ -95,9 +95,9 @@ class PGM_Gateway_Client {
if ( $status >= 500 || 429 === $status ) {
$last_error = sprintf( 'HTTP %d', $status );
if ( $attempt < PGM_MAX_RETRIES ) {
if ( $attempt < PRV_MAX_RETRIES ) {
$retry_after = wp_remote_retrieve_header( $response, 'retry-after' );
$delay = $retry_after ? min( (int) $retry_after, 60 ) : PGM_RETRY_BASE_DELAY_SECONDS * (int) pow( 2, $attempt - 1 );
$delay = $retry_after ? min( (int) $retry_after, 60 ) : PRV_RETRY_BASE_DELAY_SECONDS * (int) pow( 2, $attempt - 1 );
sleep( $delay );
}
continue;
@@ -105,13 +105,13 @@ class PGM_Gateway_Client {
if ( $status >= 400 ) {
throw new \RuntimeException(
sprintf( 'GEO Monitor gateway HTTP %d: %s', $status, substr( $raw_body, 0, 300 ) )
sprintf( 'PR Vision gateway HTTP %d: %s', $status, substr( $raw_body, 0, 300 ) )
);
}
$data = json_decode( $raw_body, true );
if ( ! is_array( $data ) ) {
throw new \RuntimeException( 'GEO Monitor: invalid JSON response from gateway.' );
throw new \RuntimeException( 'PR Vision: invalid JSON response from gateway.' );
}
return $data;
@@ -121,7 +121,7 @@ class PGM_Gateway_Client {
}
throw new \RuntimeException(
sprintf( 'GEO Monitor: gateway failed after %d attempts. Last: %s', PGM_MAX_RETRIES, $last_error )
sprintf( 'PR Vision: gateway failed after %d attempts. Last: %s', PRV_MAX_RETRIES, $last_error )
);
}
@@ -133,13 +133,13 @@ class PGM_Gateway_Client {
* @return string Base URL without trailing slash.
*/
public function build_base_url( string $provider_slug ): string {
if ( defined( 'PGM_CF_ACCOUNT_ID' ) && defined( 'PGM_CF_GATEWAY_ID' )
&& '' !== PGM_CF_ACCOUNT_ID && '' !== PGM_CF_GATEWAY_ID ) {
if ( defined( 'PRV_CF_ACCOUNT_ID' ) && defined( 'PRV_CF_GATEWAY_ID' )
&& '' !== PRV_CF_ACCOUNT_ID && '' !== PRV_CF_GATEWAY_ID ) {
return sprintf(
'%s/%s/%s/%s',
self::GATEWAY_BASE,
PGM_CF_ACCOUNT_ID,
PGM_CF_GATEWAY_ID,
PRV_CF_ACCOUNT_ID,
PRV_CF_GATEWAY_ID,
$provider_slug
);
}

View File

@@ -6,7 +6,7 @@ declare(strict_types=1);
*
* One class handles any OpenRouter-routed model. The model identifier is set
* at construction time; the runner instantiates one provider per model from
* PGM_Config::get_models().
* PRV_Config::get_models().
*
* GPT-search (openai/gpt-4o-search-preview) and Gemini-search
* (google/gemini-2.0-flash-001) do not return a structured citations array
@@ -14,18 +14,18 @@ declare(strict_types=1);
* text or from any 'annotations' key the model may include.
*
* Required WP-config constant:
* PGM_OPENROUTER_API_KEY — OpenRouter key (sk-or-…)
* PRV_OPENROUTER_API_KEY — OpenRouter key (sk-or-…)
*
* Who triggers: PGM_Probe_Runner via PGM_Probe_Provider interface.
* Dependencies: PGM_Gateway_Client, PGM_Citation_Detector, PGM_Probe_Result.
* Who triggers: PRV_Probe_Runner via PRV_Probe_Provider interface.
* Dependencies: PRV_Gateway_Client, PRV_Citation_Detector, PRV_Probe_Result.
*
* @see interface-pgm-probe-provider.php — Interface this implements.
* @see class-pgm-gateway-client.php — Shared HTTP + retry logic.
* @see class-pgm-citation-detector.php — Domain extraction + detection.
* @see interface-prv-probe-provider.php — Interface this implements.
* @see class-prv-gateway-client.php — Shared HTTP + retry logic.
* @see class-prv-citation-detector.php — Domain extraction + detection.
* @see ARCHITECTURE.md — §Provider implementations.
* @package PeptideGeoMonitor
* @package PrVision
*/
class PGM_OpenRouter_Provider implements PGM_Probe_Provider {
class PRV_OpenRouter_Provider implements PRV_Probe_Provider {
/**
* Estimated cost per probe call in USD (conservative cross-model default).
@@ -38,34 +38,34 @@ class PGM_OpenRouter_Provider implements PGM_Probe_Provider {
private string $model;
/**
* @var PGM_Gateway_Client
* @var PRV_Gateway_Client
*/
private PGM_Gateway_Client $gateway;
private PRV_Gateway_Client $gateway;
/**
* @var PGM_Citation_Detector
* @var PRV_Citation_Detector
*/
private PGM_Citation_Detector $detector;
private PRV_Citation_Detector $detector;
/**
* @param string $model OpenRouter model identifier.
* @param PGM_Gateway_Client|null $gateway Injected for testing.
* @param PGM_Citation_Detector|null $detector Injected for testing.
* @param PRV_Gateway_Client|null $gateway Injected for testing.
* @param PRV_Citation_Detector|null $detector Injected for testing.
*/
public function __construct( string $model, ?PGM_Gateway_Client $gateway = null, ?PGM_Citation_Detector $detector = null ) {
public function __construct( string $model, ?PRV_Gateway_Client $gateway = null, ?PRV_Citation_Detector $detector = null ) {
$this->model = $model;
$this->gateway = $gateway ?? new PGM_Gateway_Client();
$this->detector = $detector ?? new PGM_Citation_Detector();
$this->gateway = $gateway ?? new PRV_Gateway_Client();
$this->detector = $detector ?? new PRV_Citation_Detector();
}
/**
* {@inheritDoc}
*
* @param string $query
* @return PGM_Probe_Result
* @return PRV_Probe_Result
* @throws \RuntimeException On permanent API failure.
*/
public function probe( string $query ): PGM_Probe_Result {
public function probe( string $query ): PRV_Probe_Result {
$api_key = $this->resolve_api_key();
$body = array(
@@ -82,7 +82,7 @@ class PGM_OpenRouter_Provider implements PGM_Probe_Provider {
}
/**
* Parse the OpenRouter response into a PGM_Probe_Result.
* Parse the OpenRouter response into a PRV_Probe_Result.
*
* Attempts to extract citations from:
* 1. `annotations` array (some models include structured source references).
@@ -90,9 +90,9 @@ class PGM_OpenRouter_Provider implements PGM_Probe_Provider {
*
* @param array<string, mixed> $data Decoded API response.
*
* @return PGM_Probe_Result
* @return PRV_Probe_Result
*/
public function parse_response( array $data ): PGM_Probe_Result {
public function parse_response( array $data ): PRV_Probe_Result {
$content = '';
if ( isset( $data['choices'][0]['message']['content'] ) ) {
$content = (string) $data['choices'][0]['message']['content'];
@@ -126,7 +126,7 @@ class PGM_OpenRouter_Provider implements PGM_Probe_Provider {
$cost_usd = (float) $data['usage']['total_tokens'] * 0.000002;
}
return new PGM_Probe_Result( $raw_excerpt, $domains, $cited, $our_position, $cost_usd );
return new PRV_Probe_Result( $raw_excerpt, $domains, $cited, $our_position, $cost_usd );
}
/**
@@ -140,7 +140,7 @@ class PGM_OpenRouter_Provider implements PGM_Probe_Provider {
* {@inheritDoc}
*/
public function is_configured(): bool {
return defined( 'PGM_OPENROUTER_API_KEY' ) && '' !== PGM_OPENROUTER_API_KEY;
return defined( 'PRV_OPENROUTER_API_KEY' ) && '' !== PRV_OPENROUTER_API_KEY;
}
/**
@@ -150,11 +150,11 @@ class PGM_OpenRouter_Provider implements PGM_Probe_Provider {
* @throws \RuntimeException When the constant is not defined or empty.
*/
private function resolve_api_key(): string {
if ( ! defined( 'PGM_OPENROUTER_API_KEY' ) || '' === PGM_OPENROUTER_API_KEY ) {
if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) || '' === PRV_OPENROUTER_API_KEY ) {
throw new \RuntimeException(
__( 'PGM_OPENROUTER_API_KEY constant is not defined. Add it to wp-config.php.', 'peptide-geo-monitor' )
__( 'PRV_OPENROUTER_API_KEY constant is not defined. Add it to wp-config.php.', 'pr-vision' )
);
}
return PGM_OPENROUTER_API_KEY;
return PRV_OPENROUTER_API_KEY;
}
}

View File

@@ -10,19 +10,19 @@ declare(strict_types=1);
* available (real web retrieval vs. training-time knowledge).
*
* Required WP-config constant:
* PGM_OPENROUTER_API_KEY — OpenRouter key (sk-or-…)
* PRV_OPENROUTER_API_KEY — OpenRouter key (sk-or-…)
*
* Who triggers: PGM_Probe_Runner via the PGM_Probe_Provider interface.
* Dependencies: PGM_Gateway_Client, PGM_Citation_Detector, PGM_Probe_Result.
* Who triggers: PRV_Probe_Runner via the PRV_Probe_Provider interface.
* Dependencies: PRV_Gateway_Client, PRV_Citation_Detector, PRV_Probe_Result.
*
* @see interface-pgm-probe-provider.php — Interface this implements.
* @see class-pgm-gateway-client.php — Shared HTTP + retry logic.
* @see class-pgm-citation-detector.php — Domain extraction + detection.
* @see interface-prv-probe-provider.php — Interface this implements.
* @see class-prv-gateway-client.php — Shared HTTP + retry logic.
* @see class-prv-citation-detector.php — Domain extraction + detection.
* @see ARCHITECTURE.md — §Provider implementations.
* @see CONTEXT.md — "Perplexity sonar", "citations".
* @package PeptideGeoMonitor
* @package PrVision
*/
class PGM_Perplexity_Provider implements PGM_Probe_Provider {
class PRV_Perplexity_Provider implements PRV_Probe_Provider {
/**
* Model identifier passed to OpenRouter for Perplexity routing.
@@ -36,22 +36,22 @@ class PGM_Perplexity_Provider implements PGM_Probe_Provider {
const ESTIMATED_COST_PER_PROBE = 0.005;
/**
* @var PGM_Gateway_Client
* @var PRV_Gateway_Client
*/
private PGM_Gateway_Client $gateway;
private PRV_Gateway_Client $gateway;
/**
* @var PGM_Citation_Detector
* @var PRV_Citation_Detector
*/
private PGM_Citation_Detector $detector;
private PRV_Citation_Detector $detector;
/**
* @param PGM_Gateway_Client|null $gateway Injected for testing; auto-created otherwise.
* @param PGM_Citation_Detector|null $detector Injected for testing; auto-created otherwise.
* @param PRV_Gateway_Client|null $gateway Injected for testing; auto-created otherwise.
* @param PRV_Citation_Detector|null $detector Injected for testing; auto-created otherwise.
*/
public function __construct( ?PGM_Gateway_Client $gateway = null, ?PGM_Citation_Detector $detector = null ) {
$this->gateway = $gateway ?? new PGM_Gateway_Client();
$this->detector = $detector ?? new PGM_Citation_Detector();
public function __construct( ?PRV_Gateway_Client $gateway = null, ?PRV_Citation_Detector $detector = null ) {
$this->gateway = $gateway ?? new PRV_Gateway_Client();
$this->detector = $detector ?? new PRV_Citation_Detector();
}
/**
@@ -62,10 +62,10 @@ class PGM_Perplexity_Provider implements PGM_Probe_Provider {
*
* @param string $query
*
* @return PGM_Probe_Result
* @return PRV_Probe_Result
* @throws \RuntimeException On permanent API failure.
*/
public function probe( string $query ): PGM_Probe_Result {
public function probe( string $query ): PRV_Probe_Result {
$api_key = $this->resolve_api_key();
$body = array(
@@ -82,15 +82,15 @@ class PGM_Perplexity_Provider implements PGM_Probe_Provider {
}
/**
* Parse the OpenRouter/Perplexity response into a PGM_Probe_Result.
* Parse the OpenRouter/Perplexity response into a PRV_Probe_Result.
*
* Side effects: None.
*
* @param array<string, mixed> $data Decoded API response.
*
* @return PGM_Probe_Result
* @return PRV_Probe_Result
*/
public function parse_response( array $data ): PGM_Probe_Result {
public function parse_response( array $data ): PRV_Probe_Result {
// Extract text excerpt from the first choice.
$content = '';
if ( isset( $data['choices'][0]['message']['content'] ) ) {
@@ -115,7 +115,7 @@ class PGM_Perplexity_Provider implements PGM_Probe_Provider {
$cost_usd = (float) $data['usage']['total_tokens'] * 0.000001;
}
return new PGM_Probe_Result( $raw_excerpt, $domains, $cited, $our_position, $cost_usd );
return new PRV_Probe_Result( $raw_excerpt, $domains, $cited, $our_position, $cost_usd );
}
/**
@@ -129,7 +129,7 @@ class PGM_Perplexity_Provider implements PGM_Probe_Provider {
* {@inheritDoc}
*/
public function is_configured(): bool {
return defined( 'PGM_OPENROUTER_API_KEY' ) && '' !== PGM_OPENROUTER_API_KEY;
return defined( 'PRV_OPENROUTER_API_KEY' ) && '' !== PRV_OPENROUTER_API_KEY;
}
/**
@@ -139,11 +139,11 @@ class PGM_Perplexity_Provider implements PGM_Probe_Provider {
* @throws \RuntimeException When the constant is not defined or empty.
*/
private function resolve_api_key(): string {
if ( ! defined( 'PGM_OPENROUTER_API_KEY' ) || '' === PGM_OPENROUTER_API_KEY ) {
if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) || '' === PRV_OPENROUTER_API_KEY ) {
throw new \RuntimeException(
__( 'PGM_OPENROUTER_API_KEY constant is not defined. Add it to wp-config.php.', 'peptide-geo-monitor' )
__( 'PRV_OPENROUTER_API_KEY constant is not defined. Add it to wp-config.php.', 'pr-vision' )
);
}
return PGM_OPENROUTER_API_KEY;
return PRV_OPENROUTER_API_KEY;
}
}

View File

@@ -8,34 +8,34 @@ declare(strict_types=1);
* OpenRouter GPT-search, OpenRouter Gemini-search, …). Swapping providers
* requires only a new class — the runner, ledger, and storage are agnostic.
*
* Who triggers: PGM_Probe_Runner for each (model, query) combination.
* Who triggers: PRV_Probe_Runner for each (model, query) combination.
* Dependencies: None — pure interface.
*
* @see class-pgm-perplexity-provider.php — Perplexity sonar implementation.
* @see class-pgm-openrouter-provider.php — OpenRouter generic implementation.
* @see class-pgm-probe-runner.php — Dispatches to providers via this interface.
* @see class-prv-perplexity-provider.php — Perplexity sonar implementation.
* @see class-prv-openrouter-provider.php — OpenRouter generic implementation.
* @see class-prv-probe-runner.php — Dispatches to providers via this interface.
* @see CONTEXT.md — "provider", "probe".
* @package PeptideGeoMonitor
* @package PrVision
*/
interface PGM_Probe_Provider {
interface PRV_Probe_Provider {
/**
* Send a search/retrieval query and return a structured probe result.
*
* Implementations MUST:
* - Extract cited source domains from the model's response.
* - Detect whether PGM_TARGET_DOMAIN appears in those sources.
* - Detect whether PRV_TARGET_DOMAIN appears in those sources.
* - Record our 1-based position (null when not cited).
* - Return the call's estimated USD cost.
* - Throw \RuntimeException on unrecoverable failure (caller logs + skips).
*
* @param string $query The natural-language query to send.
*
* @return PGM_Probe_Result
* @return PRV_Probe_Result
*
* @throws \RuntimeException When the API call fails after retries.
*/
public function probe( string $query ): PGM_Probe_Result;
public function probe( string $query ): PRV_Probe_Result;
/**
* Get a short human-readable name for this provider (e.g. "Perplexity sonar").