From 60ae52663c0b609d2f11c36030b04a83ddb68177 Mon Sep 17 00:00:00 2001 From: peptiderepo <273788462+peptiderepo@users.noreply.github.com> Date: Sat, 13 Jun 2026 09:32:56 +0000 Subject: [PATCH] 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 --- .github/workflows/ci.yml | 1 - ARCHITECTURE.md | 98 ++++++------ CHANGELOG.md | 57 ++++--- CONTEXT.md | 46 +++--- CONVENTIONS.md | 40 ++--- README.md | 18 +-- composer.json | 2 +- ... => class-prv-ai-visibility-collector.php} | 32 ++-- ...r.php => interface-prv-data-collector.php} | 16 +- ...-activator.php => class-prv-activator.php} | 18 +-- ...dmin-page.php => class-prv-admin-page.php} | 86 +++++------ ...utoloader.php => class-prv-autoloader.php} | 30 ++-- ...y.php => class-prv-collector-registry.php} | 38 ++--- ...ss-pgm-config.php => class-prv-config.php} | 26 ++-- ...t-ledger.php => class-prv-cost-ledger.php} | 26 ++-- ...{class-pgm-cron.php => class-prv-cron.php} | 36 ++--- ...ctivator.php => class-prv-deactivator.php} | 14 +- ...ss-pgm-plugin.php => class-prv-plugin.php} | 20 +-- ...-result.php => class-prv-probe-result.php} | 12 +- ...-runner.php => class-prv-probe-runner.php} | 84 +++++------ ...anager.php => class-prv-table-manager.php} | 20 +-- ....php => class-prv-ai-visibility-panel.php} | 60 ++++---- ....php => interface-prv-dashboard-panel.php} | 18 +-- ...or.php => class-prv-citation-detector.php} | 17 ++- ...lient.php => class-prv-gateway-client.php} | 44 +++--- ....php => class-prv-openrouter-provider.php} | 56 +++---- ....php => class-prv-perplexity-provider.php} | 54 +++---- ...r.php => interface-prv-probe-provider.php} | 18 +-- phpcs.xml.dist | 4 +- peptide-geo-monitor.php => pr-vision.php | 38 ++--- tests/bootstrap.php | 92 ++++++------ tests/unit/test-citation-detector.php | 65 ++++---- tests/unit/test-cost-ledger.php | 84 +++++------ tests/unit/test-cron.php | 56 +++---- tests/unit/test-probe-runner.php | 140 +++++++++--------- tests/unit/test-provider-parsing.php | 66 ++++----- tests/unit/test-score-calc.php | 40 ++--- tests/unit/test-uninstall.php | 92 ++++++------ uninstall.php | 18 +-- 39 files changed, 852 insertions(+), 830 deletions(-) rename includes/collector/{class-pgm-ai-visibility-collector.php => class-prv-ai-visibility-collector.php} (86%) rename includes/collector/{interface-pgm-data-collector.php => interface-prv-data-collector.php} (65%) rename includes/core/{class-pgm-activator.php => class-prv-activator.php} (54%) rename includes/core/{class-pgm-admin-page.php => class-prv-admin-page.php} (59%) rename includes/core/{class-pgm-autoloader.php => class-prv-autoloader.php} (59%) rename includes/core/{class-pgm-collector-registry.php => class-prv-collector-registry.php} (63%) rename includes/core/{class-pgm-config.php => class-prv-config.php} (82%) rename includes/core/{class-pgm-cost-ledger.php => class-prv-cost-ledger.php} (80%) rename includes/core/{class-pgm-cron.php => class-prv-cron.php} (54%) rename includes/core/{class-pgm-deactivator.php => class-prv-deactivator.php} (57%) rename includes/core/{class-pgm-plugin.php => class-prv-plugin.php} (57%) rename includes/core/{class-pgm-probe-result.php => class-prv-probe-result.php} (83%) rename includes/core/{class-pgm-probe-runner.php => class-prv-probe-runner.php} (64%) rename includes/core/{class-pgm-table-manager.php => class-prv-table-manager.php} (81%) rename includes/panel/{class-pgm-ai-visibility-panel.php => class-prv-ai-visibility-panel.php} (63%) rename includes/panel/{interface-pgm-dashboard-panel.php => interface-prv-dashboard-panel.php} (55%) rename includes/providers/{class-pgm-citation-detector.php => class-prv-citation-detector.php} (78%) rename includes/providers/{class-pgm-gateway-client.php => class-prv-gateway-client.php} (78%) rename includes/providers/{class-pgm-openrouter-provider.php => class-prv-openrouter-provider.php} (69%) rename includes/providers/{class-pgm-perplexity-provider.php => class-prv-perplexity-provider.php} (67%) rename includes/providers/{interface-pgm-probe-provider.php => interface-prv-probe-provider.php} (75%) rename peptide-geo-monitor.php => pr-vision.php (69%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c85f64..295b155 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,6 @@ jobs: - name: Run PHPCS run: composer run lint - continue-on-error: true # ── 300-line file check ────────────────────────────────────────── file-size: diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b55b94b..cba6123 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,12 +1,12 @@ -# Architecture — Peptide GEO Monitor +# Architecture — PR Vision -**Version:** 0.1.0 | **Last updated:** 2026-06-13 +**Version:** 0.1.1 | **Last updated:** 2026-06-13 --- ## 1. Overview -`peptide-geo-monitor` is an internal WordPress plugin that runs weekly server-side LLM probes to measure whether peptiderepo.com appears in AI citations across core peptide queries. It stores a time-series in a custom table and renders an admin dashboard. +`pr-vision` is an internal WordPress plugin that runs weekly server-side LLM probes to measure whether peptiderepo.com appears in AI citations across core peptide queries. It stores a time-series in a custom table and renders an admin dashboard. **v1 scope:** AI-visibility only. The collector/panel seam is present so future data categories (keyword rankings, technical-SEO) can be added as new classes. @@ -15,37 +15,37 @@ ## 2. File tree ``` -peptide-geo-monitor/ -├── peptide-geo-monitor.php # Plugin boot: constants, autoloader, hooks -├── uninstall.php # Full data teardown: DROP TABLE + DELETE pgm_* options +pr-vision/ +├── pr-vision.php # Plugin boot: constants, autoloader, hooks +├── uninstall.php # Full data teardown: DROP TABLE + DELETE prv_* options ├── composer.json # Dev deps (PHPCS only) ├── phpcs.xml.dist # PHPCS config (WordPress standard) ├── includes/ │ ├── core/ -│ │ ├── class-pgm-autoloader.php # SPL autoloader: PGM_* → includes/** -│ │ ├── class-pgm-plugin.php # Orchestrator: boots sub-systems -│ │ ├── class-pgm-activator.php # Activation: table + defaults + cron -│ │ ├── class-pgm-deactivator.php # Deactivation: clear cron -│ │ ├── class-pgm-table-manager.php # dbDelta create/drop + get_table_name() -│ │ ├── class-pgm-config.php # Typed getters + seed_defaults() -│ │ ├── class-pgm-cron.php # Weekly WP-Cron schedule/clear -│ │ ├── class-pgm-cost-ledger.php # MTD cost + hard monthly cap enforcement -│ │ ├── class-pgm-probe-result.php # Immutable value object from a probe call -│ │ ├── class-pgm-probe-runner.php # Orchestrates peptide×intent×model run -│ │ ├── class-pgm-collector-registry.php # Singleton registry for collectors + panels -│ │ └── class-pgm-admin-page.php # Admin page: menu, Run now, rendering +│ │ ├── class-prv-autoloader.php # SPL autoloader: PRV_* → includes/** +│ │ ├── class-prv-plugin.php # Orchestrator: boots sub-systems +│ │ ├── class-prv-activator.php # Activation: table + defaults + cron +│ │ ├── class-prv-deactivator.php # Deactivation: clear cron +│ │ ├── class-prv-table-manager.php # dbDelta create/drop + get_table_name() +│ │ ├── class-prv-config.php # Typed getters + seed_defaults() +│ │ ├── class-prv-cron.php # Weekly WP-Cron schedule/clear +│ │ ├── class-prv-cost-ledger.php # MTD cost + hard monthly cap enforcement +│ │ ├── class-prv-probe-result.php # Immutable value object from a probe call +│ │ ├── class-prv-probe-runner.php # Orchestrates peptide×intent×model run +│ │ ├── class-prv-collector-registry.php # Singleton registry for collectors + panels +│ │ └── class-prv-admin-page.php # Admin page: menu, Run now, rendering │ ├── providers/ -│ │ ├── interface-pgm-probe-provider.php # probe(query): PGM_Probe_Result contract -│ │ ├── class-pgm-gateway-client.php # Cloudflare AI Gateway HTTP + retry -│ │ ├── class-pgm-citation-detector.php # Domain extraction + cite detection -│ │ ├── class-pgm-perplexity-provider.php # Perplexity sonar via OpenRouter/gateway -│ │ └── class-pgm-openrouter-provider.php # Generic OpenRouter (GPT-search, Gemini) +│ │ ├── interface-prv-probe-provider.php # probe(query): PRV_Probe_Result contract +│ │ ├── class-prv-gateway-client.php # Cloudflare AI Gateway HTTP + retry +│ │ ├── class-prv-citation-detector.php # Domain extraction + cite detection +│ │ ├── class-prv-perplexity-provider.php # Perplexity sonar via OpenRouter/gateway +│ │ └── class-prv-openrouter-provider.php # Generic OpenRouter (GPT-search, Gemini) │ ├── collector/ -│ │ ├── interface-pgm-data-collector.php # collect(): array seam -│ │ └── class-pgm-ai-visibility-collector.php # AI-visibility data from DB +│ │ ├── interface-prv-data-collector.php # collect(): array seam +│ │ └── class-prv-ai-visibility-collector.php # AI-visibility data from DB │ └── panel/ -│ ├── interface-pgm-dashboard-panel.php # render(data): void seam -│ └── class-pgm-ai-visibility-panel.php # Renders trendline + standings +│ ├── interface-prv-dashboard-panel.php # render(data): void seam +│ └── class-prv-ai-visibility-panel.php # Renders trendline + standings ├── tests/ │ ├── bootstrap.php # WP-stub bootstrap (no PHPUnit) │ └── unit/ @@ -67,37 +67,37 @@ peptide-geo-monitor/ WP-Cron (weekly) ──────────────────────────────────────┐ Admin "Run now" (POST + nonce) ─────────────────────────┤ ↓ - PGM_Probe_Runner::run() + PRV_Probe_Runner::run() │ ┌───────────────────────────────────┤ │ for each peptide × intent × model│ │ │ - │ PGM_Cost_Ledger::can_afford() │ + │ PRV_Cost_Ledger::can_afford() │ │ └─ ABORT gracefully if at cap │ │ │ - │ PGM_Probe_Provider::probe(query) │ - │ └─ PGM_Gateway_Client │ + │ PRV_Probe_Provider::probe(query) │ + │ └─ PRV_Gateway_Client │ │ └─ Cloudflare AI Gateway │ │ └─ OpenRouter │ │ └─ LLM │ │ │ - │ PGM_Citation_Detector │ + │ PRV_Citation_Detector │ │ └─ extract domains │ │ └─ detect peptiderepo.com │ │ │ - │ $wpdb→insert(pgm_ai_visibility) │ - │ PGM_Cost_Ledger::update_row_cost()│ + │ $wpdb→insert(prv_ai_visibility) │ + │ PRV_Cost_Ledger::update_row_cost()│ └───────────────────────────────────┘ │ Admin page load ────────────────────────────────────────┤ ↓ - PGM_Admin_Page::render_page() + PRV_Admin_Page::render_page() │ - PGM_Collector_Registry + PRV_Collector_Registry │ - PGM_Ai_Visibility_Collector::collect() + PRV_Ai_Visibility_Collector::collect() │ (DB reads) - PGM_Ai_Visibility_Panel::render() + PRV_Ai_Visibility_Panel::render() │ (HTML output) Browser (Chart.js) ``` @@ -106,7 +106,7 @@ Admin page load ───────────────────── ## 4. Database -**Table:** `{prefix}pgm_ai_visibility` +**Table:** `{prefix}prv_ai_visibility` | Column | Type | Notes | |--------|------|-------| @@ -123,7 +123,7 @@ Admin page load ───────────────────── | raw_excerpt | LONGTEXT | First 500 chars of LLM response | | cost_usd | DECIMAL(12,8) | Actual call cost | -Schema version tracked in `pgm_schema_version` option. +Schema version tracked in `prv_schema_version` option. --- @@ -133,7 +133,7 @@ Schema version tracked in `pgm_schema_version` option. All LLM calls route through: ``` -https://gateway.ai.cloudflare.com/v1/{PGM_CF_ACCOUNT_ID}/{PGM_CF_GATEWAY_ID}/openrouter +https://gateway.ai.cloudflare.com/v1/{PRV_CF_ACCOUNT_ID}/{PRV_CF_GATEWAY_ID}/openrouter ``` Falls back to direct OpenRouter (`https://openrouter.ai/api/v1`) when the constants are absent or empty. @@ -144,26 +144,26 @@ Pattern mirrors PRAutoBlogger's `class-open-router-config.php` and `class-open-r | Provider | Model | Citation source | |----------|-------|-----------------| -| PGM_Perplexity_Provider | `perplexity/sonar` | `citations[]` array (primary, real-web retrieval) | -| PGM_OpenRouter_Provider | `openai/gpt-4o-search-preview` | annotations or inline URL regex | -| PGM_OpenRouter_Provider | `google/gemini-2.0-flash-001` | annotations or inline URL regex | +| PRV_Perplexity_Provider | `perplexity/sonar` | `citations[]` array (primary, real-web retrieval) | +| PRV_OpenRouter_Provider | `openai/gpt-4o-search-preview` | annotations or inline URL regex | +| PRV_OpenRouter_Provider | `google/gemini-2.0-flash-001` | annotations or inline URL regex | --- ## 6. Collector / Panel seam ``` -PGM_Data_Collector (interface) PGM_Dashboard_Panel (interface) +PRV_Data_Collector (interface) PRV_Dashboard_Panel (interface) │ │ -PGM_Ai_Visibility_Collector PGM_Ai_Visibility_Panel +PRV_Ai_Visibility_Collector PRV_Ai_Visibility_Panel │ │ - └──────── PGM_Collector_Registry ──────────┘ + └──────── PRV_Collector_Registry ──────────┘ (key: "ai_visibility") ``` To add a future SEO collector (keyword rankings, schema coverage…): -1. Implement `PGM_Data_Collector` + `PGM_Dashboard_Panel`. -2. Register both in `PGM_Plugin::init()`. +1. Implement `PRV_Data_Collector` + `PRV_Dashboard_Panel`. +2. Register both in `PRV_Plugin::init()`. No dashboard shell changes required. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bc7db6..685d782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog — Peptide GEO Monitor +# Changelog — PR Vision All notable changes to this project will be documented in this file. @@ -7,28 +7,51 @@ Versioning: [Semantic Versioning](https://semver.org/) --- +## [0.1.1] — 2026-06-13 + +Rename to PR Vision; fix www-strip in competitor display; PHPCS gating; remove dead cache. + +### Changed + +- **Rename:** plugin, repo, slug, class prefix, option/hook/table prefix all updated from `pgm_`/`PGM_` to `prv_`/`PRV_` and from `peptide-geo-monitor`/`GEO Monitor` to `pr-vision`/`PR Vision`. No data migration required (no `pgm_*` data existed). + +### Fixed + +- **P2-A:** `ltrim($host, 'www.')` character-mask bug in `PRV_Citation_Detector::parse_domains()` replaced with `str_starts_with($host, 'www.') ? substr($host, 4) : $host`. Domains starting with `w` (e.g. `wikipedia.org`, `webmd.com`) are no longer corrupted in competitor standings. +- **P2-C:** Removed dead in-run cache from `PRV_Probe_Runner::run()`. The `peptides × intents × models` loop visits each `(slug, intent, model)` triplet exactly once, so the cache could never produce a hit on a single traversal. + +### CI + +- **P2-B:** Dropped `continue-on-error: true` from the PHPCS step — WordPress Coding Standards now gates the build. PHPCS was already green on v0.1.0; this enforces the DoD for future commits. + +### Tests + +- Added four assertions to `test-citation-detector.php` covering `wikipedia.org`, `webmd.com` (non-www domains starting with `w`) and `www.examine.com` to lock in the P2-A fix. + +--- + ## [0.1.0] — 2026-06-13 **Initial release — AI-visibility dashboard.** ### Added -- `PGM_Probe_Provider` interface + `PGM_Probe_Result` value object. -- `PGM_Perplexity_Provider`: Perplexity `sonar` via OpenRouter/Cloudflare AI Gateway; parses `citations[]` array. -- `PGM_OpenRouter_Provider`: generic parameterised provider for GPT-search and Gemini-search models; parses annotations + inline URL regex fallback. -- `PGM_Gateway_Client`: shared CF AI Gateway HTTP client with exponential-backoff retry and cURL auth injection (mirrors PRAutoBlogger pattern). -- `PGM_Citation_Detector`: domain extraction from citation arrays/URLs, `is_cited()`, `get_our_position()`. -- `PGM_Cost_Ledger`: month-to-date spend from DB, `can_afford()` pre-check, `update_row_cost()` settlement. -- `PGM_Probe_Runner`: full peptides × intents × models run with budget cap abort, in-run cache, and partial-run persistence. -- `PGM_Table_Manager`: `{prefix}pgm_ai_visibility` via `dbDelta`, schema-version option, `drop_table()` for uninstall. -- `PGM_Config`: typed getters + `seed_defaults()` with ~12 peptides, 3 intents, 3 models. -- `PGM_Cron`: weekly `pgm_weekly_probe` WP-Cron; `schedule_weekly()` / `clear_schedule()`. -- `PGM_Data_Collector` + `PGM_Dashboard_Panel` interfaces (collector/panel seam). -- `PGM_Collector_Registry`: singleton registry for future extensibility. -- `PGM_Ai_Visibility_Collector`: trendline (per-run visibility score), standings (per-peptide latest), MTD cost. -- `PGM_Ai_Visibility_Panel`: proxy-metric note, Chart.js trendline, per-peptide standings table, MTD cost bar, "Run now" button. -- `PGM_Admin_Page`: "GEO Monitor" top-level menu, Chart.js enqueue, Run now POST handler (nonce + `manage_options`). -- `uninstall.php`: DROP TABLE + DELETE `pgm_*` options + `wp_clear_scheduled_hook`. +- `PRV_Probe_Provider` interface + `PRV_Probe_Result` value object. +- `PRV_Perplexity_Provider`: Perplexity `sonar` via OpenRouter/Cloudflare AI Gateway; parses `citations[]` array. +- `PRV_OpenRouter_Provider`: generic parameterised provider for GPT-search and Gemini-search models; parses annotations + inline URL regex fallback. +- `PRV_Gateway_Client`: shared CF AI Gateway HTTP client with exponential-backoff retry and cURL auth injection (mirrors PRAutoBlogger pattern). +- `PRV_Citation_Detector`: domain extraction from citation arrays/URLs, `is_cited()`, `get_our_position()`. +- `PRV_Cost_Ledger`: month-to-date spend from DB, `can_afford()` pre-check, `update_row_cost()` settlement. +- `PRV_Probe_Runner`: full peptides × intents × models run with budget cap abort and partial-run persistence. +- `PRV_Table_Manager`: `{prefix}prv_ai_visibility` via `dbDelta`, schema-version option, `drop_table()` for uninstall. +- `PRV_Config`: typed getters + `seed_defaults()` with ~12 peptides, 3 intents, 3 models. +- `PRV_Cron`: weekly `prv_weekly_probe` WP-Cron; `schedule_weekly()` / `clear_schedule()`. +- `PRV_Data_Collector` + `PRV_Dashboard_Panel` interfaces (collector/panel seam). +- `PRV_Collector_Registry`: singleton registry for future extensibility. +- `PRV_Ai_Visibility_Collector`: trendline (per-run visibility score), standings (per-peptide latest), MTD cost. +- `PRV_Ai_Visibility_Panel`: proxy-metric note, Chart.js trendline, per-peptide standings table, MTD cost bar, "Run now" button. +- `PRV_Admin_Page`: "PR Vision" top-level menu, Chart.js enqueue, Run now POST handler (nonce + `manage_options`). +- `uninstall.php`: DROP TABLE + DELETE `prv_*` options + `wp_clear_scheduled_hook`. - Unit tests: citation detection, score formula, cost ledger + budget cap abort, provider response parsing (mock HTTP), cron registration, uninstall purge. - `ARCHITECTURE.md`, `CONVENTIONS.md`, `CONTEXT.md` (incl. visibility score formula), `README.md`. - CI: PHP lint (8.1/8.2/8.3) + PHPCS + 300-line check (mirrors `peptide-repo-core`). diff --git a/CONTEXT.md b/CONTEXT.md index ec249bc..1cc26d1 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,8 +1,8 @@ -# Context — Domain Glossary (Peptide GEO Monitor) +# Context — Domain Glossary (PR Vision) -**Version:** 0.1.0 | **Last updated:** 2026-06-13 +**Version:** 0.1.1 | **Last updated:** 2026-06-13 -This file is the ubiquitous-language glossary for `peptide-geo-monitor`. Every term below has a precise meaning within this codebase. AI agents and human contributors should use this terminology consistently. +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. --- @@ -11,42 +11,42 @@ This file is the ubiquitous-language glossary for `peptide-geo-monitor`. Every t ### AI Visibility Whether and where peptiderepo.com appears in AI assistant responses to peptide-related queries. The core metric this plugin measures. -**Code identifiers:** `PGM_Ai_Visibility_Collector`, `PGM_Ai_Visibility_Panel`, table column `cited`, option prefix `pgm_`. +**Code identifiers:** `PRV_Ai_Visibility_Collector`, `PRV_Ai_Visibility_Panel`, table column `cited`, option prefix `prv_`. ### Probe -A single server-side LLM API call with a specific `(peptide, intent, model)` combination. The result is a `PGM_Probe_Result` value object. +A single server-side LLM API call with a specific `(peptide, intent, model)` combination. The result is a `PRV_Probe_Result` value object. -**Code identifiers:** `PGM_Probe_Provider::probe()`, `PGM_Probe_Runner::run()`. +**Code identifiers:** `PRV_Probe_Provider::probe()`, `PRV_Probe_Runner::run()`. ### Probe Run A batch execution of all configured `peptides × prompt_intents × models`. Identified by a `run_id` (UUID). One cron tick = one run; the "Run now" button also triggers a run. -**Code identifiers:** `PGM_Probe_Runner::run()`, `run_id` column in `pgm_ai_visibility`. +**Code identifiers:** `PRV_Probe_Runner::run()`, `run_id` column in `prv_ai_visibility`. ### Peptide -A compound being tracked (e.g. BPC-157, TB-500). Stored as a slug + label pair in the `pgm_peptides` option. +A compound being tracked (e.g. BPC-157, TB-500). Stored as a slug + label pair in the `prv_peptides` option. -**Code identifiers:** `pgm_peptides` option, `peptide_slug` + `peptide_label` columns. +**Code identifiers:** `prv_peptides` option, `peptide_slug` + `peptide_label` columns. ### Prompt Intent A query template describing a searcher's intent (e.g. "what is {peptide}", "{peptide} reconstitution guide"). The `{peptide}` placeholder is replaced with the peptide's label at runtime. -**Code identifiers:** `pgm_prompt_intents` option, `prompt_intent` column. +**Code identifiers:** `prv_prompt_intents` option, `prompt_intent` column. ### Citation / Cited A probe is **cited** when `peptiderepo.com` appears in the LLM's source/citation list for that response. The `cited` column is `TINYINT(1)` (1 = cited, 0 = not cited). -**Code identifiers:** `PGM_Citation_Detector::is_cited()`, `cited` column, `PGM_Probe_Result::is_cited()`. +**Code identifiers:** `PRV_Citation_Detector::is_cited()`, `cited` column, `PRV_Probe_Result::is_cited()`. ### Source Domains The list of domains returned by the LLM as its sources for a given response (e.g. `["peptiderepo.com", "examine.com"]`). Stored as JSON in `source_domains`. -**Code identifiers:** `PGM_Citation_Detector::parse_domains()`, `PGM_Probe_Result::get_source_domains()`, `source_domains` column. +**Code identifiers:** `PRV_Citation_Detector::parse_domains()`, `PRV_Probe_Result::get_source_domains()`, `source_domains` column. ### Our Position The 1-based rank of `peptiderepo.com` in the source domains list for a cited probe (`NULL` when not cited). Position 1 = we appear first. -**Code identifiers:** `PGM_Citation_Detector::get_our_position()`, `our_position` column, `PGM_Probe_Result::get_our_position()`. +**Code identifiers:** `PRV_Citation_Detector::get_our_position()`, `our_position` column, `PRV_Probe_Result::get_our_position()`. ### Visibility Score A composite metric summarising peptiderepo.com's AI visibility for a run. @@ -60,24 +60,24 @@ visibility_score = round((base_score + position_bonus) / 2, 4) - **Range:** [0.0, 1.0]. Score 1.0 requires 100% citation rate at position 1 for all probes. - **Why position-weighted?** Being cited at position 1 is more valuable than position 10. The 1/position term gives a graduated bonus: position 1 = 1.0, position 2 = 0.5, position 5 = 0.2, etc. -- **Stored:** Computed from aggregate DB rows per run in `PGM_Ai_Visibility_Collector::compute_score()`, not persisted as a column (recomputed on page load). +- **Stored:** Computed from aggregate DB rows per run in `PRV_Ai_Visibility_Collector::compute_score()`, not persisted as a column (recomputed on page load). -**Code identifiers:** `PGM_Ai_Visibility_Collector::compute_score()`, trendline data in `collect()`. +**Code identifiers:** `PRV_Ai_Visibility_Collector::compute_score()`, trendline data in `collect()`. ### Monthly Budget Cap -The maximum USD spend on LLM API calls in a calendar month. Stored in `pgm_monthly_budget_usd` (default `$5.00`). The runner checks `PGM_Cost_Ledger::can_afford()` **before** each probe and stops gracefully when the cap is reached. +The maximum USD spend on LLM API calls in a calendar month. Stored in `prv_monthly_budget_usd` (default `$5.00`). The runner checks `PRV_Cost_Ledger::can_afford()` **before** each probe and stops gracefully when the cap is reached. -**Code identifiers:** `PGM_Cost_Ledger`, `pgm_monthly_budget_usd` option, `PGM_DEFAULT_MONTHLY_BUDGET_USD` constant. +**Code identifiers:** `PRV_Cost_Ledger`, `prv_monthly_budget_usd` option, `PRV_DEFAULT_MONTHLY_BUDGET_USD` constant. ### Provider -An implementation of `PGM_Probe_Provider` that wraps one LLM backend. v1 ships `PGM_Perplexity_Provider` (sonar) and `PGM_OpenRouter_Provider` (parameterised). +An implementation of `PRV_Probe_Provider` that wraps one LLM backend. v1 ships `PRV_Perplexity_Provider` (sonar) and `PRV_OpenRouter_Provider` (parameterised). -**Code identifiers:** `interface-pgm-probe-provider.php`, `PGM_Perplexity_Provider`, `PGM_OpenRouter_Provider`. +**Code identifiers:** `interface-prv-probe-provider.php`, `PRV_Perplexity_Provider`, `PRV_OpenRouter_Provider`. ### Collector / Panel Seam -The extensibility mechanism: `PGM_Data_Collector` produces data; `PGM_Dashboard_Panel` renders it. Both are registered with `PGM_Collector_Registry`. v1 ships only the AI-visibility pair; future SEO categories add new pairs without touching the shell. +The extensibility mechanism: `PRV_Data_Collector` produces data; `PRV_Dashboard_Panel` renders it. Both are registered with `PRV_Collector_Registry`. v1 ships only the AI-visibility pair; future SEO categories add new pairs without touching the shell. -**Code identifiers:** `interface-pgm-data-collector.php`, `interface-pgm-dashboard-panel.php`, `PGM_Collector_Registry`. +**Code identifiers:** `interface-prv-data-collector.php`, `interface-prv-dashboard-panel.php`, `PRV_Collector_Registry`. ### 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. @@ -85,9 +85,9 @@ Generative Engine Optimization — the practice of improving a brand's visibilit ### Proxy Metric The plugin's scores are API-probe measurements, not real-world consumer LLM usage. The admin page labels these explicitly as "a directional proxy, not the consumer ChatGPT/Gemini apps." -**Code identifiers:** `PGM_Ai_Visibility_Panel::render_proxy_note()`. +**Code identifiers:** `PRV_Ai_Visibility_Panel::render_proxy_note()`. ### MTD Cost Month-to-date spend in USD, summed from the `cost_usd` column for rows in the current calendar month. -**Code identifiers:** `PGM_Cost_Ledger::get_month_to_date_usd()`. +**Code identifiers:** `PRV_Cost_Ledger::get_month_to_date_usd()`. diff --git a/CONVENTIONS.md b/CONVENTIONS.md index c4d6667..0fae72c 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -1,6 +1,6 @@ -# Conventions — Peptide GEO Monitor +# Conventions — PR Vision -**Version:** 0.1.0 | **Last updated:** 2026-06-13 +**Version:** 0.1.1 | **Last updated:** 2026-06-13 Applies to all contributors and AI agents working in this repo. Where this document and `AGENT-OPERATING-STANDARD.md` conflict on process, the Standard wins; this document wins on code style. @@ -16,20 +16,20 @@ Applies to all contributors and AI agents working in this repo. Where this docum | Concept | Convention | Example | |---------|-----------|---------| -| Class | `PGM_` prefix + PascalCase | `PGM_Cost_Ledger` | -| Interface | `PGM_` prefix + PascalCase | `PGM_Probe_Provider` | -| File | `class-pgm-*.php` or `interface-pgm-*.php` | `class-pgm-probe-runner.php` | -| Option | `pgm_` prefix | `pgm_monthly_budget_usd` | -| Hook / cron | `pgm_` prefix | `pgm_weekly_probe` | -| Constant | `PGM_` prefix | `PGM_TARGET_DOMAIN` | -| wp-config key | `PGM_` prefix | `PGM_OPENROUTER_API_KEY` | +| Class | `PRV_` prefix + PascalCase | `PRV_Cost_Ledger` | +| Interface | `PRV_` prefix + PascalCase | `PRV_Probe_Provider` | +| File | `class-prv-*.php` or `interface-prv-*.php` | `class-prv-probe-runner.php` | +| Option | `prv_` prefix | `prv_monthly_budget_usd` | +| Hook / cron | `prv_` prefix | `prv_weekly_probe` | +| Constant | `PRV_` prefix | `PRV_TARGET_DOMAIN` | +| wp-config key | `PRV_` prefix | `PRV_OPENROUTER_API_KEY` | | Test file | `test-*.php` | `test-citation-detector.php` | ## 3. File structure - **One class per file.** Interfaces get their own file. - **300-line limit** (enforced by CI). Split at the method boundary when approaching the limit. -- Autoloader resolves `PGM_Foo_Bar` → `class-pgm-foo-bar.php` via a search across `includes/` subdirectories. +- Autoloader resolves `PRV_Foo_Bar` → `class-prv-foo-bar.php` via a search across `includes/` subdirectories. ## 4. Docblocks @@ -43,7 +43,7 @@ Every class must have a preamble docblock: * * @see * @see - * @package PeptideGeoMonitor + * @package PrVision */ ``` @@ -63,22 +63,22 @@ Every public method must have: ## 6. Tests - Tests live in `tests/unit/test-*.php` and are run by the CI `lint-php` job via `php $test`. -- Each test file `require_once`s `tests/bootstrap.php` and calls `exit( pgm_test_summary() )`. -- Use `pgm_assert()`, `pgm_assert_equals()`, `pgm_assert_throws()`. -- Mock HTTP via `$GLOBALS['pgm_test_state']['remote_posts']` (queued responses). +- Each test file `require_once`s `tests/bootstrap.php` and calls `exit( prv_test_summary() )`. +- Use `prv_assert()`, `prv_assert_equals()`, `prv_assert_throws()`. +- Mock HTTP via `$GLOBALS['prv_test_state']['remote_posts']` (queued responses). - No PHPUnit dependency. ## 7. Adding a new provider -1. Create `includes/providers/class-pgm-{name}-provider.php`. -2. Implement `PGM_Probe_Provider` (`probe()`, `get_name()`, `is_configured()`). -3. Add a branch in `PGM_Probe_Runner::resolve_provider()`. +1. Create `includes/providers/class-prv-{name}-provider.php`. +2. Implement `PRV_Probe_Provider` (`probe()`, `get_name()`, `is_configured()`). +3. Add a branch in `PRV_Probe_Runner::resolve_provider()`. 4. Update `ARCHITECTURE.md` §5 (Providers table). 5. Add a parse test in `tests/unit/test-provider-parsing.php`. ## 8. Adding a new collector/panel pair -1. Implement `PGM_Data_Collector` in `includes/collector/`. -2. Implement `PGM_Dashboard_Panel` in `includes/panel/`. -3. Register both in `PGM_Plugin::init()` via `PGM_Collector_Registry::instance()`. +1. Implement `PRV_Data_Collector` in `includes/collector/`. +2. Implement `PRV_Dashboard_Panel` in `includes/panel/`. +3. Register both in `PRV_Plugin::init()` via `PRV_Collector_Registry::instance()`. 4. Update `ARCHITECTURE.md` §6 (Collector/Panel seam). diff --git a/README.md b/README.md index 6405bb0..6881333 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Peptide GEO Monitor +# PR Vision An internal WordPress plugin that runs weekly server-side LLM probes to measure whether peptiderepo.com is cited by AI assistants across core peptides. @@ -18,11 +18,11 @@ The displayed metric is a **directional proxy** — API probes do not replicate ```php // OpenRouter API key (sk-or-…) — required for all providers. -define( 'PGM_OPENROUTER_API_KEY', 'sk-or-YOUR_KEY_HERE' ); +define( 'PRV_OPENROUTER_API_KEY', 'sk-or-YOUR_KEY_HERE' ); // Cloudflare AI Gateway routing (optional — falls back to direct OpenRouter). -define( 'PGM_CF_ACCOUNT_ID', 'YOUR_CF_ACCOUNT_ID' ); -define( 'PGM_CF_GATEWAY_ID', 'YOUR_GATEWAY_NAME' ); +define( 'PRV_CF_ACCOUNT_ID', 'YOUR_CF_ACCOUNT_ID' ); +define( 'PRV_CF_GATEWAY_ID', 'YOUR_GATEWAY_NAME' ); ``` **Never commit these values. They are read at runtime from wp-config.php only.** @@ -37,7 +37,7 @@ See `ARCHITECTURE.md` for the full file tree, data flow, and design decisions. ## Admin page -Navigate to **GEO Monitor** in the WordPress admin sidebar (`manage_options` required). +Navigate to **PR Vision** in the WordPress admin sidebar (`manage_options` required). The page shows: - A proxy-metric disclaimer note. @@ -50,26 +50,26 @@ The page shows: ## Scheduling -A weekly WP-Cron event (`pgm_weekly_probe`) is registered on plugin activation and cleared on deactivation. Probe runs (cron or manual) respect the hard monthly budget cap. +A weekly WP-Cron event (`prv_weekly_probe`) is registered on plugin activation and cleared on deactivation. Probe runs (cron or manual) respect the hard monthly budget cap. --- ## Cost cap -Default cap: **$5.00 USD/month** (configurable via the `pgm_monthly_budget_usd` option). The runner checks the month-to-date spend **before** each API call and stops gracefully when the cap is reached — partial runs are recorded, never over-spend. +Default cap: **$5.00 USD/month** (configurable via the `prv_monthly_budget_usd` option). The runner checks the month-to-date spend **before** each API call and stops gracefully when the cap is reached — partial runs are recorded, never over-spend. --- ## Uninstall -Uninstalling the plugin drops the `{prefix}pgm_ai_visibility` table and deletes all `pgm_*` options. Deactivation only clears the cron schedule. +Uninstalling the plugin drops the `{prefix}prv_ai_visibility` table and deletes all `prv_*` options. Deactivation only clears the cron schedule. --- ## Developer notes - PHP 8.1+ required. No runtime Composer dependencies. -- All classes prefixed `PGM_`. All options/hooks prefixed `pgm_`. +- All classes prefixed `PRV_`. All options/hooks prefixed `prv_`. - Files are kept under 300 lines; one class per file. - See `CONVENTIONS.md` for naming patterns and extension guides. - See `CONTEXT.md` for the domain glossary including the visibility score formula. diff --git a/composer.json b/composer.json index 36f4752..75ac6b5 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "peptiderepo/peptide-geo-monitor", + "name": "peptiderepo/pr-vision", "description": "AI-visibility (GEO) monitoring dashboard for peptiderepo.com — weekly LLM probes, cost ledger, trendline admin panel.", "type": "wordpress-plugin", "license": "GPL-2.0-or-later", diff --git a/includes/collector/class-pgm-ai-visibility-collector.php b/includes/collector/class-prv-ai-visibility-collector.php similarity index 86% rename from includes/collector/class-pgm-ai-visibility-collector.php rename to includes/collector/class-prv-ai-visibility-collector.php index 1b81d07..d45bfe5 100644 --- a/includes/collector/class-pgm-ai-visibility-collector.php +++ b/includes/collector/class-prv-ai-visibility-collector.php @@ -2,9 +2,9 @@ declare(strict_types=1); /** - * AI-visibility data collector — v1 implementation of PGM_Data_Collector. + * AI-visibility data collector — v1 implementation of PRV_Data_Collector. * - * Reads the pgm_ai_visibility table and returns structured data for: + * Reads the prv_ai_visibility table and returns structured data for: * - Trendline: per-run visibility score (% cited, position-weighted). * - Standings: per-peptide latest cited status, our position, competing domains. * - Run metadata: last run time, MTD cost vs cap. @@ -14,27 +14,27 @@ declare(strict_types=1); * position_bonus = Σ (1 / our_position) for cited probes / total_probes * visibility_score = round((base_score + position_bonus) / 2, 4) * - * Who triggers: PGM_Admin_Page calls the registered collector via the registry. - * Dependencies: $wpdb, PGM_Table_Manager, PGM_Cost_Ledger, PGM_Config. + * Who triggers: PRV_Admin_Page calls the registered collector via the registry. + * Dependencies: $wpdb, PRV_Table_Manager, PRV_Cost_Ledger, PRV_Config. * - * @see interface-pgm-data-collector.php — Interface this implements. - * @see class-pgm-ai-visibility-panel.php — Renders the data returned here. + * @see interface-prv-data-collector.php — Interface this implements. + * @see class-prv-ai-visibility-panel.php — Renders the data returned here. * @see CONTEXT.md — Score formula + glossary. * @see ARCHITECTURE.md — §Storage, §Score. - * @package PeptideGeoMonitor + * @package PrVision */ -class PGM_Ai_Visibility_Collector implements PGM_Data_Collector { +class PRV_Ai_Visibility_Collector implements PRV_Data_Collector { /** - * @var PGM_Cost_Ledger + * @var PRV_Cost_Ledger */ - private PGM_Cost_Ledger $ledger; + private PRV_Cost_Ledger $ledger; /** - * @param PGM_Cost_Ledger|null $ledger Injected for testing. + * @param PRV_Cost_Ledger|null $ledger Injected for testing. */ - public function __construct( ?PGM_Cost_Ledger $ledger = null ) { - $this->ledger = $ledger ?? new PGM_Cost_Ledger(); + public function __construct( ?PRV_Cost_Ledger $ledger = null ) { + $this->ledger = $ledger ?? new PRV_Cost_Ledger(); } /** @@ -51,7 +51,7 @@ class PGM_Ai_Visibility_Collector implements PGM_Data_Collector { public function collect(): array { global $wpdb; - $table = PGM_Table_Manager::get_table_name(); + $table = PRV_Table_Manager::get_table_name(); // ── Trendline: one score per run_id ──────────────────────────── // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching @@ -120,7 +120,7 @@ class PGM_Ai_Visibility_Collector implements PGM_Data_Collector { 'standings' => $standings, 'last_run_at' => $last_run ? (string) $last_run : null, 'mtd_cost_usd' => $this->ledger->get_month_to_date_usd(), - 'monthly_cap_usd' => PGM_Config::get_monthly_budget_usd(), + 'monthly_cap_usd' => PRV_Config::get_monthly_budget_usd(), ); } @@ -175,6 +175,6 @@ class PGM_Ai_Visibility_Collector implements PGM_Data_Collector { $counts = array_count_values( $all ); arsort( $counts ); $top = array_slice( array_keys( $counts ), 0, 5 ); - return array_values( array_filter( $top, fn( $d ) => PGM_TARGET_DOMAIN !== $d ) ); + return array_values( array_filter( $top, fn( $d ) => PRV_TARGET_DOMAIN !== $d ) ); } } diff --git a/includes/collector/interface-pgm-data-collector.php b/includes/collector/interface-prv-data-collector.php similarity index 65% rename from includes/collector/interface-pgm-data-collector.php rename to includes/collector/interface-prv-data-collector.php index 53d15a0..8a2dd33 100644 --- a/includes/collector/interface-pgm-data-collector.php +++ b/includes/collector/interface-prv-data-collector.php @@ -2,22 +2,22 @@ declare(strict_types=1); /** - * Seam: contract for all GEO-monitor data collectors. + * Seam: contract for all PR Vision data collectors. * - * v1 implements only PGM_Ai_Visibility_Collector. Future collectors + * v1 implements only PRV_Ai_Visibility_Collector. Future collectors * (keyword rankings, schema coverage, technical-SEO) implement this - * interface and register themselves with PGM_Collector_Registry without + * interface and register themselves with PRV_Collector_Registry without * changing the dashboard shell. * - * Who triggers: PGM_Collector_Registry dispatches registered collectors. + * Who triggers: PRV_Collector_Registry dispatches registered collectors. * Dependencies: None — pure interface. * - * @see class-pgm-ai-visibility-collector.php — v1 implementation. - * @see class-pgm-collector-registry.php — Registration hub. + * @see class-prv-ai-visibility-collector.php — v1 implementation. + * @see class-prv-collector-registry.php — Registration hub. * @see ARCHITECTURE.md — §Collector/Panel seam. - * @package PeptideGeoMonitor + * @package PrVision */ -interface PGM_Data_Collector { +interface PRV_Data_Collector { /** * Collect and return the data payload for this category. diff --git a/includes/core/class-pgm-activator.php b/includes/core/class-prv-activator.php similarity index 54% rename from includes/core/class-pgm-activator.php rename to includes/core/class-prv-activator.php index c9138ab..2e59e4e 100644 --- a/includes/core/class-pgm-activator.php +++ b/includes/core/class-prv-activator.php @@ -8,14 +8,14 @@ declare(strict_types=1); * WP-Cron probe schedule. Safe to call on re-activation — dbDelta is * idempotent and the cron schedule is only added when absent. * - * Who triggers: register_activation_hook() in peptide-geo-monitor.php. - * Dependencies: PGM_Table_Manager, PGM_Cron, PGM_Config. + * Who triggers: register_activation_hook() in pr-vision.php. + * Dependencies: PRV_Table_Manager, PRV_Cron, PRV_Config. * - * @see class-pgm-table-manager.php — Database schema creation. - * @see class-pgm-cron.php — Cron scheduling. - * @package PeptideGeoMonitor + * @see class-prv-table-manager.php — Database schema creation. + * @see class-prv-cron.php — Cron scheduling. + * @package PrVision */ -class PGM_Activator { +class PRV_Activator { /** * Run all activation tasks. @@ -25,8 +25,8 @@ class PGM_Activator { * @return void */ public static function activate(): void { - PGM_Table_Manager::create_table(); - PGM_Config::seed_defaults(); - PGM_Cron::schedule_weekly(); + PRV_Table_Manager::create_table(); + PRV_Config::seed_defaults(); + PRV_Cron::schedule_weekly(); } } diff --git a/includes/core/class-pgm-admin-page.php b/includes/core/class-prv-admin-page.php similarity index 59% rename from includes/core/class-pgm-admin-page.php rename to includes/core/class-prv-admin-page.php index 8e17b80..0a3b08e 100644 --- a/includes/core/class-pgm-admin-page.php +++ b/includes/core/class-prv-admin-page.php @@ -2,37 +2,37 @@ declare(strict_types=1); /** - * GEO Monitor top-level admin page. + * PR Vision top-level admin page. * - * Registers the "GEO Monitor" menu item (manage_options), enqueues Chart.js, + * Registers the "PR Vision" menu item (manage_options), enqueues Chart.js, * handles the "Run now" POST action (nonce + capability), and delegates - * rendering to the registered panels via PGM_Collector_Registry. + * rendering to the registered panels via PRV_Collector_Registry. * - * Who triggers: PGM_Plugin::init() → is_admin() guard. - * Dependencies: PGM_Collector_Registry, PGM_Probe_Runner. + * Who triggers: PRV_Plugin::init() → is_admin() guard. + * Dependencies: PRV_Collector_Registry, PRV_Probe_Runner. * - * @see class-pgm-plugin.php — Calls register_hooks(). - * @see class-pgm-collector-registry.php — Provides panels. - * @see class-pgm-probe-runner.php — Invoked by the Run now action. + * @see class-prv-plugin.php — Calls register_hooks(). + * @see class-prv-collector-registry.php — Provides panels. + * @see class-prv-probe-runner.php — Invoked by the Run now action. * @see ARCHITECTURE.md — §Admin dashboard. - * @package PeptideGeoMonitor + * @package PrVision */ -class PGM_Admin_Page { +class PRV_Admin_Page { /** * Admin menu slug. */ - const MENU_SLUG = 'pgm-geo-monitor'; + const MENU_SLUG = 'pr-vision'; /** * Nonce action for the Run now form. */ - const NONCE_ACTION = 'pgm_run_now'; + const NONCE_ACTION = 'prv_run_now'; /** * Nonce field name. */ - const NONCE_FIELD = 'pgm_nonce'; + const NONCE_FIELD = 'prv_nonce'; /** * Register WordPress admin hooks. @@ -43,12 +43,12 @@ class PGM_Admin_Page { */ public function register_hooks(): void { add_action( 'admin_menu', array( $this, 'add_menu_page' ) ); - add_action( 'admin_post_pgm_run_now', array( $this, 'handle_run_now' ) ); + add_action( 'admin_post_prv_run_now', array( $this, 'handle_run_now' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); } /** - * Add the "GEO Monitor" top-level menu page. + * Add the "PR Vision" top-level menu page. * * Side effects: Adds a WP admin menu entry. * @@ -56,8 +56,8 @@ class PGM_Admin_Page { */ public function add_menu_page(): void { add_menu_page( - __( 'GEO Monitor', 'peptide-geo-monitor' ), - __( 'GEO Monitor', 'peptide-geo-monitor' ), + __( 'PR Vision', 'pr-vision' ), + __( 'PR Vision', 'pr-vision' ), 'manage_options', self::MENU_SLUG, array( $this, 'render_page' ), @@ -67,7 +67,7 @@ class PGM_Admin_Page { } /** - * Enqueue Chart.js on the GEO Monitor page only. + * Enqueue Chart.js on the PR Vision page only. * * @param string $hook Current admin page hook. * @@ -79,7 +79,7 @@ class PGM_Admin_Page { } wp_enqueue_script( - 'pgm-chartjs', + 'prv-chartjs', 'https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js', array(), '4.4.3', @@ -88,20 +88,20 @@ class PGM_Admin_Page { wp_add_inline_style( 'wp-admin', - '.pgm-meta-bar{display:flex;gap:20px;margin:10px 0 15px;flex-wrap:wrap;} - .pgm-meta-item{background:#f6f7f7;padding:6px 12px;border-radius:4px;font-size:13px;} - .pgm-card{background:#fff;border:1px solid #c3c4c7;padding:16px 20px;margin-bottom:20px;border-radius:4px;} - .pgm-card h2{margin-top:0;} - .pgm-proxy-note{margin:10px 0;} - .pgm-standings th,.pgm-standings td{vertical-align:middle;} - canvas#pgm-trendline-chart{max-height:300px;}' + '.prv-meta-bar{display:flex;gap:20px;margin:10px 0 15px;flex-wrap:wrap;} + .prv-meta-item{background:#f6f7f7;padding:6px 12px;border-radius:4px;font-size:13px;} + .prv-card{background:#fff;border:1px solid #c3c4c7;padding:16px 20px;margin-bottom:20px;border-radius:4px;} + .prv-card h2{margin-top:0;} + .prv-proxy-note{margin:10px 0;} + .prv-standings th,.prv-standings td{vertical-align:middle;} + canvas#prv-trendline-chart{max-height:300px;}' ); } /** * Handle the "Run now" POST action. * - * Validates nonce and capability before dispatching to PGM_Probe_Runner. + * Validates nonce and capability before dispatching to PRV_Probe_Runner. * Redirects back to the admin page with a status query arg. * * Side effects: Triggers a full probe run; redirects. @@ -110,23 +110,23 @@ class PGM_Admin_Page { */ public function handle_run_now(): void { if ( ! current_user_can( 'manage_options' ) ) { - wp_die( esc_html__( 'Insufficient permissions.', 'peptide-geo-monitor' ), 403 ); + wp_die( esc_html__( 'Insufficient permissions.', 'pr-vision' ), 403 ); } $nonce = isset( $_POST[ self::NONCE_FIELD ] ) ? sanitize_text_field( wp_unslash( $_POST[ self::NONCE_FIELD ] ) ) : ''; if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) { - wp_die( esc_html__( 'Security check failed.', 'peptide-geo-monitor' ), 403 ); + wp_die( esc_html__( 'Security check failed.', 'pr-vision' ), 403 ); } - $runner = new PGM_Probe_Runner(); + $runner = new PRV_Probe_Runner(); $counts = $runner->run(); $redirect = add_query_arg( array( 'page' => self::MENU_SLUG, - 'pgm_run_done' => 1, - 'pgm_probed' => (int) $counts['probed'], - 'pgm_skipped' => (int) ( $counts['skipped_budget'] + $counts['skipped_error'] ), + 'prv_run_done' => 1, + 'prv_probed' => (int) $counts['probed'], + 'prv_skipped' => (int) ( $counts['skipped_budget'] + $counts['skipped_error'] ), ), admin_url( 'admin.php' ) ); @@ -136,7 +136,7 @@ class PGM_Admin_Page { } /** - * Render the full GEO Monitor admin page. + * Render the full PR Vision admin page. * * Side effects: Outputs HTML directly. * @@ -144,22 +144,22 @@ class PGM_Admin_Page { */ public function render_page(): void { if ( ! current_user_can( 'manage_options' ) ) { - wp_die( esc_html__( 'Insufficient permissions.', 'peptide-geo-monitor' ) ); + wp_die( esc_html__( 'Insufficient permissions.', 'pr-vision' ) ); } echo '
'; - echo '

' . esc_html__( 'GEO Monitor — AI Visibility', 'peptide-geo-monitor' ) . '

'; + echo '

' . esc_html__( 'PR Vision — AI Visibility', 'pr-vision' ) . '

'; $this->render_run_done_notice(); // "Run now" button form. echo '
'; - echo ''; + echo ''; wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD ); - submit_button( __( 'Run now', 'peptide-geo-monitor' ), 'secondary', 'pgm_run_now_btn', false ); + submit_button( __( 'Run now', 'pr-vision' ), 'secondary', 'prv_run_now_btn', false ); echo '
'; - $registry = PGM_Collector_Registry::instance(); + $registry = PRV_Collector_Registry::instance(); foreach ( $registry->get_collectors() as $key => $collector ) { $panel = $registry->get_panel( $key ); @@ -180,19 +180,19 @@ class PGM_Admin_Page { */ private function render_run_done_notice(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( empty( $_GET['pgm_run_done'] ) ) { + if ( empty( $_GET['prv_run_done'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $probed = (int) ( $_GET['pgm_probed'] ?? 0 ); + $probed = (int) ( $_GET['prv_probed'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $skipped = (int) ( $_GET['pgm_skipped'] ?? 0 ); + $skipped = (int) ( $_GET['prv_skipped'] ?? 0 ); echo '

'; echo esc_html( sprintf( /* translators: 1: probed count, 2: skipped count */ - __( 'Run complete: %1$d probes collected, %2$d skipped (budget or error).', 'peptide-geo-monitor' ), + __( 'Run complete: %1$d probes collected, %2$d skipped (budget or error).', 'pr-vision' ), $probed, $skipped ) diff --git a/includes/core/class-pgm-autoloader.php b/includes/core/class-prv-autoloader.php similarity index 59% rename from includes/core/class-pgm-autoloader.php rename to includes/core/class-prv-autoloader.php index f33991d..b48b1ce 100644 --- a/includes/core/class-pgm-autoloader.php +++ b/includes/core/class-prv-autoloader.php @@ -2,20 +2,20 @@ declare(strict_types=1); /** - * Class and interface autoloader for the Peptide GEO Monitor plugin. + * Class and interface autoloader for the PR Vision plugin. * - * Converts PGM_* class/interface names to file paths under includes/. + * Converts PRV_* class/interface names to file paths under includes/. * Naming conventions: - * PGM_Foo_Bar (class) → class-pgm-foo-bar.php - * PGM_Foo_Bar (interface) → interface-pgm-foo-bar.php (tried second) + * PRV_Foo_Bar (class) → class-prv-foo-bar.php + * PRV_Foo_Bar (interface) → interface-prv-foo-bar.php (tried second) * - * Who triggers: Plugin boot (peptide-geo-monitor.php) — called once before - * any PGM_* class or interface is instantiated. + * Who triggers: Plugin boot (pr-vision.php) — called once before + * any PRV_* class or interface is instantiated. * Dependencies: None (pure PHP). * - * @package PeptideGeoMonitor + * @package PrVision */ -class PGM_Autoloader { +class PRV_Autoloader { /** * Register the autoloader with the SPL stack. @@ -27,7 +27,7 @@ class PGM_Autoloader { } /** - * Attempt to load a PGM_* class or interface file. + * Attempt to load a PRV_* class or interface file. * * Walks the known include subdirectories in order, trying the class- * filename first and the interface-filename second. @@ -39,18 +39,18 @@ class PGM_Autoloader { * @return void */ public static function load( string $class_name ): void { - if ( 0 !== strpos( $class_name, 'PGM_' ) ) { + if ( 0 !== strpos( $class_name, 'PRV_' ) ) { return; } $slug = strtolower( str_replace( '_', '-', $class_name ) ); $search_dirs = array( - PGM_PLUGIN_DIR . 'includes/core/', - PGM_PLUGIN_DIR . 'includes/providers/', - PGM_PLUGIN_DIR . 'includes/collector/', - PGM_PLUGIN_DIR . 'includes/panel/', - PGM_PLUGIN_DIR . 'includes/', + PRV_PLUGIN_DIR . 'includes/core/', + PRV_PLUGIN_DIR . 'includes/providers/', + PRV_PLUGIN_DIR . 'includes/collector/', + PRV_PLUGIN_DIR . 'includes/panel/', + PRV_PLUGIN_DIR . 'includes/', ); // Try class file first, then interface file. diff --git a/includes/core/class-pgm-collector-registry.php b/includes/core/class-prv-collector-registry.php similarity index 63% rename from includes/core/class-pgm-collector-registry.php rename to includes/core/class-prv-collector-registry.php index 47ec851..be03074 100644 --- a/includes/core/class-pgm-collector-registry.php +++ b/includes/core/class-prv-collector-registry.php @@ -4,39 +4,39 @@ declare(strict_types=1); /** * Registry: holds all registered collectors and panels. * - * PGM_Plugin::init() registers the v1 AI-visibility collector + panel. + * PRV_Plugin::init() registers the v1 AI-visibility collector + panel. * Future collectors/panels call register_collector()/register_panel() * without touching the dashboard shell. * - * Who triggers: PGM_Plugin::init() on plugins_loaded. - * Dependencies: PGM_Data_Collector, PGM_Dashboard_Panel interfaces. + * Who triggers: PRV_Plugin::init() on plugins_loaded. + * Dependencies: PRV_Data_Collector, PRV_Dashboard_Panel interfaces. * - * @see interface-pgm-data-collector.php — Collector contract. - * @see interface-pgm-dashboard-panel.php — Panel contract. - * @see class-pgm-plugin.php — Calls register_* on boot. + * @see interface-prv-data-collector.php — Collector contract. + * @see interface-prv-dashboard-panel.php — Panel contract. + * @see class-prv-plugin.php — Calls register_* on boot. * @see ARCHITECTURE.md — §Collector/Panel seam. - * @package PeptideGeoMonitor + * @package PrVision */ -class PGM_Collector_Registry { +class PRV_Collector_Registry { /** * Singleton instance. * - * @var PGM_Collector_Registry|null + * @var PRV_Collector_Registry|null */ - private static ?PGM_Collector_Registry $instance = null; + private static ?PRV_Collector_Registry $instance = null; /** * Registered collectors keyed by their collector key. * - * @var array + * @var array */ private array $collectors = array(); /** * Registered panels keyed by their panel key (matches collector key). * - * @var array + * @var array */ private array $panels = array(); @@ -60,11 +60,11 @@ class PGM_Collector_Registry { /** * Register a data collector. * - * @param PGM_Data_Collector $collector + * @param PRV_Data_Collector $collector * * @return void */ - public function register_collector( PGM_Data_Collector $collector ): void { + public function register_collector( PRV_Data_Collector $collector ): void { $this->collectors[ $collector->get_key() ] = $collector; } @@ -72,18 +72,18 @@ class PGM_Collector_Registry { * Register a dashboard panel. * * @param string $key Panel key (must match a collector key). - * @param PGM_Dashboard_Panel $panel + * @param PRV_Dashboard_Panel $panel * * @return void */ - public function register_panel( string $key, PGM_Dashboard_Panel $panel ): void { + public function register_panel( string $key, PRV_Dashboard_Panel $panel ): void { $this->panels[ $key ] = $panel; } /** * Get all registered collectors. * - * @return array + * @return array */ public function get_collectors(): array { return $this->collectors; @@ -94,9 +94,9 @@ class PGM_Collector_Registry { * * @param string $key Panel key. * - * @return PGM_Dashboard_Panel|null + * @return PRV_Dashboard_Panel|null */ - public function get_panel( string $key ): ?PGM_Dashboard_Panel { + public function get_panel( string $key ): ?PRV_Dashboard_Panel { return $this->panels[ $key ] ?? null; } diff --git a/includes/core/class-pgm-config.php b/includes/core/class-prv-config.php similarity index 82% rename from includes/core/class-pgm-config.php rename to includes/core/class-prv-config.php index e1e5c43..fbdb5f9 100644 --- a/includes/core/class-pgm-config.php +++ b/includes/core/class-prv-config.php @@ -4,19 +4,19 @@ declare(strict_types=1); /** * Configuration helper: seed defaults and retrieve plugin settings. * - * All mutable settings live in wp_options under the pgm_ prefix. This class + * All mutable settings live in wp_options under the prv_ prefix. This class * provides typed getters (no magic values in call sites) and seeds the initial * defaults on first activation. * - * Who triggers: PGM_Activator::activate() for seeding; every component that + * Who triggers: PRV_Activator::activate() for seeding; every component that * needs a setting reads through here. * Dependencies: get_option(), update_option(). * * @see CONTEXT.md — Domain glossary: peptides, prompt intents, models. * @see ARCHITECTURE.md — §Config (options + defaults). - * @package PeptideGeoMonitor + * @package PrVision */ -class PGM_Config { +class PRV_Config { /** * Seed the default option values on first activation. @@ -28,10 +28,10 @@ class PGM_Config { * @return void */ public static function seed_defaults(): void { - add_option( 'pgm_monthly_budget_usd', PGM_DEFAULT_MONTHLY_BUDGET_USD ); - add_option( 'pgm_peptides', self::default_peptides() ); - add_option( 'pgm_prompt_intents', self::default_prompt_intents() ); - add_option( 'pgm_models', self::default_models() ); + add_option( 'prv_monthly_budget_usd', PRV_DEFAULT_MONTHLY_BUDGET_USD ); + add_option( 'prv_peptides', self::default_peptides() ); + add_option( 'prv_prompt_intents', self::default_prompt_intents() ); + add_option( 'prv_models', self::default_models() ); } /** @@ -40,8 +40,8 @@ class PGM_Config { * @return float Positive float; falls back to the plugin default. */ public static function get_monthly_budget_usd(): float { - $val = (float) get_option( 'pgm_monthly_budget_usd', PGM_DEFAULT_MONTHLY_BUDGET_USD ); - return $val > 0.0 ? $val : PGM_DEFAULT_MONTHLY_BUDGET_USD; + $val = (float) get_option( 'prv_monthly_budget_usd', PRV_DEFAULT_MONTHLY_BUDGET_USD ); + return $val > 0.0 ? $val : PRV_DEFAULT_MONTHLY_BUDGET_USD; } /** @@ -52,7 +52,7 @@ class PGM_Config { * @return array */ public static function get_peptides(): array { - $raw = get_option( 'pgm_peptides', self::default_peptides() ); + $raw = get_option( 'prv_peptides', self::default_peptides() ); return is_array( $raw ) ? $raw : self::default_peptides(); } @@ -64,7 +64,7 @@ class PGM_Config { * @return array */ public static function get_prompt_intents(): array { - $raw = get_option( 'pgm_prompt_intents', self::default_prompt_intents() ); + $raw = get_option( 'prv_prompt_intents', self::default_prompt_intents() ); return is_array( $raw ) ? $raw : self::default_prompt_intents(); } @@ -74,7 +74,7 @@ class PGM_Config { * @return array */ public static function get_models(): array { - $raw = get_option( 'pgm_models', self::default_models() ); + $raw = get_option( 'prv_models', self::default_models() ); return is_array( $raw ) ? $raw : self::default_models(); } diff --git a/includes/core/class-pgm-cost-ledger.php b/includes/core/class-prv-cost-ledger.php similarity index 80% rename from includes/core/class-pgm-cost-ledger.php rename to includes/core/class-prv-cost-ledger.php index 7aef316..8165bfa 100644 --- a/includes/core/class-pgm-cost-ledger.php +++ b/includes/core/class-prv-cost-ledger.php @@ -9,19 +9,19 @@ declare(strict_types=1); * throwing — a partial run that respects the budget is acceptable; an * over-spend is not. * - * Costs are stored in the pgm_ai_visibility table (cost_usd column per row) + * Costs are stored in the prv_ai_visibility table (cost_usd column per row) * and summarized by summing the current calendar month's rows. No separate * ledger table is needed; the visibility table IS the ledger. * - * Who triggers: PGM_Probe_Runner before and after each API call. - * Dependencies: $wpdb, PGM_Table_Manager. + * Who triggers: PRV_Probe_Runner before and after each API call. + * Dependencies: $wpdb, PRV_Table_Manager. * - * @see class-pgm-probe-runner.php — Calls can_afford() and record_call(). - * @see class-pgm-table-manager.php — Table where cost_usd is stored. + * @see class-prv-probe-runner.php — Calls can_afford() and record_call(). + * @see class-prv-table-manager.php — Table where cost_usd is stored. * @see CONTEXT.md — "monthly cap" definition. - * @package PeptideGeoMonitor + * @package PrVision */ -class PGM_Cost_Ledger { +class PRV_Cost_Ledger { /** * Retrieve the total probe cost incurred in the current calendar month (UTC). @@ -33,7 +33,7 @@ class PGM_Cost_Ledger { public function get_month_to_date_usd(): float { global $wpdb; - $table = PGM_Table_Manager::get_table_name(); + $table = PRV_Table_Manager::get_table_name(); $month = gmdate( 'Y-m' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching @@ -60,7 +60,7 @@ class PGM_Cost_Ledger { * @return bool True when the call can proceed within budget. */ public function can_afford( float $estimated_cost_usd ): bool { - $cap = PGM_Config::get_monthly_budget_usd(); + $cap = PRV_Config::get_monthly_budget_usd(); $spent = $this->get_month_to_date_usd(); return ( $spent + $estimated_cost_usd ) <= $cap; } @@ -71,7 +71,7 @@ class PGM_Cost_Ledger { * @return float Remaining USD; 0.0 when the cap is already hit. */ public function get_remaining_budget_usd(): float { - $cap = PGM_Config::get_monthly_budget_usd(); + $cap = PRV_Config::get_monthly_budget_usd(); $spent = $this->get_month_to_date_usd(); return max( 0.0, $cap - $spent ); } @@ -79,13 +79,13 @@ class PGM_Cost_Ledger { /** * Record a completed API call's cost against a specific row ID. * - * Updates the cost_usd column on the row created by PGM_Probe_Runner. + * Updates the cost_usd column on the row created by PRV_Probe_Runner. * The run is already written to the table before the cost is settled so * even partial runs (budget abort) have their completed rows costed. * * Side effects: Database write. * - * @param int $row_id Primary key of the pgm_ai_visibility row. + * @param int $row_id Primary key of the prv_ai_visibility row. * @param float $cost_usd Actual cost in USD. * * @return bool True on success. @@ -93,7 +93,7 @@ class PGM_Cost_Ledger { public function update_row_cost( int $row_id, float $cost_usd ): bool { global $wpdb; - $table = PGM_Table_Manager::get_table_name(); + $table = PRV_Table_Manager::get_table_name(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $result = $wpdb->update( diff --git a/includes/core/class-pgm-cron.php b/includes/core/class-prv-cron.php similarity index 54% rename from includes/core/class-pgm-cron.php rename to includes/core/class-prv-cron.php index 4c33e98..9816ea5 100644 --- a/includes/core/class-pgm-cron.php +++ b/includes/core/class-prv-cron.php @@ -4,39 +4,39 @@ declare(strict_types=1); /** * WP-Cron management for the weekly AI-visibility probe. * - * Registers the pgm_weekly_probe hook, schedules/clears the event, and - * dispatches to PGM_Probe_Runner on each tick. + * Registers the prv_weekly_probe hook, schedules/clears the event, and + * dispatches to PRV_Probe_Runner on each tick. * - * Who triggers: PGM_Plugin::init() (hook registration), PGM_Activator - * (schedule), PGM_Deactivator (clear). - * Dependencies: PGM_Probe_Runner. + * Who triggers: PRV_Plugin::init() (hook registration), PRV_Activator + * (schedule), PRV_Deactivator (clear). + * Dependencies: PRV_Probe_Runner. * - * @see class-pgm-probe-runner.php — Runner invoked on each cron tick. - * @see class-pgm-activator.php — Calls schedule_weekly() on activation. - * @package PeptideGeoMonitor + * @see class-prv-probe-runner.php — Runner invoked on each cron tick. + * @see class-prv-activator.php — Calls schedule_weekly() on activation. + * @package PrVision */ -class PGM_Cron { +class PRV_Cron { /** * Register WordPress action hooks. * - * Side effects: Adds action for PGM_CRON_HOOK. + * Side effects: Adds action for PRV_CRON_HOOK. * * @return void */ public function register_hooks(): void { - add_action( PGM_CRON_HOOK, array( $this, 'handle_cron_tick' ) ); + add_action( PRV_CRON_HOOK, array( $this, 'handle_cron_tick' ) ); } /** * Callback fired by WP-Cron on the weekly schedule. * - * Side effects: Instantiates and runs PGM_Probe_Runner. + * Side effects: Instantiates and runs PRV_Probe_Runner. * * @return void */ public function handle_cron_tick(): void { - $runner = new PGM_Probe_Runner(); + $runner = new PRV_Probe_Runner(); $runner->run(); } @@ -50,8 +50,8 @@ class PGM_Cron { * @return void */ public static function schedule_weekly(): void { - if ( ! wp_next_scheduled( PGM_CRON_HOOK ) ) { - wp_schedule_event( time(), 'weekly', PGM_CRON_HOOK ); + if ( ! wp_next_scheduled( PRV_CRON_HOOK ) ) { + wp_schedule_event( time(), 'weekly', PRV_CRON_HOOK ); } } @@ -65,9 +65,9 @@ class PGM_Cron { * @return void */ public static function clear_schedule(): void { - $timestamp = wp_next_scheduled( PGM_CRON_HOOK ); + $timestamp = wp_next_scheduled( PRV_CRON_HOOK ); if ( false !== $timestamp ) { - wp_unschedule_event( $timestamp, PGM_CRON_HOOK ); + wp_unschedule_event( $timestamp, PRV_CRON_HOOK ); } } @@ -77,6 +77,6 @@ class PGM_Cron { * @return bool */ public static function is_scheduled(): bool { - return false !== wp_next_scheduled( PGM_CRON_HOOK ); + return false !== wp_next_scheduled( PRV_CRON_HOOK ); } } diff --git a/includes/core/class-pgm-deactivator.php b/includes/core/class-prv-deactivator.php similarity index 57% rename from includes/core/class-pgm-deactivator.php rename to includes/core/class-prv-deactivator.php index 807d511..548340f 100644 --- a/includes/core/class-pgm-deactivator.php +++ b/includes/core/class-prv-deactivator.php @@ -7,23 +7,23 @@ declare(strict_types=1); * Clears the scheduled WP-Cron event so probes stop running when the plugin * is deactivated. Does NOT purge data — that is uninstall's job. * - * Who triggers: register_deactivation_hook() in peptide-geo-monitor.php. - * Dependencies: PGM_Cron. + * Who triggers: register_deactivation_hook() in pr-vision.php. + * Dependencies: PRV_Cron. * - * @see class-pgm-cron.php — Cron registration + clearing. + * @see class-prv-cron.php — Cron registration + clearing. * @see uninstall.php — Full data purge on uninstall. - * @package PeptideGeoMonitor + * @package PrVision */ -class PGM_Deactivator { +class PRV_Deactivator { /** * Clear the weekly cron schedule. * - * Side effects: Removes the pgm_weekly_probe event from WP-Cron. + * Side effects: Removes the prv_weekly_probe event from WP-Cron. * * @return void */ public static function deactivate(): void { - PGM_Cron::clear_schedule(); + PRV_Cron::clear_schedule(); } } diff --git a/includes/core/class-pgm-plugin.php b/includes/core/class-prv-plugin.php similarity index 57% rename from includes/core/class-pgm-plugin.php rename to includes/core/class-prv-plugin.php index 28d7348..eb2a619 100644 --- a/includes/core/class-pgm-plugin.php +++ b/includes/core/class-prv-plugin.php @@ -2,19 +2,19 @@ declare(strict_types=1); /** - * Main orchestrator for the Peptide GEO Monitor plugin. + * Main orchestrator for the PR Vision plugin. * * Boots sub-systems in the correct order, hooks into WP, and keeps * the top-level file under the 300-line limit by delegating all logic * to specialist classes. * - * Who triggers: plugins_loaded action (peptide-geo-monitor.php). - * Dependencies: PGM_Cron, PGM_Admin_Page, PGM_Collector_Registry. + * Who triggers: plugins_loaded action (pr-vision.php). + * Dependencies: PRV_Cron, PRV_Admin_Page, PRV_Collector_Registry. * * @see ARCHITECTURE.md — Boot sequence diagram. - * @package PeptideGeoMonitor + * @package PrVision */ -class PGM_Plugin { +class PRV_Plugin { /** * Wire up all WordPress hooks. @@ -25,17 +25,17 @@ class PGM_Plugin { * @return void */ public function init(): void { - $cron = new PGM_Cron(); + $cron = new PRV_Cron(); $cron->register_hooks(); if ( is_admin() ) { - $page = new PGM_Admin_Page(); + $page = new PRV_Admin_Page(); $page->register_hooks(); } // Register the v1 AI-visibility collector + panel. - $registry = PGM_Collector_Registry::instance(); - $registry->register_collector( new PGM_Ai_Visibility_Collector() ); - $registry->register_panel( 'ai_visibility', new PGM_Ai_Visibility_Panel() ); + $registry = PRV_Collector_Registry::instance(); + $registry->register_collector( new PRV_Ai_Visibility_Collector() ); + $registry->register_panel( 'ai_visibility', new PRV_Ai_Visibility_Panel() ); } } diff --git a/includes/core/class-pgm-probe-result.php b/includes/core/class-prv-probe-result.php similarity index 83% rename from includes/core/class-pgm-probe-result.php rename to includes/core/class-prv-probe-result.php index f62e6e7..2833849 100644 --- a/includes/core/class-pgm-probe-result.php +++ b/includes/core/class-prv-probe-result.php @@ -4,18 +4,18 @@ declare(strict_types=1); /** * Value object: the result of a single LLM probe call. * - * Returned by every PGM_Probe_Provider implementation. Immutable — all + * Returned by every PRV_Probe_Provider implementation. Immutable — all * fields are set in the constructor and read via typed getters. * - * Who triggers: PGM_Perplexity_Provider and PGM_OpenRouter_Provider. + * Who triggers: PRV_Perplexity_Provider and PRV_OpenRouter_Provider. * Dependencies: None. * - * @see interface-pgm-probe-provider.php — Provider interface that returns this. - * @see class-pgm-probe-runner.php — Consumes the result for storage. + * @see interface-prv-probe-provider.php — Provider interface that returns this. + * @see class-prv-probe-runner.php — Consumes the result for storage. * @see CONTEXT.md — "probe result", "cited", "our_position". - * @package PeptideGeoMonitor + * @package PrVision */ -class PGM_Probe_Result { +class PRV_Probe_Result { /** * @param string $raw_excerpt First ~500 chars of the LLM's response. diff --git a/includes/core/class-pgm-probe-runner.php b/includes/core/class-prv-probe-runner.php similarity index 64% rename from includes/core/class-pgm-probe-runner.php rename to includes/core/class-prv-probe-runner.php index bb5958b..ef8a464 100644 --- a/includes/core/class-pgm-probe-runner.php +++ b/includes/core/class-prv-probe-runner.php @@ -6,40 +6,32 @@ declare(strict_types=1); * * For each combination the runner: * 1. Checks the monthly budget cap (aborts gracefully when hit). - * 2. Caches responses for identical (peptide, intent, model) within the run. - * 3. Dispatches to the correct provider (resolved from PGM_Config::get_models()). - * 4. Writes a row to the pgm_ai_visibility table. - * 5. Settles the actual cost against the ledger row. + * 2. Dispatches to the correct provider (resolved from PRV_Config::get_models()). + * 3. Writes a row to the prv_ai_visibility table. + * 4. Settles the actual cost against the ledger row. * - * Who triggers: PGM_Cron::handle_cron_tick() and PGM_Admin_Page (Run now action). - * Dependencies: PGM_Config, PGM_Cost_Ledger, PGM_Table_Manager, all providers. + * Who triggers: PRV_Cron::handle_cron_tick() and PRV_Admin_Page (Run now action). + * Dependencies: PRV_Config, PRV_Cost_Ledger, PRV_Table_Manager, all providers. * - * @see class-pgm-cron.php — Calls run() on the weekly schedule. - * @see class-pgm-cost-ledger.php — Budget cap enforcement. - * @see class-pgm-perplexity-provider.php - * @see class-pgm-openrouter-provider.php + * @see class-prv-cron.php — Calls run() on the weekly schedule. + * @see class-prv-cost-ledger.php — Budget cap enforcement. + * @see class-prv-perplexity-provider.php + * @see class-prv-openrouter-provider.php * @see ARCHITECTURE.md — §Probe run flow. - * @package PeptideGeoMonitor + * @package PrVision */ -class PGM_Probe_Runner { +class PRV_Probe_Runner { /** - * @var PGM_Cost_Ledger + * @var PRV_Cost_Ledger */ - private PGM_Cost_Ledger $ledger; + private PRV_Cost_Ledger $ledger; /** - * In-run response cache: key = "{peptide_slug}|{intent}|{model}" → PGM_Probe_Result. - * - * @var array + * @param PRV_Cost_Ledger|null $ledger Injected for testing; auto-created otherwise. */ - private array $cache = array(); - - /** - * @param PGM_Cost_Ledger|null $ledger Injected for testing; auto-created otherwise. - */ - public function __construct( ?PGM_Cost_Ledger $ledger = null ) { - $this->ledger = $ledger ?? new PGM_Cost_Ledger(); + public function __construct( ?PRV_Cost_Ledger $ledger = null ) { + $this->ledger = $ledger ?? new PRV_Cost_Ledger(); } /** @@ -56,9 +48,9 @@ class PGM_Probe_Runner { */ public function run(): array { $run_id = $this->generate_run_id(); - $peptides = PGM_Config::get_peptides(); - $intents = PGM_Config::get_prompt_intents(); - $models = PGM_Config::get_models(); + $peptides = PRV_Config::get_peptides(); + $intents = PRV_Config::get_prompt_intents(); + $models = PRV_Config::get_models(); $counts = array( 'probed' => 0, @@ -85,19 +77,12 @@ class PGM_Probe_Runner { continue; } - // In-run cache check. - $cache_key = $peptide['slug'] . '|' . $intent_tpl . '|' . $model; - if ( isset( $this->cache[ $cache_key ] ) ) { - $result = $this->cache[ $cache_key ]; - } else { - try { - $result = $provider->probe( $query ); - $this->cache[ $cache_key ] = $result; - } catch ( \Exception $e ) { - error_log( sprintf( 'PGM: probe failed [%s / %s]: %s', $model, $peptide['slug'], $e->getMessage() ) ); - $counts['skipped_error']++; - continue; - } + try { + $result = $provider->probe( $query ); + } catch ( \Exception $e ) { + error_log( sprintf( 'PRV: probe failed [%s / %s]: %s', $model, $peptide['slug'], $e->getMessage() ) ); + $counts['skipped_error']++; + continue; } $row_id = $this->persist_result( $run_id, $peptide, $model, $intent_tpl, $result ); @@ -110,7 +95,6 @@ class PGM_Probe_Runner { } } - $this->cache = array(); // Reset cache after run. return $counts; } @@ -121,14 +105,14 @@ class PGM_Probe_Runner { * @param array{slug:string, label:string} $peptide Peptide config. * @param string $model Model identifier. * @param string $intent_tpl Raw intent template string. - * @param PGM_Probe_Result $result Probe result. + * @param PRV_Probe_Result $result Probe result. * * @return int Inserted row ID, or 0 on failure. */ - private function persist_result( string $run_id, array $peptide, string $model, string $intent_tpl, PGM_Probe_Result $result ): int { + private function persist_result( string $run_id, array $peptide, string $model, string $intent_tpl, PRV_Probe_Result $result ): int { global $wpdb; - $table = PGM_Table_Manager::get_table_name(); + $table = PRV_Table_Manager::get_table_name(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert( @@ -157,14 +141,14 @@ class PGM_Probe_Runner { * * @param string $model OpenRouter model identifier. * - * @return PGM_Probe_Provider|null Null when the model is unrecognised. + * @return PRV_Probe_Provider|null Null when the model is unrecognised. */ - private function resolve_provider( string $model ): ?PGM_Probe_Provider { + private function resolve_provider( string $model ): ?PRV_Probe_Provider { if ( 'perplexity/sonar' === $model ) { - return new PGM_Perplexity_Provider(); + return new PRV_Perplexity_Provider(); } // All other models route through the generic OpenRouter provider. - return new PGM_OpenRouter_Provider( $model ); + return new PRV_OpenRouter_Provider( $model ); } /** @@ -176,9 +160,9 @@ class PGM_Probe_Runner { */ private function get_estimated_cost( string $model ): float { if ( 'perplexity/sonar' === $model ) { - return PGM_Perplexity_Provider::ESTIMATED_COST_PER_PROBE; + return PRV_Perplexity_Provider::ESTIMATED_COST_PER_PROBE; } - return PGM_OpenRouter_Provider::ESTIMATED_COST_PER_PROBE; + return PRV_OpenRouter_Provider::ESTIMATED_COST_PER_PROBE; } /** diff --git a/includes/core/class-pgm-table-manager.php b/includes/core/class-prv-table-manager.php similarity index 81% rename from includes/core/class-pgm-table-manager.php rename to includes/core/class-prv-table-manager.php index c97b571..020f52f 100644 --- a/includes/core/class-pgm-table-manager.php +++ b/includes/core/class-prv-table-manager.php @@ -2,25 +2,25 @@ declare(strict_types=1); /** - * Database table creation and schema versioning for PGM. + * Database table creation and schema versioning for PR Vision. * - * Creates {prefix}pgm_ai_visibility via dbDelta (idempotent). Tracks the - * schema version in the pgm_schema_version option so future migrations can + * Creates {prefix}prv_ai_visibility via dbDelta (idempotent). Tracks the + * schema version in the prv_schema_version option so future migrations can * detect when an upgrade is needed without destructive ALTER TABLE. * - * Who triggers: PGM_Activator::activate() on plugin activation. + * Who triggers: PRV_Activator::activate() on plugin activation. * Dependencies: $wpdb global, dbDelta() (requires upgrade.php). * - * @see class-pgm-activator.php — Calls create_table() on activation. + * @see class-prv-activator.php — Calls create_table() on activation. * @see ARCHITECTURE.md — §Storage. - * @package PeptideGeoMonitor + * @package PrVision */ -class PGM_Table_Manager { +class PRV_Table_Manager { /** * Name of the custom table (without $wpdb->prefix). */ - const TABLE_BASE = 'pgm_ai_visibility'; + const TABLE_BASE = 'prv_ai_visibility'; /** * Create or upgrade the AI-visibility table. @@ -28,7 +28,7 @@ class PGM_Table_Manager { * Uses dbDelta to create or non-destructively alter the table. * Records the schema version in an option afterwards. * - * Side effects: Writes to the database; sets pgm_schema_version option. + * Side effects: Writes to the database; sets prv_schema_version option. * * @return void */ @@ -63,7 +63,7 @@ class PGM_Table_Manager { dbDelta( $sql ); - update_option( 'pgm_schema_version', PGM_SCHEMA_VERSION ); + update_option( 'prv_schema_version', PRV_SCHEMA_VERSION ); } /** diff --git a/includes/panel/class-pgm-ai-visibility-panel.php b/includes/panel/class-prv-ai-visibility-panel.php similarity index 63% rename from includes/panel/class-pgm-ai-visibility-panel.php rename to includes/panel/class-prv-ai-visibility-panel.php index 75f53ca..dce78ec 100644 --- a/includes/panel/class-pgm-ai-visibility-panel.php +++ b/includes/panel/class-prv-ai-visibility-panel.php @@ -2,7 +2,7 @@ declare(strict_types=1); /** - * AI-visibility dashboard panel — renders the GEO Monitor admin page. + * AI-visibility dashboard panel — renders the PR Vision admin page. * * Renders four sections: * 1. Proxy-metric disclaimer note. @@ -10,31 +10,31 @@ declare(strict_types=1); * 3. Per-peptide standings table (cited, position, top competitor domains). * 4. MTD cost vs cap + last-run time. * - * All output is escaped. Chart.js is enqueued via PGM_Admin_Page. + * All output is escaped. Chart.js is enqueued via PRV_Admin_Page. * - * Who triggers: PGM_Admin_Page::render_page() via PGM_Collector_Registry. - * Dependencies: PGM_Ai_Visibility_Collector (data format contract). + * Who triggers: PRV_Admin_Page::render_page() via PRV_Collector_Registry. + * Dependencies: PRV_Ai_Visibility_Collector (data format contract). * - * @see interface-pgm-dashboard-panel.php — Interface this implements. - * @see class-pgm-ai-visibility-collector.php — Produces the $data array. - * @see class-pgm-admin-page.php — Enqueues Chart.js; calls render(). - * @package PeptideGeoMonitor + * @see interface-prv-dashboard-panel.php — Interface this implements. + * @see class-prv-ai-visibility-collector.php — Produces the $data array. + * @see class-prv-admin-page.php — Enqueues Chart.js; calls render(). + * @package PrVision */ -class PGM_Ai_Visibility_Panel implements PGM_Dashboard_Panel { +class PRV_Ai_Visibility_Panel implements PRV_Dashboard_Panel { /** * {@inheritDoc} * - * @param array $data Payload from PGM_Ai_Visibility_Collector. + * @param array $data Payload from PRV_Ai_Visibility_Collector. * * @return void */ public function render( array $data ): void { $trendline = is_array( $data['trendline'] ?? null ) ? $data['trendline'] : array(); $standings = is_array( $data['standings'] ?? null ) ? $data['standings'] : array(); - $last_run_at = isset( $data['last_run_at'] ) && $data['last_run_at'] ? esc_html( (string) $data['last_run_at'] ) : esc_html__( 'Never', 'peptide-geo-monitor' ); + $last_run_at = isset( $data['last_run_at'] ) && $data['last_run_at'] ? esc_html( (string) $data['last_run_at'] ) : esc_html__( 'Never', 'pr-vision' ); $mtd_cost = isset( $data['mtd_cost_usd'] ) ? (float) $data['mtd_cost_usd'] : 0.0; - $cap = isset( $data['monthly_cap_usd'] ) ? (float) $data['monthly_cap_usd'] : PGM_DEFAULT_MONTHLY_BUDGET_USD; + $cap = isset( $data['monthly_cap_usd'] ) ? (float) $data['monthly_cap_usd'] : PRV_DEFAULT_MONTHLY_BUDGET_USD; $this->render_proxy_note(); $this->render_meta_bar( $last_run_at, $mtd_cost, $cap ); @@ -46,7 +46,7 @@ class PGM_Ai_Visibility_Panel implements PGM_Dashboard_Panel { * {@inheritDoc} */ public function get_title(): string { - return __( 'AI Visibility', 'peptide-geo-monitor' ); + return __( 'AI Visibility', 'pr-vision' ); } /** @@ -55,9 +55,9 @@ class PGM_Ai_Visibility_Panel implements PGM_Dashboard_Panel { * @return void */ private function render_proxy_note(): void { - echo '

'; - echo '' . esc_html__( 'Note:', 'peptide-geo-monitor' ) . ' '; - echo esc_html__( 'Scores are measured via API probes — a directional proxy, not the consumer ChatGPT/Gemini apps (different retrieval, personalisation, and system prompts).', 'peptide-geo-monitor' ); + echo '

'; + echo '' . esc_html__( 'Note:', 'pr-vision' ) . ' '; + echo esc_html__( 'Scores are measured via API probes — a directional proxy, not the consumer ChatGPT/Gemini apps (different retrieval, personalisation, and system prompts).', 'pr-vision' ); echo '

'; } @@ -72,9 +72,9 @@ class PGM_Ai_Visibility_Panel implements PGM_Dashboard_Panel { */ private function render_meta_bar( string $last_run_at, float $mtd_cost, float $cap ): void { $pct = $cap > 0 ? min( 100.0, round( $mtd_cost / $cap * 100, 1 ) ) : 0; - echo '
'; - echo '' . esc_html__( 'Last run:', 'peptide-geo-monitor' ) . ' ' . esc_html( $last_run_at ) . ''; - echo '' . esc_html__( 'MTD cost:', 'peptide-geo-monitor' ) . ' $' . esc_html( number_format( $mtd_cost, 4 ) ) . ' / $' . esc_html( number_format( $cap, 2 ) ) . ' (' . esc_html( $pct ) . '% of cap)'; + echo '
'; + echo '' . esc_html__( 'Last run:', 'pr-vision' ) . ' ' . esc_html( $last_run_at ) . ''; + echo '' . esc_html__( 'MTD cost:', 'pr-vision' ) . ' $' . esc_html( number_format( $mtd_cost, 4 ) ) . ' / $' . esc_html( number_format( $cap, 2 ) ) . ' (' . esc_html( $pct ) . '% of cap)'; echo '
'; } @@ -86,10 +86,10 @@ class PGM_Ai_Visibility_Panel implements PGM_Dashboard_Panel { * @return void */ private function render_trendline( array $trendline ): void { - echo '

' . esc_html__( 'Visibility Score — Trend', 'peptide-geo-monitor' ) . '

'; + echo '

' . esc_html__( 'Visibility Score — Trend', 'pr-vision' ) . '

'; if ( empty( $trendline ) ) { - echo '

' . esc_html__( 'No probe runs recorded yet. Click "Run now" to collect the first data point.', 'peptide-geo-monitor' ) . '

'; + echo '

' . esc_html__( 'No probe runs recorded yet. Click "Run now" to collect the first data point.', 'pr-vision' ) . '

'; return; } @@ -103,9 +103,9 @@ class PGM_Ai_Visibility_Panel implements PGM_Dashboard_Panel { $labels_json = wp_json_encode( $labels ); $scores_json = wp_json_encode( $scores ); - echo ''; + echo ''; echo ''; echo '
'; @@ -119,19 +119,19 @@ class PGM_Ai_Visibility_Panel implements PGM_Dashboard_Panel { * @return void */ private function render_standings( array $standings ): void { - echo '

' . esc_html__( 'Current Standings', 'peptide-geo-monitor' ) . '

'; + echo '

' . esc_html__( 'Current Standings', 'pr-vision' ) . '

'; if ( empty( $standings ) ) { - echo '

' . esc_html__( 'No standings data yet.', 'peptide-geo-monitor' ) . '

'; + echo '

' . esc_html__( 'No standings data yet.', 'pr-vision' ) . '

'; return; } - echo ''; + echo '
'; echo ''; - echo ''; - echo ''; - echo ''; - echo ''; + echo ''; + echo ''; + echo ''; + echo ''; echo ''; foreach ( $standings as $row ) { diff --git a/includes/panel/interface-pgm-dashboard-panel.php b/includes/panel/interface-prv-dashboard-panel.php similarity index 55% rename from includes/panel/interface-pgm-dashboard-panel.php rename to includes/panel/interface-prv-dashboard-panel.php index e7aedcc..4c06320 100644 --- a/includes/panel/interface-pgm-dashboard-panel.php +++ b/includes/panel/interface-prv-dashboard-panel.php @@ -2,21 +2,21 @@ declare(strict_types=1); /** - * Seam: contract for all GEO-monitor dashboard panels. + * Seam: contract for all PR Vision dashboard panels. * - * Each panel renders one data category within the GEO Monitor admin page. - * v1 ships only PGM_Ai_Visibility_Panel. Future panels (keyword rankings, - * schema coverage…) implement this and register with PGM_Collector_Registry. + * Each panel renders one data category within the PR Vision admin page. + * v1 ships only PRV_Ai_Visibility_Panel. Future panels (keyword rankings, + * schema coverage…) implement this and register with PRV_Collector_Registry. * - * Who triggers: PGM_Admin_Page when building the GEO Monitor page. + * Who triggers: PRV_Admin_Page when building the PR Vision page. * Dependencies: None — pure interface. * - * @see class-pgm-ai-visibility-panel.php — v1 implementation. - * @see class-pgm-collector-registry.php — Registration hub. + * @see class-prv-ai-visibility-panel.php — v1 implementation. + * @see class-prv-collector-registry.php — Registration hub. * @see ARCHITECTURE.md — §Collector/Panel seam. - * @package PeptideGeoMonitor + * @package PrVision */ -interface PGM_Dashboard_Panel { +interface PRV_Dashboard_Panel { /** * Render the panel HTML into the current output buffer. diff --git a/includes/providers/class-pgm-citation-detector.php b/includes/providers/class-prv-citation-detector.php similarity index 78% rename from includes/providers/class-pgm-citation-detector.php rename to includes/providers/class-prv-citation-detector.php index 126e6c0..e7bd09d 100644 --- a/includes/providers/class-pgm-citation-detector.php +++ b/includes/providers/class-prv-citation-detector.php @@ -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; } diff --git a/includes/providers/class-pgm-gateway-client.php b/includes/providers/class-prv-gateway-client.php similarity index 78% rename from includes/providers/class-pgm-gateway-client.php rename to includes/providers/class-prv-gateway-client.php index dea8b19..9504523 100644 --- a/includes/providers/class-pgm-gateway-client.php +++ b/includes/providers/class-prv-gateway-client.php @@ -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 ); } diff --git a/includes/providers/class-pgm-openrouter-provider.php b/includes/providers/class-prv-openrouter-provider.php similarity index 69% rename from includes/providers/class-pgm-openrouter-provider.php rename to includes/providers/class-prv-openrouter-provider.php index 9babbb4..f7fc425 100644 --- a/includes/providers/class-pgm-openrouter-provider.php +++ b/includes/providers/class-prv-openrouter-provider.php @@ -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 $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; } } diff --git a/includes/providers/class-pgm-perplexity-provider.php b/includes/providers/class-prv-perplexity-provider.php similarity index 67% rename from includes/providers/class-pgm-perplexity-provider.php rename to includes/providers/class-prv-perplexity-provider.php index 90c6f97..9ac205d 100644 --- a/includes/providers/class-pgm-perplexity-provider.php +++ b/includes/providers/class-prv-perplexity-provider.php @@ -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 $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; } } diff --git a/includes/providers/interface-pgm-probe-provider.php b/includes/providers/interface-prv-probe-provider.php similarity index 75% rename from includes/providers/interface-pgm-probe-provider.php rename to includes/providers/interface-prv-probe-provider.php index cefb7db..dd6c24d 100644 --- a/includes/providers/interface-pgm-probe-provider.php +++ b/includes/providers/interface-prv-probe-provider.php @@ -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"). diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 0c7ebb5..3e713d9 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -1,6 +1,6 @@ - - WordPress coding standards for Peptide GEO Monitor. + + WordPress coding standards for PR Vision. . diff --git a/peptide-geo-monitor.php b/pr-vision.php similarity index 69% rename from peptide-geo-monitor.php rename to pr-vision.php index 4604fe3..0cd35d4 100644 --- a/peptide-geo-monitor.php +++ b/pr-vision.php @@ -1,13 +1,13 @@ init(); } ); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 7d3269e..fc638c7 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -5,7 +5,7 @@ * * Pattern mirrors peptide-repo-core's bootstrap (flat PHP, no PHPUnit). * - * @package PeptideGeoMonitor + * @package PrVision */ declare(strict_types=1); @@ -16,21 +16,21 @@ if ( ! defined( 'ABSPATH' ) ) { define( 'ABSPATH', __DIR__ . '/' ); } -define( 'PGM_VERSION', '0.1.0' ); -define( 'PGM_PLUGIN_FILE', __DIR__ . '/../peptide-geo-monitor.php' ); -define( 'PGM_PLUGIN_DIR', realpath( __DIR__ . '/..' ) . '/' ); -define( 'PGM_PLUGIN_URL', 'http://example.test/wp-content/plugins/peptide-geo-monitor/' ); -define( 'PGM_SCHEMA_VERSION', 1 ); -define( 'PGM_MAX_RETRIES', 3 ); -define( 'PGM_API_TIMEOUT_SECONDS', 60 ); -define( 'PGM_RETRY_BASE_DELAY_SECONDS', 2 ); -define( 'PGM_DEFAULT_MONTHLY_BUDGET_USD', 5.0 ); -define( 'PGM_CRON_HOOK', 'pgm_weekly_probe' ); -define( 'PGM_TARGET_DOMAIN', 'peptiderepo.com' ); +define( 'PRV_VERSION', '0.1.1' ); +define( 'PRV_PLUGIN_FILE', __DIR__ . '/../pr-vision.php' ); +define( 'PRV_PLUGIN_DIR', realpath( __DIR__ . '/..' ) . '/' ); +define( 'PRV_PLUGIN_URL', 'http://example.test/wp-content/plugins/pr-vision/' ); +define( 'PRV_SCHEMA_VERSION', 1 ); +define( 'PRV_MAX_RETRIES', 3 ); +define( 'PRV_API_TIMEOUT_SECONDS', 60 ); +define( 'PRV_RETRY_BASE_DELAY_SECONDS', 2 ); +define( 'PRV_DEFAULT_MONTHLY_BUDGET_USD', 5.0 ); +define( 'PRV_CRON_HOOK', 'prv_weekly_probe' ); +define( 'PRV_TARGET_DOMAIN', 'peptiderepo.com' ); /* ── Global test state ─────────────────────────────────────────────────── */ -$GLOBALS['pgm_test_state'] = [ +$GLOBALS['prv_test_state'] = [ 'options' => [], 'actions' => [], 'wpdb_insert_id' => 1, @@ -40,8 +40,8 @@ $GLOBALS['pgm_test_state'] = [ 'remote_posts' => [], ]; -function pgm_test_reset(): void { - $GLOBALS['pgm_test_state'] = [ +function prv_test_reset(): void { + $GLOBALS['prv_test_state'] = [ 'options' => [], 'actions' => [], 'wpdb_insert_id' => 1, @@ -50,29 +50,29 @@ function pgm_test_reset(): void { 'cron_events' => [], 'remote_posts' => [], ]; - PGM_Collector_Registry::reset_for_testing(); + PRV_Collector_Registry::reset_for_testing(); } /* ── WordPress function stubs ──────────────────────────────────────────── */ function get_option( string $name, $default = false ) { - return $GLOBALS['pgm_test_state']['options'][ $name ] ?? $default; + return $GLOBALS['prv_test_state']['options'][ $name ] ?? $default; } function update_option( string $name, $value ): bool { - $GLOBALS['pgm_test_state']['options'][ $name ] = $value; + $GLOBALS['prv_test_state']['options'][ $name ] = $value; return true; } function add_option( string $name, $value = '' ): bool { - if ( ! isset( $GLOBALS['pgm_test_state']['options'][ $name ] ) ) { - $GLOBALS['pgm_test_state']['options'][ $name ] = $value; + if ( ! isset( $GLOBALS['prv_test_state']['options'][ $name ] ) ) { + $GLOBALS['prv_test_state']['options'][ $name ] = $value; } return true; } function add_action( string $hook, $callback, int $priority = 10, int $accepted_args = 1 ): bool { - $GLOBALS['pgm_test_state']['actions'][] = compact( 'hook', 'callback', 'priority' ); + $GLOBALS['prv_test_state']['actions'][] = compact( 'hook', 'callback', 'priority' ); return true; } @@ -83,19 +83,19 @@ function remove_action( string $hook, $callback, int $priority = 10 ): bool { function add_menu_page( ...$args ): void {} function wp_schedule_event( int $timestamp, string $recurrence, string $hook ): void { - $GLOBALS['pgm_test_state']['cron_events'][ $hook ] = $timestamp; + $GLOBALS['prv_test_state']['cron_events'][ $hook ] = $timestamp; } function wp_next_scheduled( string $hook ) { - return $GLOBALS['pgm_test_state']['cron_events'][ $hook ] ?? false; + return $GLOBALS['prv_test_state']['cron_events'][ $hook ] ?? false; } function wp_unschedule_event( int $timestamp, string $hook ): void { - unset( $GLOBALS['pgm_test_state']['cron_events'][ $hook ] ); + unset( $GLOBALS['prv_test_state']['cron_events'][ $hook ] ); } function wp_clear_scheduled_hook( string $hook ): void { - unset( $GLOBALS['pgm_test_state']['cron_events'][ $hook ] ); + unset( $GLOBALS['prv_test_state']['cron_events'][ $hook ] ); } function wp_rand( int $min = 0, int $max = 0 ): int { @@ -180,7 +180,7 @@ function is_wp_error( $thing ): bool { function wp_remote_post( string $url, array $args = array() ) { // Return from test queue if set, else a generic success. - $queue = &$GLOBALS['pgm_test_state']['remote_posts']; + $queue = &$GLOBALS['prv_test_state']['remote_posts']; if ( ! empty( $queue ) ) { return array_shift( $queue ); } @@ -220,17 +220,17 @@ class stdClass_wpdb { } public function query( string $sql ): bool { return true; } public function insert( string $table, array $data, array $format = [] ): int { - $this->insert_id = $GLOBALS['pgm_test_state']['wpdb_insert_id']++; + $this->insert_id = $GLOBALS['prv_test_state']['wpdb_insert_id']++; return 1; } public function update( string $table, array $data, array $where, array $data_format = [], array $where_format = [] ): int { return 1; } public function get_results( string $query, string $output = 'OBJECT' ): array { - return is_array( $GLOBALS['pgm_test_state']['wpdb_results'] ) ? $GLOBALS['pgm_test_state']['wpdb_results'] : []; + return is_array( $GLOBALS['prv_test_state']['wpdb_results'] ) ? $GLOBALS['prv_test_state']['wpdb_results'] : []; } public function get_var( string $query ): mixed { - return $GLOBALS['pgm_test_state']['wpdb_var']; + return $GLOBALS['prv_test_state']['wpdb_var']; } } @@ -240,35 +240,35 @@ function dbDelta( string $sql ): array { return []; } /* ── Test assertion helpers ────────────────────────────────────────────── */ -$GLOBALS['pgm_test_report'] = [ 'pass' => 0, 'fail' => 0, 'failures' => [] ]; +$GLOBALS['prv_test_report'] = [ 'pass' => 0, 'fail' => 0, 'failures' => [] ]; -function pgm_assert( bool $condition, string $label ): void { +function prv_assert( bool $condition, string $label ): void { if ( $condition ) { - $GLOBALS['pgm_test_report']['pass']++; + $GLOBALS['prv_test_report']['pass']++; echo " PASS: {$label}\n"; } else { - $GLOBALS['pgm_test_report']['fail']++; - $GLOBALS['pgm_test_report']['failures'][] = $label; + $GLOBALS['prv_test_report']['fail']++; + $GLOBALS['prv_test_report']['failures'][] = $label; echo " FAIL: {$label}\n"; } } -function pgm_assert_equals( $expected, $actual, string $label ): void { +function prv_assert_equals( $expected, $actual, string $label ): void { $pass = ( $expected === $actual ); - pgm_assert( $pass, $label . ( $pass ? '' : ' — expected ' . var_export( $expected, true ) . ', got ' . var_export( $actual, true ) ) ); + prv_assert( $pass, $label . ( $pass ? '' : ' — expected ' . var_export( $expected, true ) . ', got ' . var_export( $actual, true ) ) ); } -function pgm_assert_throws( callable $fn, string $exception_class, string $label ): void { +function prv_assert_throws( callable $fn, string $exception_class, string $label ): void { try { $fn(); - pgm_assert( false, $label . ' — expected ' . $exception_class . ' but no exception thrown' ); + prv_assert( false, $label . ' — expected ' . $exception_class . ' but no exception thrown' ); } catch ( \Throwable $e ) { - pgm_assert( $e instanceof $exception_class, $label . ' — got ' . get_class( $e ) ); + prv_assert( $e instanceof $exception_class, $label . ' — got ' . get_class( $e ) ); } } -function pgm_test_summary(): int { - $r = $GLOBALS['pgm_test_report']; +function prv_test_summary(): int { + $r = $GLOBALS['prv_test_report']; echo "\n---\n"; echo "Totals: {$r['pass']} passed, {$r['fail']} failed\n"; if ( $r['fail'] > 0 ) { @@ -283,8 +283,8 @@ function pgm_test_summary(): int { /* ── Load autoloader + all plugin classes ──────────────────────────────── */ -require_once __DIR__ . '/../includes/core/class-pgm-autoloader.php'; -PGM_Autoloader::register(); -require_once __DIR__ . '/../includes/providers/interface-pgm-probe-provider.php'; -require_once __DIR__ . '/../includes/collector/interface-pgm-data-collector.php'; -require_once __DIR__ . '/../includes/panel/interface-pgm-dashboard-panel.php'; +require_once __DIR__ . '/../includes/core/class-prv-autoloader.php'; +PRV_Autoloader::register(); +require_once __DIR__ . '/../includes/providers/interface-prv-probe-provider.php'; +require_once __DIR__ . '/../includes/collector/interface-prv-data-collector.php'; +require_once __DIR__ . '/../includes/panel/interface-prv-dashboard-panel.php'; diff --git a/tests/unit/test-citation-detector.php b/tests/unit/test-citation-detector.php index ce5d8f1..64c7421 100644 --- a/tests/unit/test-citation-detector.php +++ b/tests/unit/test-citation-detector.php @@ -1,74 +1,74 @@ parse_domains( [ 'https://peptiderepo.com/bpc-157/', 'https://www.examine.com/supplements/bpc-157/', 'https://pubmed.ncbi.nlm.nih.gov/12345', ] ); -pgm_assert( in_array( 'peptiderepo.com', $domains, true ), 'parse_domains: peptiderepo.com extracted from URL' ); -pgm_assert( in_array( 'examine.com', $domains, true ), 'parse_domains: www. stripped from examine.com' ); -pgm_assert( in_array( 'pubmed.ncbi.nlm.nih.gov', $domains, true ), 'parse_domains: full subdomain preserved when not www.' ); -pgm_assert_equals( 3, count( $domains ), 'parse_domains: exactly 3 unique domains' ); +prv_assert( in_array( 'peptiderepo.com', $domains, true ), 'parse_domains: peptiderepo.com extracted from URL' ); +prv_assert( in_array( 'examine.com', $domains, true ), 'parse_domains: www. stripped from examine.com' ); +prv_assert( in_array( 'pubmed.ncbi.nlm.nih.gov', $domains, true ), 'parse_domains: full subdomain preserved when not www.' ); +prv_assert_equals( 3, count( $domains ), 'parse_domains: exactly 3 unique domains' ); // ── Test: parse object-style citations (Perplexity format) ─────────────── -pgm_test_reset(); +prv_test_reset(); $domains = $detector->parse_domains( [ [ 'url' => 'https://peptiderepo.com/tb-500/' ], [ 'url' => 'https://examine.com/tb500' ], [ 'url' => 'https://peptiderepo.com/bpc-157/' ], // duplicate domain ] ); -pgm_assert_equals( 2, count( $domains ), 'parse_domains: object-style citations, duplicates removed' ); -pgm_assert( in_array( 'peptiderepo.com', $domains, true ), 'parse_domains: object-style, peptiderepo.com present' ); +prv_assert_equals( 2, count( $domains ), 'parse_domains: object-style citations, duplicates removed' ); +prv_assert( in_array( 'peptiderepo.com', $domains, true ), 'parse_domains: object-style, peptiderepo.com present' ); // ── Test: parse empty input ────────────────────────────────────────────── -pgm_test_reset(); +prv_test_reset(); $domains = $detector->parse_domains( [] ); -pgm_assert_equals( [], $domains, 'parse_domains: empty input returns empty array' ); +prv_assert_equals( [], $domains, 'parse_domains: empty input returns empty array' ); // ── Test: is_cited detection ───────────────────────────────────────────── -pgm_test_reset(); -pgm_assert( $detector->is_cited( ['peptiderepo.com', 'examine.com'] ), 'is_cited: returns true when target present' ); -pgm_assert( ! $detector->is_cited( ['examine.com', 'pubmed.ncbi.nlm.nih.gov'] ), 'is_cited: returns false when target absent' ); -pgm_assert( ! $detector->is_cited( [] ), 'is_cited: returns false for empty list' ); +prv_test_reset(); +prv_assert( $detector->is_cited( ['peptiderepo.com', 'examine.com'] ), 'is_cited: returns true when target present' ); +prv_assert( ! $detector->is_cited( ['examine.com', 'pubmed.ncbi.nlm.nih.gov'] ), 'is_cited: returns false when target absent' ); +prv_assert( ! $detector->is_cited( [] ), 'is_cited: returns false for empty list' ); // ── Test: get_our_position ─────────────────────────────────────────────── -pgm_test_reset(); +prv_test_reset(); $domains = ['examine.com', 'peptiderepo.com', 'pubmed.ncbi.nlm.nih.gov']; -pgm_assert_equals( 2, $detector->get_our_position( $domains ), 'get_our_position: 1-based, position 2' ); +prv_assert_equals( 2, $detector->get_our_position( $domains ), 'get_our_position: 1-based, position 2' ); $domains_first = ['peptiderepo.com', 'examine.com']; -pgm_assert_equals( 1, $detector->get_our_position( $domains_first ), 'get_our_position: position 1 when first' ); +prv_assert_equals( 1, $detector->get_our_position( $domains_first ), 'get_our_position: position 1 when first' ); $domains_absent = ['examine.com', 'pubmed.ncbi.nlm.nih.gov']; -pgm_assert_equals( null, $detector->get_our_position( $domains_absent ), 'get_our_position: null when not present' ); +prv_assert_equals( null, $detector->get_our_position( $domains_absent ), 'get_our_position: null when not present' ); // ── Test: malformed / unexpected input ─────────────────────────────────── -pgm_test_reset(); +prv_test_reset(); $domains = $detector->parse_domains( [ 'not-a-url', 42, @@ -76,6 +76,21 @@ $domains = $detector->parse_domains( [ ['no_url_key' => 'foo'], ['url' => ''], ] ); -pgm_assert_equals( [], $domains, 'parse_domains: gracefully skips malformed items' ); +prv_assert_equals( [], $domains, 'parse_domains: gracefully skips malformed items' ); -exit( pgm_test_summary() ); + +// ── Test: P2-A — www-strip must not corrupt non-www domains starting with 'w' ── + +prv_test_reset(); +$domains = $detector->parse_domains( [ + 'https://wikipedia.org/wiki/bpc-157', + 'https://www.examine.com/bpc-157/', + 'https://webmd.com/drug/bpc-157', +] ); + +prv_assert( in_array( 'wikipedia.org', $domains, true ), 'P2-A: wikipedia.org survives intact (no char-mask corruption)' ); +prv_assert( in_array( 'examine.com', $domains, true ), 'P2-A: www.examine.com correctly stripped to examine.com' ); +prv_assert( in_array( 'webmd.com', $domains, true ), 'P2-A: webmd.com survives intact (no char-mask corruption)' ); +prv_assert_equals( 3, count( $domains ), 'P2-A: exactly 3 unique domains from 3 distinct URLs' ); + +exit( prv_test_summary() ); diff --git a/tests/unit/test-cost-ledger.php b/tests/unit/test-cost-ledger.php index 6a9db4f..1506eaa 100644 --- a/tests/unit/test-cost-ledger.php +++ b/tests/unit/test-cost-ledger.php @@ -1,85 +1,85 @@ can_afford( 1.0 ), 'can_afford: 2.50 spent + 1.00 = 3.50 < 5.00 → true' ); +$ledger = new PRV_Cost_Ledger(); +prv_assert( $ledger->can_afford( 1.0 ), 'can_afford: 2.50 spent + 1.00 = 3.50 < 5.00 → true' ); // ── Test: can_afford when at cap ───────────────────────────────────────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['options']['pgm_monthly_budget_usd'] = 5.0; -$GLOBALS['pgm_test_state']['wpdb_var'] = '5.00'; +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 5.0; +$GLOBALS['prv_test_state']['wpdb_var'] = '5.00'; -$ledger = new PGM_Cost_Ledger(); -pgm_assert( ! $ledger->can_afford( 0.01 ), 'can_afford: 5.00 spent + 0.01 > 5.00 → false' ); +$ledger = new PRV_Cost_Ledger(); +prv_assert( ! $ledger->can_afford( 0.01 ), 'can_afford: 5.00 spent + 0.01 > 5.00 → false' ); // ── Test: can_afford when exactly at cap ───────────────────────────────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['options']['pgm_monthly_budget_usd'] = 5.0; -$GLOBALS['pgm_test_state']['wpdb_var'] = '4.99'; +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 5.0; +$GLOBALS['prv_test_state']['wpdb_var'] = '4.99'; -$ledger = new PGM_Cost_Ledger(); -pgm_assert( $ledger->can_afford( 0.01 ), 'can_afford: 4.99 + 0.01 = 5.00 exactly → true (at cap is allowed)' ); +$ledger = new PRV_Cost_Ledger(); +prv_assert( $ledger->can_afford( 0.01 ), 'can_afford: 4.99 + 0.01 = 5.00 exactly → true (at cap is allowed)' ); // ── Test: get_remaining_budget_usd ────────────────────────────────────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['options']['pgm_monthly_budget_usd'] = 5.0; -$GLOBALS['pgm_test_state']['wpdb_var'] = '3.25'; +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 5.0; +$GLOBALS['prv_test_state']['wpdb_var'] = '3.25'; -$ledger = new PGM_Cost_Ledger(); -pgm_assert_equals( 1.75, round( $ledger->get_remaining_budget_usd(), 2 ), 'get_remaining_budget_usd: 5.00 - 3.25 = 1.75' ); +$ledger = new PRV_Cost_Ledger(); +prv_assert_equals( 1.75, round( $ledger->get_remaining_budget_usd(), 2 ), 'get_remaining_budget_usd: 5.00 - 3.25 = 1.75' ); // ── Test: get_remaining_budget_usd never negative ──────────────────────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['options']['pgm_monthly_budget_usd'] = 5.0; -$GLOBALS['pgm_test_state']['wpdb_var'] = '10.00'; // over cap +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 5.0; +$GLOBALS['prv_test_state']['wpdb_var'] = '10.00'; // over cap -$ledger = new PGM_Cost_Ledger(); -pgm_assert_equals( 0.0, $ledger->get_remaining_budget_usd(), 'get_remaining_budget_usd: clamps to 0.0 when over cap' ); +$ledger = new PRV_Cost_Ledger(); +prv_assert_equals( 0.0, $ledger->get_remaining_budget_usd(), 'get_remaining_budget_usd: clamps to 0.0 when over cap' ); // ── Test: get_month_to_date_usd with null DB result (no rows yet) ──────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['options']['pgm_monthly_budget_usd'] = 5.0; -$GLOBALS['pgm_test_state']['wpdb_var'] = null; +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 5.0; +$GLOBALS['prv_test_state']['wpdb_var'] = null; -$ledger = new PGM_Cost_Ledger(); -pgm_assert_equals( 0.0, $ledger->get_month_to_date_usd(), 'get_month_to_date_usd: null DB result → 0.0' ); +$ledger = new PRV_Cost_Ledger(); +prv_assert_equals( 0.0, $ledger->get_month_to_date_usd(), 'get_month_to_date_usd: null DB result → 0.0' ); // ── Test: update_row_cost returns true ────────────────────────────────── -pgm_test_reset(); -$ledger = new PGM_Cost_Ledger(); -pgm_assert( $ledger->update_row_cost( 42, 0.00123456 ), 'update_row_cost: returns true on success' ); +prv_test_reset(); +$ledger = new PRV_Cost_Ledger(); +prv_assert( $ledger->update_row_cost( 42, 0.00123456 ), 'update_row_cost: returns true on success' ); // ── Test: budget abort mid-run (runner stops when cap hit) ────────────── -pgm_test_reset(); +prv_test_reset(); // Simulate a nearly-exhausted budget. -$GLOBALS['pgm_test_state']['options']['pgm_monthly_budget_usd'] = 0.001; // $0.001 cap -$GLOBALS['pgm_test_state']['wpdb_var'] = '0.001'; // fully spent +$GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 0.001; // $0.001 cap +$GLOBALS['prv_test_state']['wpdb_var'] = '0.001'; // fully spent -$ledger2 = new PGM_Cost_Ledger(); -pgm_assert( ! $ledger2->can_afford( 0.000001 ), 'budget abort: can_afford returns false at cap → runner would stop' ); +$ledger2 = new PRV_Cost_Ledger(); +prv_assert( ! $ledger2->can_afford( 0.000001 ), 'budget abort: can_afford returns false at cap → runner would stop' ); -exit( pgm_test_summary() ); +exit( prv_test_summary() ); diff --git a/tests/unit/test-cron.php b/tests/unit/test-cron.php index 37c2ccd..63b4ac0 100644 --- a/tests/unit/test-cron.php +++ b/tests/unit/test-cron.php @@ -1,64 +1,64 @@ register_hooks(); -$hooks = array_column( $GLOBALS['pgm_test_state']['actions'], 'hook' ); -pgm_assert( in_array( PGM_CRON_HOOK, $hooks, true ), 'register_hooks: pgm_weekly_probe action registered' ); +$hooks = array_column( $GLOBALS['prv_test_state']['actions'], 'hook' ); +prv_assert( in_array( PRV_CRON_HOOK, $hooks, true ), 'register_hooks: prv_weekly_probe action registered' ); // ── Test: hook constant is correct string ──────────────────────────────── -pgm_assert_equals( 'pgm_weekly_probe', PGM_CRON_HOOK, 'PGM_CRON_HOOK: constant value correct' ); +prv_assert_equals( 'prv_weekly_probe', PRV_CRON_HOOK, 'PRV_CRON_HOOK: constant value correct' ); -// ── Test: PGM_Activator calls schedule_weekly ──────────────────────────── +// ── Test: PRV_Activator calls schedule_weekly ──────────────────────────── -pgm_test_reset(); +prv_test_reset(); // Seed options so seed_defaults() doesn't fail. -PGM_Activator::activate(); -pgm_assert( PGM_Cron::is_scheduled(), 'PGM_Activator::activate: weekly cron scheduled after activation' ); +PRV_Activator::activate(); +prv_assert( PRV_Cron::is_scheduled(), 'PRV_Activator::activate: weekly cron scheduled after activation' ); -// ── Test: PGM_Deactivator calls clear_schedule ─────────────────────────── +// ── Test: PRV_Deactivator calls clear_schedule ─────────────────────────── -PGM_Deactivator::deactivate(); -pgm_assert( ! PGM_Cron::is_scheduled(), 'PGM_Deactivator::deactivate: cron cleared after deactivation' ); +PRV_Deactivator::deactivate(); +prv_assert( ! PRV_Cron::is_scheduled(), 'PRV_Deactivator::deactivate: cron cleared after deactivation' ); -exit( pgm_test_summary() ); +exit( prv_test_summary() ); diff --git a/tests/unit/test-probe-runner.php b/tests/unit/test-probe-runner.php index 791aeca..17b00e0 100644 --- a/tests/unit/test-probe-runner.php +++ b/tests/unit/test-probe-runner.php @@ -1,34 +1,34 @@ 'bpc-157','label'=>'BPC-157']]; -$GLOBALS['pgm_test_state']['options']['pgm_prompt_intents'] = ['what is {peptide}']; -$GLOBALS['pgm_test_state']['options']['pgm_models'] = ['perplexity/sonar']; -$GLOBALS['pgm_test_state']['remote_posts'] = [ +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_peptides'] = [['slug'=>'bpc-157','label'=>'BPC-157']]; +$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}']; +$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar']; +$GLOBALS['prv_test_state']['remote_posts'] = [ ['response' => ['code' => 200], 'body' => $cited_body], ]; -$ledger1 = new PGM_Mock_Ledger(); -$runner1 = new PGM_Probe_Runner( $ledger1 ); +$ledger1 = new PRV_Mock_Ledger(); +$runner1 = new PRV_Probe_Runner( $ledger1 ); $counts1 = $runner1->run(); -pgm_assert_equals( 1, $counts1['probed'], 'basic run: 1 probed' ); -pgm_assert_equals( 0, $counts1['skipped_budget'], 'basic run: 0 budget skips' ); -pgm_assert_equals( 0, $counts1['skipped_error'], 'basic run: 0 error skips' ); -pgm_assert_equals( 1, $ledger1->afford_count, 'basic run: 1 can_afford check' ); +prv_assert_equals( 1, $counts1['probed'], 'basic run: 1 probed' ); +prv_assert_equals( 0, $counts1['skipped_budget'], 'basic run: 0 budget skips' ); +prv_assert_equals( 0, $counts1['skipped_error'], 'basic run: 0 error skips' ); +prv_assert_equals( 1, $ledger1->afford_count, 'basic run: 1 can_afford check' ); // ── Test: 2×1×1 run completes all probes when budget sufficient ─────────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['options']['pgm_peptides'] = [ +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_peptides'] = [ ['slug'=>'bpc-157','label'=>'BPC-157'], ['slug'=>'tb-500','label'=>'TB-500'], ]; -$GLOBALS['pgm_test_state']['options']['pgm_prompt_intents'] = ['what is {peptide}']; -$GLOBALS['pgm_test_state']['options']['pgm_models'] = ['perplexity/sonar']; -$GLOBALS['pgm_test_state']['remote_posts'] = [ +$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}']; +$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar']; +$GLOBALS['prv_test_state']['remote_posts'] = [ ['response' => ['code' => 200], 'body' => $cited_body], ['response' => ['code' => 200], 'body' => $cited_body], ]; -$ledger2 = new PGM_Mock_Ledger(); -$runner2 = new PGM_Probe_Runner( $ledger2 ); +$ledger2 = new PRV_Mock_Ledger(); +$runner2 = new PRV_Probe_Runner( $ledger2 ); $counts2 = $runner2->run(); -pgm_assert_equals( 2, $counts2['probed'], '2×1×1 run: 2 probes completed' ); -pgm_assert_equals( 2, $ledger2->afford_count, '2×1×1 run: 2 can_afford checks' ); +prv_assert_equals( 2, $counts2['probed'], '2×1×1 run: 2 probes completed' ); +prv_assert_equals( 2, $ledger2->afford_count, '2×1×1 run: 2 can_afford checks' ); // ── Test: budget cap abort after N probes ──────────────────────────────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['options']['pgm_peptides'] = [ +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_peptides'] = [ ['slug'=>'bpc-157','label'=>'BPC-157'], ['slug'=>'tb-500','label'=>'TB-500'], ['slug'=>'mk-677','label'=>'MK-677'], ]; -$GLOBALS['pgm_test_state']['options']['pgm_prompt_intents'] = ['what is {peptide}']; -$GLOBALS['pgm_test_state']['options']['pgm_models'] = ['perplexity/sonar']; +$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}']; +$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar']; // Queue 3 responses but cap budget at 1. -$GLOBALS['pgm_test_state']['remote_posts'] = [ +$GLOBALS['prv_test_state']['remote_posts'] = [ ['response' => ['code' => 200], 'body' => $cited_body], ['response' => ['code' => 200], 'body' => $cited_body], ['response' => ['code' => 200], 'body' => $cited_body], ]; -$exhausting_ledger = new PGM_Mock_Ledger(); +$exhausting_ledger = new PRV_Mock_Ledger(); $exhausting_ledger->exhaust_after = 1; // Budget exhausted after 1st can_afford check. -$runner3 = new PGM_Probe_Runner( $exhausting_ledger ); +$runner3 = new PRV_Probe_Runner( $exhausting_ledger ); $counts3 = $runner3->run(); -pgm_assert_equals( 1, $counts3['probed'], 'budget abort: 1 probe completed before cap' ); -pgm_assert( $counts3['skipped_budget'] >= 2, 'budget abort: ≥2 probes skipped (budget)' ); -pgm_assert_equals( 0, $counts3['skipped_error'], 'budget abort: 0 error skips' ); +prv_assert_equals( 1, $counts3['probed'], 'budget abort: 1 probe completed before cap' ); +prv_assert( $counts3['skipped_budget'] >= 2, 'budget abort: ≥2 probes skipped (budget)' ); +prv_assert_equals( 0, $counts3['skipped_error'], 'budget abort: 0 error skips' ); // ── Test: HTTP error counts as skipped_error ───────────────────────────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['options']['pgm_peptides'] = [['slug'=>'bpc-157','label'=>'BPC-157']]; -$GLOBALS['pgm_test_state']['options']['pgm_prompt_intents'] = ['what is {peptide}']; -$GLOBALS['pgm_test_state']['options']['pgm_models'] = ['perplexity/sonar']; +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_peptides'] = [['slug'=>'bpc-157','label'=>'BPC-157']]; +$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}']; +$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar']; // All retries fail with 500. -$GLOBALS['pgm_test_state']['remote_posts'] = [ +$GLOBALS['prv_test_state']['remote_posts'] = [ ['response' => ['code' => 500], 'body' => 'Error'], ['response' => ['code' => 500], 'body' => 'Error'], ['response' => ['code' => 500], 'body' => 'Error'], ]; -$ledger4 = new PGM_Mock_Ledger(); -$runner4 = new PGM_Probe_Runner( $ledger4 ); +$ledger4 = new PRV_Mock_Ledger(); +$runner4 = new PRV_Probe_Runner( $ledger4 ); $counts4 = $runner4->run(); -pgm_assert_equals( 0, $counts4['probed'], 'HTTP 500: 0 probes' ); -pgm_assert( $counts4['skipped_error'] >= 1, 'HTTP 500: ≥1 error skip recorded' ); +prv_assert_equals( 0, $counts4['probed'], 'HTTP 500: 0 probes' ); +prv_assert( $counts4['skipped_error'] >= 1, 'HTTP 500: ≥1 error skip recorded' ); // ── Test: WP_Error from wp_remote_post counts as skipped_error ────────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['options']['pgm_peptides'] = [['slug'=>'bpc-157','label'=>'BPC-157']]; -$GLOBALS['pgm_test_state']['options']['pgm_prompt_intents'] = ['what is {peptide}']; -$GLOBALS['pgm_test_state']['options']['pgm_models'] = ['perplexity/sonar']; +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_peptides'] = [['slug'=>'bpc-157','label'=>'BPC-157']]; +$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}']; +$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar']; // Return WP_Error for all retries. $wp_error = new WP_Error( 'http_request_failed', 'cURL error 28: Operation timed out' ); -$GLOBALS['pgm_test_state']['remote_posts'] = [ $wp_error, $wp_error, $wp_error ]; +$GLOBALS['prv_test_state']['remote_posts'] = [ $wp_error, $wp_error, $wp_error ]; -$ledger5 = new PGM_Mock_Ledger(); -$runner5 = new PGM_Probe_Runner( $ledger5 ); +$ledger5 = new PRV_Mock_Ledger(); +$runner5 = new PRV_Probe_Runner( $ledger5 ); $counts5 = $runner5->run(); -pgm_assert_equals( 0, $counts5['probed'], 'WP_Error: 0 probes completed' ); -pgm_assert( $counts5['skipped_error'] >= 1, 'WP_Error: ≥1 error skip recorded' ); +prv_assert_equals( 0, $counts5['probed'], 'WP_Error: 0 probes completed' ); +prv_assert( $counts5['skipped_error'] >= 1, 'WP_Error: ≥1 error skip recorded' ); // ── Test: run returns correct keys in counts array ─────────────────────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['options']['pgm_peptides'] = [['slug'=>'bpc-157','label'=>'BPC-157']]; -$GLOBALS['pgm_test_state']['options']['pgm_prompt_intents'] = ['what is {peptide}']; -$GLOBALS['pgm_test_state']['options']['pgm_models'] = ['perplexity/sonar']; -$GLOBALS['pgm_test_state']['remote_posts'] = [ +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_peptides'] = [['slug'=>'bpc-157','label'=>'BPC-157']]; +$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}']; +$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar']; +$GLOBALS['prv_test_state']['remote_posts'] = [ ['response' => ['code' => 200], 'body' => $cited_body], ]; -$ledger6 = new PGM_Mock_Ledger(); -$runner6 = new PGM_Probe_Runner( $ledger6 ); +$ledger6 = new PRV_Mock_Ledger(); +$runner6 = new PRV_Probe_Runner( $ledger6 ); $counts6 = $runner6->run(); -pgm_assert( array_key_exists( 'probed', $counts6 ), 'run: returns probed key' ); -pgm_assert( array_key_exists( 'skipped_budget', $counts6 ), 'run: returns skipped_budget key' ); -pgm_assert( array_key_exists( 'skipped_error', $counts6 ), 'run: returns skipped_error key' ); +prv_assert( array_key_exists( 'probed', $counts6 ), 'run: returns probed key' ); +prv_assert( array_key_exists( 'skipped_budget', $counts6 ), 'run: returns skipped_budget key' ); +prv_assert( array_key_exists( 'skipped_error', $counts6 ), 'run: returns skipped_error key' ); -exit( pgm_test_summary() ); +exit( prv_test_summary() ); diff --git a/tests/unit/test-provider-parsing.php b/tests/unit/test-provider-parsing.php index 2ca94ad..2043bbf 100644 --- a/tests/unit/test-provider-parsing.php +++ b/tests/unit/test-provider-parsing.php @@ -2,25 +2,25 @@ /** * Tests for provider response parsing. * - * Exercises PGM_Perplexity_Provider::parse_response() and - * PGM_OpenRouter_Provider::parse_response() with mock HTTP responses, + * Exercises PRV_Perplexity_Provider::parse_response() and + * PRV_OpenRouter_Provider::parse_response() with mock HTTP responses, * verifying domain extraction, citation detection, and cost calculation. * - * @package PeptideGeoMonitor + * @package PrVision */ declare(strict_types=1); require_once __DIR__ . '/../bootstrap.php'; -echo "=== PGM Provider Response Parsing Tests ===\n"; +echo "=== PRV Provider Response Parsing Tests ===\n"; -// ── PGM_Perplexity_Provider parsing ───────────────────────────────────── +// ── PRV_Perplexity_Provider parsing ───────────────────────────────────── -$provider = new PGM_Perplexity_Provider( null, new PGM_Citation_Detector() ); +$provider = new PRV_Perplexity_Provider( null, new PRV_Citation_Detector() ); // Test: cited via citations array -pgm_test_reset(); +prv_test_reset(); $data = [ 'choices' => [[ 'message' => [ 'content' => 'BPC-157 is a peptide. See peptiderepo.com' ] ]], 'citations' => [ @@ -30,34 +30,34 @@ $data = [ 'usage' => [ 'total_tokens' => 500 ], ]; $result = $provider->parse_response( $data ); -pgm_assert( $result->is_cited(), 'Perplexity: cited=true when peptiderepo.com in citations' ); -pgm_assert_equals( 1, $result->get_our_position(), 'Perplexity: position=1 when first in citations' ); -pgm_assert( count( $result->get_source_domains() ) === 2, 'Perplexity: 2 source domains extracted' ); -pgm_assert_equals( 500 * 0.000001, $result->get_cost_usd(), 'Perplexity: cost from token usage' ); +prv_assert( $result->is_cited(), 'Perplexity: cited=true when peptiderepo.com in citations' ); +prv_assert_equals( 1, $result->get_our_position(), 'Perplexity: position=1 when first in citations' ); +prv_assert( count( $result->get_source_domains() ) === 2, 'Perplexity: 2 source domains extracted' ); +prv_assert_equals( 500 * 0.000001, $result->get_cost_usd(), 'Perplexity: cost from token usage' ); // Test: not cited, no citations -pgm_test_reset(); +prv_test_reset(); $data_empty = [ 'choices' => [[ 'message' => [ 'content' => 'BPC-157 info here.' ] ]], 'citations' => [], 'usage' => [ 'total_tokens' => 200 ], ]; $result_empty = $provider->parse_response( $data_empty ); -pgm_assert( ! $result_empty->is_cited(), 'Perplexity: cited=false with empty citations' ); -pgm_assert_equals( null, $result_empty->get_our_position(), 'Perplexity: position=null when not cited' ); +prv_assert( ! $result_empty->is_cited(), 'Perplexity: cited=false with empty citations' ); +prv_assert_equals( null, $result_empty->get_our_position(), 'Perplexity: position=null when not cited' ); // Test: excerpt truncated to 500 chars -pgm_test_reset(); +prv_test_reset(); $long_content = str_repeat( 'a', 600 ); $data_long = [ 'choices' => [[ 'message' => [ 'content' => $long_content ] ]], 'citations' => [], ]; $result_long = $provider->parse_response( $data_long ); -pgm_assert_equals( 500, mb_strlen( $result_long->get_raw_excerpt() ), 'Perplexity: excerpt truncated to 500 chars' ); +prv_assert_equals( 500, mb_strlen( $result_long->get_raw_excerpt() ), 'Perplexity: excerpt truncated to 500 chars' ); // Test: object-style citations -pgm_test_reset(); +prv_test_reset(); $data_obj = [ 'choices' => [[ 'message' => [ 'content' => 'answer' ] ]], 'citations' => [ @@ -66,26 +66,26 @@ $data_obj = [ ], ]; $result_obj = $provider->parse_response( $data_obj ); -pgm_assert( $result_obj->is_cited(), 'Perplexity: object-style citations — cited=true' ); -pgm_assert_equals( 1, $result_obj->get_our_position(), 'Perplexity: object-style — position=1' ); +prv_assert( $result_obj->is_cited(), 'Perplexity: object-style citations — cited=true' ); +prv_assert_equals( 1, $result_obj->get_our_position(), 'Perplexity: object-style — position=1' ); // Test: fallback cost when no usage data -pgm_test_reset(); +prv_test_reset(); $data_no_usage = [ 'choices' => [[ 'message' => [ 'content' => 'answer' ] ]], 'citations' => [], ]; $result_no_usage = $provider->parse_response( $data_no_usage ); -pgm_assert_equals( PGM_Perplexity_Provider::ESTIMATED_COST_PER_PROBE, $result_no_usage->get_cost_usd(), 'Perplexity: fallback to ESTIMATED_COST_PER_PROBE when no usage' ); +prv_assert_equals( PRV_Perplexity_Provider::ESTIMATED_COST_PER_PROBE, $result_no_usage->get_cost_usd(), 'Perplexity: fallback to ESTIMATED_COST_PER_PROBE when no usage' ); -// ── PGM_OpenRouter_Provider parsing ───────────────────────────────────── +// ── PRV_OpenRouter_Provider parsing ───────────────────────────────────── echo "\n--- OpenRouter provider parsing ---\n"; -$or_provider = new PGM_OpenRouter_Provider( 'openai/gpt-4o-search-preview', null, new PGM_Citation_Detector() ); +$or_provider = new PRV_OpenRouter_Provider( 'openai/gpt-4o-search-preview', null, new PRV_Citation_Detector() ); // Test: annotations path (structured citations) -pgm_test_reset(); +prv_test_reset(); $data_ann = [ 'choices' => [[ 'message' => [ @@ -99,11 +99,11 @@ $data_ann = [ 'usage' => [ 'total_tokens' => 300 ], ]; $result_ann = $or_provider->parse_response( $data_ann ); -pgm_assert( $result_ann->is_cited(), 'OpenRouter: annotations path — cited=true' ); -pgm_assert_equals( 1, $result_ann->get_our_position(), 'OpenRouter: annotations path — position=1' ); +prv_assert( $result_ann->is_cited(), 'OpenRouter: annotations path — cited=true' ); +prv_assert_equals( 1, $result_ann->get_our_position(), 'OpenRouter: annotations path — position=1' ); // Test: regex URL extraction fallback -pgm_test_reset(); +prv_test_reset(); $data_regex = [ 'choices' => [[ 'message' => [ @@ -113,23 +113,23 @@ $data_regex = [ 'usage' => [ 'total_tokens' => 150 ], ]; $result_regex = $or_provider->parse_response( $data_regex ); -pgm_assert( $result_regex->is_cited(), 'OpenRouter: regex fallback — cited=true from inline URL' ); +prv_assert( $result_regex->is_cited(), 'OpenRouter: regex fallback — cited=true from inline URL' ); // Test: not cited -pgm_test_reset(); +prv_test_reset(); $data_no_cite = [ 'choices' => [[ 'message' => [ 'content' => 'Some response without our domain.' ] ]], ]; $result_no_cite = $or_provider->parse_response( $data_no_cite ); -pgm_assert( ! $result_no_cite->is_cited(), 'OpenRouter: not cited when peptiderepo.com absent from text' ); +prv_assert( ! $result_no_cite->is_cited(), 'OpenRouter: not cited when peptiderepo.com absent from text' ); // Test: cost calculation from token usage -pgm_test_reset(); +prv_test_reset(); $data_cost = [ 'choices' => [[ 'message' => [ 'content' => 'test' ] ]], 'usage' => [ 'total_tokens' => 1000 ], ]; $result_cost = $or_provider->parse_response( $data_cost ); -pgm_assert_equals( 1000 * 0.000002, $result_cost->get_cost_usd(), 'OpenRouter: cost from token usage (2$/1M)' ); +prv_assert_equals( 1000 * 0.000002, $result_cost->get_cost_usd(), 'OpenRouter: cost from token usage (2$/1M)' ); -exit( pgm_test_summary() ); +exit( prv_test_summary() ); diff --git a/tests/unit/test-score-calc.php b/tests/unit/test-score-calc.php index f71819a..5d00963 100644 --- a/tests/unit/test-score-calc.php +++ b/tests/unit/test-score-calc.php @@ -1,28 +1,28 @@ compute_score( 0, 0, 0.0 ), 'score: zero total returns 0.0' ); +prv_test_reset(); +prv_assert_equals( 0.0, $collector->compute_score( 0, 0, 0.0 ), 'score: zero total returns 0.0' ); // ── Test: all cited, position 1 ───────────────────────────────────────── @@ -30,15 +30,15 @@ pgm_assert_equals( 0.0, $collector->compute_score( 0, 0, 0.0 ), 'score: zero tot // base_score = 10/10 = 1.0 // position_bonus = (1+1+1+…)/10 = 10/10 = 1.0 // score = (1.0 + 1.0) / 2 = 1.0 -pgm_test_reset(); -pgm_assert_equals( 1.0, $collector->compute_score( 10, 10, 10.0 ), 'score: all cited at position 1 → 1.0' ); +prv_test_reset(); +prv_assert_equals( 1.0, $collector->compute_score( 10, 10, 10.0 ), 'score: all cited at position 1 → 1.0' ); // ── Test: none cited ──────────────────────────────────────────────────── // 10 probes, 0 cited, position_sum=0 // base_score = 0, position_bonus = 0 → score = 0 -pgm_test_reset(); -pgm_assert_equals( 0.0, $collector->compute_score( 0, 10, 0.0 ), 'score: none cited → 0.0' ); +prv_test_reset(); +prv_assert_equals( 0.0, $collector->compute_score( 0, 10, 0.0 ), 'score: none cited → 0.0' ); // ── Test: half cited at position 2 ────────────────────────────────────── @@ -46,9 +46,9 @@ pgm_assert_equals( 0.0, $collector->compute_score( 0, 10, 0.0 ), 'score: none ci // base_score = 5/10 = 0.5 // position_bonus = 2.5/10 = 0.25 // score = (0.5 + 0.25) / 2 = 0.375 -pgm_test_reset(); +prv_test_reset(); $score = $collector->compute_score( 5, 10, 2.5 ); -pgm_assert_equals( 0.375, $score, 'score: half cited at position 2 → 0.375' ); +prv_assert_equals( 0.375, $score, 'score: half cited at position 2 → 0.375' ); // ── Test: single probe, cited at position 3 ───────────────────────────── @@ -56,25 +56,25 @@ pgm_assert_equals( 0.375, $score, 'score: half cited at position 2 → 0.375' ); // base_score = 1.0 // position_bonus = 0.3333 // score = (1.0 + 0.3333) / 2 ≈ 0.6667 (rounded to 4 decimals) -pgm_test_reset(); +prv_test_reset(); $score = $collector->compute_score( 1, 1, 1.0 / 3.0 ); -pgm_assert_equals( 0.6667, $score, 'score: single probe cited at position 3 → 0.6667' ); +prv_assert_equals( 0.6667, $score, 'score: single probe cited at position 3 → 0.6667' ); // ── Test: score is bounded [0, 1] ─────────────────────────────────────── // Extreme: position_sum could theoretically exceed total but that's logically // impossible with 1-based positions (position_sum ≤ cited_count ≤ total). // Verify edge case: 1 probe, 1 cited, position 1 -pgm_test_reset(); -pgm_assert( $collector->compute_score( 1, 1, 1.0 ) <= 1.0, 'score: never exceeds 1.0' ); -pgm_assert( $collector->compute_score( 0, 10, 0.0 ) >= 0.0, 'score: never below 0.0' ); +prv_test_reset(); +prv_assert( $collector->compute_score( 1, 1, 1.0 ) <= 1.0, 'score: never exceeds 1.0' ); +prv_assert( $collector->compute_score( 0, 10, 0.0 ) >= 0.0, 'score: never below 0.0' ); // ── Test: precision — 4 decimal places ────────────────────────────────── -pgm_test_reset(); +prv_test_reset(); $score = $collector->compute_score( 3, 7, 1.5 ); // base = 3/7 ≈ 0.4286, bonus = 1.5/7 ≈ 0.2143, avg ≈ 0.3214 $expected = round( ( 3.0 / 7.0 + 1.5 / 7.0 ) / 2.0, 4 ); -pgm_assert_equals( $expected, $score, 'score: 4-decimal precision maintained' ); +prv_assert_equals( $expected, $score, 'score: 4-decimal precision maintained' ); -exit( pgm_test_summary() ); +exit( prv_test_summary() ); diff --git a/tests/unit/test-uninstall.php b/tests/unit/test-uninstall.php index 8beb04c..4d8016a 100644 --- a/tests/unit/test-uninstall.php +++ b/tests/unit/test-uninstall.php @@ -2,31 +2,31 @@ /** * Tests for uninstall purge: table drop + options deletion. * - * Exercises PGM_Table_Manager::drop_table() and verifies the uninstall + * Exercises PRV_Table_Manager::drop_table() and verifies the uninstall * script logic (simulated because it requires WP_UNINSTALL_PLUGIN). * - * @package PeptideGeoMonitor + * @package PrVision */ declare(strict_types=1); require_once __DIR__ . '/../bootstrap.php'; -echo "=== PGM Uninstall Purge Tests ===\n"; +echo "=== PRV Uninstall Purge Tests ===\n"; // ── Mock $wpdb to track DROP calls ────────────────────────────────────── -$GLOBALS['pgm_test_state']['drop_calls'] = []; +$GLOBALS['prv_test_state']['drop_calls'] = []; // Extend the stub to record DROP statements. global $wpdb; $wpdb = new class extends stdClass_wpdb { public function query( string $sql ): bool { if ( str_contains( strtoupper( $sql ), 'DROP TABLE' ) ) { - $GLOBALS['pgm_test_state']['drop_calls'][] = $sql; + $GLOBALS['prv_test_state']['drop_calls'][] = $sql; } - if ( str_contains( strtoupper( $sql ), 'DELETE FROM' ) && str_contains( $sql, "pgm\\_" ) ) { - $GLOBALS['pgm_test_state']['options_deleted'] = true; + if ( str_contains( strtoupper( $sql ), 'DELETE FROM' ) && str_contains( $sql, "prv\\_" ) ) { + $GLOBALS['prv_test_state']['options_deleted'] = true; } return true; } @@ -34,68 +34,68 @@ $wpdb = new class extends stdClass_wpdb { // ── Test: drop_table issues DROP TABLE IF EXISTS ───────────────────────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['drop_calls'] = []; -PGM_Table_Manager::drop_table(); +prv_test_reset(); +$GLOBALS['prv_test_state']['drop_calls'] = []; +PRV_Table_Manager::drop_table(); -pgm_assert( ! empty( $GLOBALS['pgm_test_state']['drop_calls'] ), 'drop_table: DROP TABLE query executed' ); -$drop_sql = $GLOBALS['pgm_test_state']['drop_calls'][0] ?? ''; -pgm_assert( str_contains( strtoupper( $drop_sql ), 'DROP TABLE IF EXISTS' ), 'drop_table: uses DROP TABLE IF EXISTS' ); -pgm_assert( str_contains( $drop_sql, 'pgm_ai_visibility' ), 'drop_table: targets pgm_ai_visibility table' ); +prv_assert( ! empty( $GLOBALS['prv_test_state']['drop_calls'] ), 'drop_table: DROP TABLE query executed' ); +$drop_sql = $GLOBALS['prv_test_state']['drop_calls'][0] ?? ''; +prv_assert( str_contains( strtoupper( $drop_sql ), 'DROP TABLE IF EXISTS' ), 'drop_table: uses DROP TABLE IF EXISTS' ); +prv_assert( str_contains( $drop_sql, 'prv_ai_visibility' ), 'drop_table: targets prv_ai_visibility table' ); // ── Test: create_table sets the schema_version option ──────────────────── -pgm_test_reset(); -PGM_Table_Manager::create_table(); -$schema_ver = get_option( 'pgm_schema_version' ); -pgm_assert_equals( PGM_SCHEMA_VERSION, $schema_ver, 'create_table: pgm_schema_version option set after create' ); +prv_test_reset(); +PRV_Table_Manager::create_table(); +$schema_ver = get_option( 'prv_schema_version' ); +prv_assert_equals( PRV_SCHEMA_VERSION, $schema_ver, 'create_table: prv_schema_version option set after create' ); // ── Test: get_table_name returns prefixed name ─────────────────────────── -pgm_test_reset(); -$table = PGM_Table_Manager::get_table_name(); -pgm_assert_equals( 'wp_pgm_ai_visibility', $table, 'get_table_name: returns wp_ prefixed table name' ); +prv_test_reset(); +$table = PRV_Table_Manager::get_table_name(); +prv_assert_equals( 'wp_prv_ai_visibility', $table, 'get_table_name: returns wp_ prefixed table name' ); // ── Test: seed_defaults writes expected option keys ────────────────────── -pgm_test_reset(); -PGM_Config::seed_defaults(); +prv_test_reset(); +PRV_Config::seed_defaults(); -pgm_assert( isset( $GLOBALS['pgm_test_state']['options']['pgm_monthly_budget_usd'] ), 'seed_defaults: pgm_monthly_budget_usd set' ); -pgm_assert( isset( $GLOBALS['pgm_test_state']['options']['pgm_peptides'] ), 'seed_defaults: pgm_peptides set' ); -pgm_assert( isset( $GLOBALS['pgm_test_state']['options']['pgm_prompt_intents'] ), 'seed_defaults: pgm_prompt_intents set' ); -pgm_assert( isset( $GLOBALS['pgm_test_state']['options']['pgm_models'] ), 'seed_defaults: pgm_models set' ); +prv_assert( isset( $GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] ), 'seed_defaults: prv_monthly_budget_usd set' ); +prv_assert( isset( $GLOBALS['prv_test_state']['options']['prv_peptides'] ), 'seed_defaults: prv_peptides set' ); +prv_assert( isset( $GLOBALS['prv_test_state']['options']['prv_prompt_intents'] ), 'seed_defaults: prv_prompt_intents set' ); +prv_assert( isset( $GLOBALS['prv_test_state']['options']['prv_models'] ), 'seed_defaults: prv_models set' ); // ── Test: seed_defaults does not overwrite existing values ─────────────── -pgm_test_reset(); -$GLOBALS['pgm_test_state']['options']['pgm_monthly_budget_usd'] = 99.0; -PGM_Config::seed_defaults(); -pgm_assert_equals( 99.0, $GLOBALS['pgm_test_state']['options']['pgm_monthly_budget_usd'], 'seed_defaults: does not overwrite existing pgm_monthly_budget_usd' ); +prv_test_reset(); +$GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 99.0; +PRV_Config::seed_defaults(); +prv_assert_equals( 99.0, $GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'], 'seed_defaults: does not overwrite existing prv_monthly_budget_usd' ); -// ── Test: default budget is PGM_DEFAULT_MONTHLY_BUDGET_USD ────────────── +// ── Test: default budget is PRV_DEFAULT_MONTHLY_BUDGET_USD ────────────── -pgm_test_reset(); -PGM_Config::seed_defaults(); -pgm_assert_equals( PGM_DEFAULT_MONTHLY_BUDGET_USD, $GLOBALS['pgm_test_state']['options']['pgm_monthly_budget_usd'], 'seed_defaults: default budget = PGM_DEFAULT_MONTHLY_BUDGET_USD (5.0)' ); +prv_test_reset(); +PRV_Config::seed_defaults(); +prv_assert_equals( PRV_DEFAULT_MONTHLY_BUDGET_USD, $GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'], 'seed_defaults: default budget = PRV_DEFAULT_MONTHLY_BUDGET_USD (5.0)' ); // ── Test: default peptide count is 12 ─────────────────────────────────── -pgm_test_reset(); -$peptides = PGM_Config::get_peptides(); -pgm_assert_equals( 12, count( $peptides ), 'get_peptides: default seed has 12 peptides' ); +prv_test_reset(); +$peptides = PRV_Config::get_peptides(); +prv_assert_equals( 12, count( $peptides ), 'get_peptides: default seed has 12 peptides' ); // ── Test: default prompt intent count is 3 ────────────────────────────── -pgm_test_reset(); -$intents = PGM_Config::get_prompt_intents(); -pgm_assert_equals( 3, count( $intents ), 'get_prompt_intents: default seed has 3 intents' ); +prv_test_reset(); +$intents = PRV_Config::get_prompt_intents(); +prv_assert_equals( 3, count( $intents ), 'get_prompt_intents: default seed has 3 intents' ); // ── Test: default model count is 3 ────────────────────────────────────── -pgm_test_reset(); -$models = PGM_Config::get_models(); -pgm_assert_equals( 3, count( $models ), 'get_models: default seed has 3 models' ); -pgm_assert( in_array( 'perplexity/sonar', $models, true ), 'get_models: perplexity/sonar in default models' ); +prv_test_reset(); +$models = PRV_Config::get_models(); +prv_assert_equals( 3, count( $models ), 'get_models: default seed has 3 models' ); +prv_assert( in_array( 'perplexity/sonar', $models, true ), 'get_models: perplexity/sonar in default models' ); -exit( pgm_test_summary() ); +exit( prv_test_summary() ); diff --git a/uninstall.php b/uninstall.php index e650dc7..7b6fb1d 100644 --- a/uninstall.php +++ b/uninstall.php @@ -1,13 +1,13 @@ prefix . 'pgm_ai_visibility'; +$table = $wpdb->prefix . 'prv_ai_visibility'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); -/* ── 2. Delete all pgm_ prefixed options ──────────────────────────── */ +/* ── 2. Delete all prv_ prefixed options ──────────────────────────── */ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->query( - "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'pgm\_%'" + "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'prv\_%'" ); /* ── 3. Clear any remaining scheduled cron events ─────────────────── */ -wp_clear_scheduled_hook( 'pgm_weekly_probe' ); +wp_clear_scheduled_hook( 'prv_weekly_probe' );
' . esc_html__( 'Peptide', 'peptide-geo-monitor' ) . '' . esc_html__( 'Cited?', 'peptide-geo-monitor' ) . '' . esc_html__( 'Our Position', 'peptide-geo-monitor' ) . '' . esc_html__( 'Top Competing Domains', 'peptide-geo-monitor' ) . '' . esc_html__( 'Peptide', 'pr-vision' ) . '' . esc_html__( 'Cited?', 'pr-vision' ) . '' . esc_html__( 'Our Position', 'pr-vision' ) . '' . esc_html__( 'Top Competing Domains', 'pr-vision' ) . '