Files
pr-vision/ARCHITECTURE.md
peptiderepo ca1a41f6f9 feat: peptide-geo-monitor v0.1.0 — AI-visibility dashboard
Add initial plugin implementation per the CTO engineering handoff
(geomonitor/threads/2026-06-ai-visibility-v1/01-cto-eng-handoff.md).

## What
- PGM_Probe_Provider interface + PGM_Probe_Result value object
- PGM_Perplexity_Provider (sonar via OpenRouter/CF AI Gateway)
- PGM_OpenRouter_Provider (GPT-search + Gemini-search, generic parameterised)
- PGM_Gateway_Client: shared HTTP + exponential-backoff retry (mirrors PRAB)
- PGM_Citation_Detector: domain extraction, is_cited(), get_our_position()
- PGM_Cost_Ledger: MTD cost from DB, can_afford() pre-check, hard monthly cap
- PGM_Probe_Runner: peptides×intents×models run, budget abort, in-run cache
- PGM_Table_Manager: pgm_ai_visibility dbDelta, schema-version, drop_table()
- PGM_Config: typed getters, seed_defaults() (12 peptides, 3 intents, 3 models)
- PGM_Cron: weekly pgm_weekly_probe schedule/clear
- PGM_Data_Collector + PGM_Dashboard_Panel interfaces (seam for future panels)
- PGM_Collector_Registry: singleton registry
- PGM_Ai_Visibility_Collector: trendline, standings, MTD cost (score formula)
- PGM_Ai_Visibility_Panel: proxy note, Chart.js trendline, standings table
- PGM_Admin_Page: GEO Monitor menu, Run now POST handler (nonce+capability)
- uninstall.php: DROP TABLE + DELETE pgm_* options + wp_clear_scheduled_hook
- Unit tests (6 files): citation detection, score calc, cost ledger + budget
  abort, provider response parsing (mock HTTP), cron, uninstall purge
- ARCHITECTURE.md, CONVENTIONS.md, CONTEXT.md (score formula), README.md,
  CHANGELOG.md — same-commit per DoD
- CI workflow: PHP lint (8.1/8.2/8.3) + PHPCS + 300-line check

## Security
- Secrets from wp-config constants only; none hardcoded
- manage_options + nonce on every admin action
- Input sanitized; output escaped

Agent-Session: geomonitor v0.1.0
2026-06-13 08:18:56 +00:00

9.4 KiB
Raw Blame History

Architecture — Peptide GEO Monitor

Version: 0.1.0 | 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.

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

peptide-geo-monitor/
├── peptide-geo-monitor.php          # Plugin boot: constants, autoloader, hooks
├── uninstall.php                    # Full data teardown: DROP TABLE + DELETE pgm_* 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
│   ├── 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)
│   ├── collector/
│   │   ├── interface-pgm-data-collector.php  # collect(): array seam
│   │   └── class-pgm-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
├── 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) ─────────────────────────┤
                                                        ↓
                                            PGM_Probe_Runner::run()
                                                        │
                    ┌───────────────────────────────────┤
                    │  for each peptide × intent × model│
                    │                                   │
                    │  PGM_Cost_Ledger::can_afford()     │
                    │    └─ ABORT gracefully if at cap   │
                    │                                   │
                    │  PGM_Probe_Provider::probe(query)  │
                    │    └─ PGM_Gateway_Client           │
                    │         └─ Cloudflare AI Gateway   │
                    │              └─ OpenRouter         │
                    │                   └─ LLM           │
                    │                                   │
                    │  PGM_Citation_Detector             │
                    │    └─ extract domains              │
                    │    └─ detect peptiderepo.com       │
                    │                                   │
                    │  $wpdb→insert(pgm_ai_visibility)   │
                    │  PGM_Cost_Ledger::update_row_cost()│
                    └───────────────────────────────────┘
                                                        │
Admin page load ────────────────────────────────────────┤
                                                        ↓
                                  PGM_Admin_Page::render_page()
                                          │
                                PGM_Collector_Registry
                                          │
                         PGM_Ai_Visibility_Collector::collect()
                                          │  (DB reads)
                         PGM_Ai_Visibility_Panel::render()
                                          │  (HTML output)
                                   Browser (Chart.js)

4. Database

Table: {prefix}pgm_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 pgm_schema_version option.


5. External API integrations

Cloudflare AI Gateway

All LLM calls route through:

https://gateway.ai.cloudflare.com/v1/{PGM_CF_ACCOUNT_ID}/{PGM_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
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

6. Collector / Panel seam

PGM_Data_Collector (interface)           PGM_Dashboard_Panel (interface)
        │                                          │
PGM_Ai_Visibility_Collector          PGM_Ai_Visibility_Panel
        │                                          │
        └──────── PGM_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(). No dashboard shell changes required.

7. Security

  • Secrets read from wp-config.php constants at runtime — never hardcoded.
  • All admin actions: manage_options capability 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.