Rename to PR Vision (PRV_/pr-vision); fix www-strip char-mask in competitor display (str_starts_with); make PHPCS a CI gate; remove dead in-run cache. QA: APPROVE — qa-reviews/pr-vision/2026-06-13-c633fdb9e1541386a713a6de12d2ee2ae4239640.md Agent-Session: pr-vision rename v0.1.1 merge
9.3 KiB
Architecture — PR Vision
Version: 0.1.1 | Last updated: 2026-06-13
1. Overview
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.
2. File tree
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-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-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-prv-data-collector.php # collect(): array seam
│ │ └── class-prv-ai-visibility-collector.php # AI-visibility data from DB
│ └── panel/
│ ├── 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/
│ ├── test-citation-detector.php # Domain parsing + cite detection
│ ├── test-score-calc.php # Visibility score formula
│ ├── test-cost-ledger.php # MTD cost + budget cap
│ ├── test-provider-parsing.php # Mock-HTTP provider response parsing
│ ├── test-cron.php # Schedule/clear + activation/deactivation
│ ├── test-probe-runner.php # Run mechanics + budget abort
│ └── test-uninstall.php # Table drop + option purge
└── .github/workflows/ci.yml # PHP lint matrix + PHPCS + 300-line check
3. Data flow
WP-Cron (weekly) ──────────────────────────────────────┐
Admin "Run now" (POST + nonce) ─────────────────────────┤
↓
PRV_Probe_Runner::run()
│
┌───────────────────────────────────┤
│ for each peptide × intent × model│
│ │
│ PRV_Cost_Ledger::can_afford() │
│ └─ ABORT gracefully if at cap │
│ │
│ PRV_Probe_Provider::probe(query) │
│ └─ PRV_Gateway_Client │
│ └─ Cloudflare AI Gateway │
│ └─ OpenRouter │
│ └─ LLM │
│ │
│ PRV_Citation_Detector │
│ └─ extract domains │
│ └─ detect peptiderepo.com │
│ │
│ $wpdb→insert(prv_ai_visibility) │
│ PRV_Cost_Ledger::update_row_cost()│
└───────────────────────────────────┘
│
Admin page load ────────────────────────────────────────┤
↓
PRV_Admin_Page::render_page()
│
PRV_Collector_Registry
│
PRV_Ai_Visibility_Collector::collect()
│ (DB reads)
PRV_Ai_Visibility_Panel::render()
│ (HTML output)
Browser (Chart.js)
4. Database
Table: {prefix}prv_ai_visibility
| Column | Type | Notes |
|---|---|---|
| id | BIGINT UNSIGNED PK | Auto-increment |
| run_id | VARCHAR(36) | UUID per run |
| captured_at | DATETIME | UTC |
| peptide_slug | VARCHAR(200) | e.g. "bpc-157" |
| peptide_label | VARCHAR(200) | e.g. "BPC-157" |
| model | VARCHAR(200) | e.g. "perplexity/sonar" |
| prompt_intent | VARCHAR(200) | Template string |
| cited | TINYINT(1) | 1 = peptiderepo.com in sources |
| our_position | INT NULL | 1-based position; NULL if not cited |
| source_domains | LONGTEXT | JSON array of domains |
| raw_excerpt | LONGTEXT | First 500 chars of LLM response |
| cost_usd | DECIMAL(12,8) | Actual call cost |
Schema version tracked in prv_schema_version option.
5. External API integrations
Cloudflare AI Gateway
All LLM calls route through:
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.
Pattern mirrors PRAutoBlogger's class-open-router-config.php and class-open-router-request-builder.php — same auth injection, same cURL belt-and-suspenders for Hostinger.
Providers (v1)
| Provider | Model | Citation source |
|---|---|---|
| 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
PRV_Data_Collector (interface) PRV_Dashboard_Panel (interface)
│ │
PRV_Ai_Visibility_Collector PRV_Ai_Visibility_Panel
│ │
└──────── PRV_Collector_Registry ──────────┘
(key: "ai_visibility")
To add a future SEO collector (keyword rankings, schema coverage…):
- Implement
PRV_Data_Collector+PRV_Dashboard_Panel. - Register both in
PRV_Plugin::init(). No dashboard shell changes required.
7. Security
- Secrets read from
wp-config.phpconstants at runtime — never hardcoded. - All admin actions:
manage_optionscapability check + nonce verification. - All DB values escaped with
$wpdb->prepare(). - All HTML output escaped with
esc_html(),esc_url(),wp_json_encode().
8. Key decisions
| Decision | Rationale |
|---|---|
| Use OpenRouter for all models | Single API key, single gateway URL, Perplexity sonar is accessible via OpenRouter's model routing. |
| Perplexity sonar as primary | Returns an explicit citations[] array — the most faithful citation signal available. |
| Cost ledger from visibility table | No separate ledger table needed; SUM(cost_usd) per calendar month is sufficient and keeps the schema minimal. |
| Chart.js via CDN | No build step, no bundler, admin-only load, pinned version. |
| Plain PHP tests (no PHPUnit) | Mirrors peptide-repo-core's pattern; CI runs in the lint-php job with no extra setup. |