6 Commits

Author SHA1 Message Date
peptiderepo
09dc6db9ce P2 pre-merge fixes: io_captured column, reconcile copy, harden security test
Fix 1: Add io_captured TINYINT(1) NOT NULL DEFAULT 0 to prv_call_meta
CREATE (baked into initial schema — table is new in v0.3.0, no separate
migration needed). PRV_Capture_Writer::write_io() sets io_captured=1 via
set_io_captured() on success; error rows and legacy rows keep 0.
Drawer state now driven by io_captured instead of config_version heuristic:
  0         → Legacy/not-captured
  1 + io    → Normal
  1 + no io → Pruned
ARCHITECTURE.md schema section updated.

Fix 2: Soften reconciliation copy in PRV_Costs_Renderer (~line 75):
'matches the Dashboard MTD total' → 'approximately reconciles to the
Dashboard MTD total' — error calls write prv_call_meta (cost 0) but not
prv_ai_visibility, so exact equality is not guaranteed.

Fix 3: Harden security test (test-capture-writer Test 4): inject sentinel
sk-or-TEST-LEAK-CANARY as PRV_OPENROUTER_API_KEY constant; queue a mocked
401 wp_remote_post response; drive PRV_Probe_Runner → PRV_Probe_Run_Executor
→ probe_one() 401 error path end-to-end; assert the sentinel, Bearer, and
Authorization appear in NO stored row (prv_call_meta or prv_call_io).
Original direct-writer security test retained as Test 3.
2026-06-15 09:56:11 +00:00
peptiderepo
94bef937c7 Fix CI round 4: blank line before @var tags required by WPCS
WPCS rule: blank * line required before tag block in doccomment.
Added blank before @var in both property docblocks.
Saved 2 lines by removing cosmetic blank lines between class
opening brace and first property, and between the two properties.
2026-06-15 09:34:20 +00:00
peptiderepo
3780ff92e2 Fix CI round 3: executor class docblock + @var format + CommaSpacing
- Restored separate file docblock + class docblock (WPCS requires
  class to have its own doc comment with short description)
- Property @var docblocks: expanded back to 4-line format with
  short description on first line (WPCS MissingShortDescription)
- write_meta compact arrays: added
  Universal.WhiteSpace.CommaSpacing.TooMuchSpaceAfter to
  phpcs:disable block (column-aligned pairs use tab spacing)
- Class closing brace: removed blank line before }
- Trims @see and blank lines to keep file exactly at 300 lines
2026-06-15 09:32:43 +00:00
peptiderepo
74d2f29117 Fix CI round 2: InterpolatedNotPrepared + executor WPCS + test stub
class-prv-cost-rollup-query.php:
- get_mtd_summary + get_model_breakdown: phpcs:disable/enable block for
  InterpolatedNotPrepared (single-line ignore doesn't cover multi-line
  string spans)

class-prv-probe-run-executor.php:
- Reduced to 290 lines (well under 300)
- write_meta array calls: phpcs:disable ArrayItemNoNewLine for compact
  2-per-line format; write_io try/catch explicit comment on empty catch
- persist_result: expanded insert array to one item per line
- Class docblock consolidated; property @var docblocks to one-line format
- Closing class brace: removed extra blank line (PSR2 CloseBraceAfterBody)

tests/bootstrap.php + test-cost-audit.php:
- Added get_row() stub to stdClass_wpdb (PRV_Cost_Rollup_Query uses
  get_row for get_mtd_summary, not get_var)
- Added wpdb_row key to initial state + prv_test_reset()
- Test 1: seed wpdb_row with expected row data instead of wpdb_var
2026-06-15 09:30:05 +00:00
peptiderepo
c489ed3424 Fix CI: PHP 8.1 compat + WPCS violations in v0.3.0 new files
PHP 8.1: Cannot use positional argument after argument unpacking —
  class-prv-call-log-query.php line 77 (LIMIT/OFFSET args merged into
  array before spread).

WPCS fixes across 8 files:
- class-prv-admin-page-css.php: short description capital letter
- class-prv-admin-page.php: docblock started on same line as closing brace
- class-prv-call-drawer-renderer.php: param spacing, empty elseif blocks,
  extra indent on render_inline_script
- class-prv-call-log-query.php: InterpolatedNotPrepared (use
  phpcs:disable/enable block for WHERE-clause interpolation)
- class-prv-cost-rollup-query.php: InterpolatedNotPrepared for GROUP BY
  with dynamic column (phpcs:disable/enable block)
- class-prv-costs-renderer.php: AssociativeArrayFound on inline sub-arrays
- class-prv-probe-run-executor.php: docblock spacing, empty catch blocks,
  inline array items, ArrayItemNoNewLine (phpcs:disable on write_meta calls)
- class-prv-probe-runner.php: extra space before ?? operator
2026-06-15 09:23:21 +00:00
peptiderepo
4eee9f5f64 PR Vision v0.3.0: per-call cost accountability + LLM call audit trail
P0 security:
- PRV_Gateway_Client: remove response body from RuntimeException (HTTP status only)
- PRV_Capture_Writer: allowlist-only write (prompt/response text, never headers/auth)
- Mandatory capture order: can_afford → probe → persist_result → update_row_cost → write_meta → capture_io
- capture_io last and wrapped in swallowing try/catch

New tables (SCHEMA_VERSION 3):
- prv_call_meta: cost/metadata per call, kept indefinitely
- prv_call_io: rendered prompt + raw response, pruned on retention window (default 90d)

New classes:
- PRV_Call_Meta_Table, PRV_Call_Io_Table: table schema + dbDelta
- PRV_Capture_Writer: metadata + I/O write; security allowlist enforced
- PRV_Prune_Cron: daily prv_daily_prune cron deletes aged prv_call_io rows only
- PRV_Cost_Rollup_Query: MTD summary, model breakdown, drill-down, projection
- PRV_Call_Log_Query: paginated + filtered call-log query + per-call I/O fetch
- PRV_Costs_Page + PRV_Costs_Renderer: Costs admin sub-page (cap progress, bento, model table, drill-down)
- PRV_Call_Log_Page + PRV_Call_Log_Table_Renderer: Call Log admin sub-page (filter bar, paginated table, WCAG AA pagination)
- PRV_Call_Drawer_Renderer: non-modal complementary drawer (5 states: Normal/Pruned/Legacy/Failed/Loading)
- PRV_Call_Drawer_Script: inline JS for drawer (300-line split from renderer)
- PRV_Call_Detail_Ajax: wp_ajax_prv_call_detail AJAX handler
- PRV_Admin_Page_Css: CSS provider (300-line split from PRV_Admin_Page)
- PRV_Probe_Run_Executor: inner probe loop (300-line split from PRV_Probe_Runner)

Updated:
- PRV_Probe_Result: tokens_in/tokens_out params + getters
- Both providers: report tokens_in/tokens_out from usage.prompt_tokens/completion_tokens
- PRV_Probe_Runner: slimmed to lock+UUID+delegation; injects PRV_Capture_Writer
- PRV_Config: get_io_retention_days(); PRV_IO_RETENTION_DEFAULT_DAYS constant
- PRV_Upgrader/Activator/Deactivator/Plugin: register new tables, cron, pages, AJAX
- Settings: retention days field + save in controller
- uninstall.php: DROP both new tables, clear prv_daily_prune cron
- Subnav 4-tab strip (Dashboard | Costs | Call Log | Settings) on all pages
- ARCHITECTURE.md, CHANGELOG.md, CONTEXT.md, CONVENTIONS.md updated

Tests:
- test-capture-writer.php: write_meta, write_io, P0 security (401 record has no key/Bearer/sk-or-)
- test-cost-audit.php: MTD reconciliation, projection, prune, forced capture_io exception, table drops

Agent-Session: peptiderepo-geomonitor-v0.3.0-cost-audit
2026-06-15 09:09:29 +00:00
6 changed files with 4 additions and 196 deletions

View File

@@ -7,28 +7,6 @@ Versioning: [Semantic Versioning](https://semver.org/)
--- ---
## [0.3.1] — 2026-06-15
Self-healing cron scheduling: `PRV_Cron_Guard` ensures managed WP-Cron events
are re-scheduled on `init` when missing (e.g. after an rsync deploy that
bypasses the activation hook).
### Fixed
- Managed cron hooks (`prv_weekly_probe`, `prv_daily_prune`) are now
re-scheduled automatically on any page load where they are missing.
Delegates to `PRV_Cron::schedule()` and `PRV_Prune_Cron::schedule()` so
recurrence and first-run offset are identical to what the activator sets.
Hot-path cost: two `wp_next_scheduled()` checks; no DB writes unless an
event is missing.
### Added
- **`PRV_Cron_Guard`** (`includes/core/class-prv-cron-guard.php`) — idempotent
guard registered on `init`; calls canonical scheduler methods when missing.
- `PRV_Plugin::init()` — wires `PRV_Cron_Guard::register_hooks()`.
- Tests: `test-cron-guard.php`.
---
## [0.3.0] — 2026-06-15 ## [0.3.0] — 2026-06-15
Per-call cost accountability + LLM call audit trail. Every probe call now Per-call cost accountability + LLM call audit trail. Every probe call now

View File

@@ -1,69 +0,0 @@
<?php
/**
* Self-healing cron guard: re-schedules managed cron events when missing.
*
* @package PrVision
*/
declare(strict_types=1);
/**
* Idempotent guard that ensures all managed WP-Cron events are scheduled.
*
* WordPress fires the plugin activator only on activation, not on rsync
* file-sync updates (our deploy method). This guard runs on `init` and
* re-schedules any missing cron event by delegating to the canonical
* scheduler method for that hook -- so recurrence and first-run offset are
* always consistent with the activator.
*
* Managed hooks:
* - `prv_weekly_probe` -- scheduled via PRV_Cron::schedule()
* - `prv_daily_prune` -- scheduled via PRV_Prune_Cron::schedule()
*
* Hot-path cost: two `wp_next_scheduled()` checks per request. No DB
* writes unless an event is actually missing.
*
* Who triggers: init action (registered in PRV_Plugin::init()).
* Dependencies: PRV_Cron, PRV_Prune_Cron.
*
* @see class-prv-cron.php -- Canonical weekly probe scheduler.
* @see class-prv-prune-cron.php -- Canonical daily prune scheduler.
* @see class-prv-activator.php -- Original schedule call on activation.
* @package PrVision
*/
class PRV_Cron_Guard {
/**
* Register the guard on the `init` hook.
*
* Side effects: Adds action for `init`.
*
* @return void
*/
public function register_hooks(): void {
add_action( 'init', array( $this, 'ensure_scheduled' ) );
}
/**
* Ensure every managed cron hook is scheduled.
*
* For each managed hook, checks `wp_next_scheduled()`. If the event is
* missing, delegates to the canonical scheduler (same method the
* activator calls) so recurrence and offset are never duplicated here.
* Calling this when all events are already scheduled is a pure no-op.
*
* Side effects: May call PRV_Cron::schedule() or
* PRV_Prune_Cron::schedule() when an event is missing.
*
* @return void
*/
public function ensure_scheduled(): void {
if ( ! wp_next_scheduled( PRV_CRON_HOOK ) ) {
PRV_Cron::schedule();
}
if ( ! wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK ) ) {
PRV_Prune_Cron::schedule();
}
}
}

View File

@@ -17,15 +17,13 @@ declare(strict_types=1);
* Who triggers: plugins_loaded action (pr-vision.php). * Who triggers: plugins_loaded action (pr-vision.php).
* Dependencies: PRV_Upgrader, PRV_Cron, PRV_Admin_Page, PRV_Settings_Page, * Dependencies: PRV_Upgrader, PRV_Cron, PRV_Admin_Page, PRV_Settings_Page,
* PRV_Settings_Controller, PRV_Costs_Page, PRV_Call_Log_Page, * PRV_Settings_Controller, PRV_Costs_Page, PRV_Call_Log_Page,
* PRV_Call_Detail_Ajax, PRV_Prune_Cron, PRV_Cron_Guard, * PRV_Call_Detail_Ajax, PRV_Prune_Cron, PRV_Collector_Registry.
* PRV_Collector_Registry.
* *
* @see ARCHITECTURE.md -- Boot sequence diagram. * @see ARCHITECTURE.md -- Boot sequence diagram.
* @see class-prv-upgrader.php -- Runs migrations on every boot. * @see class-prv-upgrader.php -- Runs migrations on every boot.
* @see class-prv-costs-page.php -- [v0.3.0] Costs admin sub-page. * @see class-prv-costs-page.php -- [v0.3.0] Costs admin sub-page.
* @see class-prv-call-log-page.php -- [v0.3.0] Call Log admin sub-page. * @see class-prv-call-log-page.php -- [v0.3.0] Call Log admin sub-page.
* @see class-prv-prune-cron.php -- [v0.3.0] Daily prune cron. * @see class-prv-prune-cron.php -- [v0.3.0] Daily prune cron.
* @see class-prv-cron-guard.php -- [v0.3.1] Self-healing cron guard.
* @package PrVision * @package PrVision
*/ */
class PRV_Plugin { class PRV_Plugin {
@@ -52,9 +50,6 @@ class PRV_Plugin {
$prune = new PRV_Prune_Cron(); $prune = new PRV_Prune_Cron();
$prune->register_hooks(); $prune->register_hooks();
$guard = new PRV_Cron_Guard();
$guard->register_hooks();
if ( is_admin() ) { if ( is_admin() ) {
$page = new PRV_Admin_Page(); $page = new PRV_Admin_Page();
$page->register_hooks(); $page->register_hooks();

View File

@@ -3,7 +3,7 @@
* Plugin Name: PR Vision * Plugin Name: PR Vision
* Plugin URI: https://peptiderepo.com * Plugin URI: https://peptiderepo.com
* Description: Weekly server-side LLM probes for AI-visibility (GEO) tracking. Records whether peptiderepo.com is cited by LLMs across core peptides, stores time-series, and renders an admin dashboard + settings UI. * Description: Weekly server-side LLM probes for AI-visibility (GEO) tracking. Records whether peptiderepo.com is cited by LLMs across core peptides, stores time-series, and renders an admin dashboard + settings UI.
* Version: 0.3.1 * Version: 0.3.0
* Author: peptiderepo * Author: peptiderepo
* Author URI: https://peptiderepo.com * Author URI: https://peptiderepo.com
* License: GPL-2.0-or-later * License: GPL-2.0-or-later
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
/* ── Constants ────────────────────────────────────────────────────────── */ /* ── Constants ────────────────────────────────────────────────────────── */
define( 'PRV_VERSION', '0.3.1' ); define( 'PRV_VERSION', '0.3.0' );
define( 'PRV_PLUGIN_FILE', __FILE__ ); define( 'PRV_PLUGIN_FILE', __FILE__ );
define( 'PRV_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PRV_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'PRV_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'PRV_PLUGIN_URL', plugin_dir_url( __FILE__ ) );

View File

@@ -27,7 +27,7 @@ if ( ! defined( 'ARRAY_N' ) ) {
define( 'ARRAY_N', 'ARRAY_N' ); define( 'ARRAY_N', 'ARRAY_N' );
} }
define( 'PRV_VERSION', '0.3.1' ); define( 'PRV_VERSION', '0.3.0' );
define( 'PRV_PLUGIN_FILE', __DIR__ . '/../pr-vision.php' ); define( 'PRV_PLUGIN_FILE', __DIR__ . '/../pr-vision.php' );
define( 'PRV_PLUGIN_DIR', realpath( __DIR__ . '/..' ) . '/' ); define( 'PRV_PLUGIN_DIR', realpath( __DIR__ . '/..' ) . '/' );
define( 'PRV_PLUGIN_URL', 'http://example.test/wp-content/plugins/pr-vision/' ); define( 'PRV_PLUGIN_URL', 'http://example.test/wp-content/plugins/pr-vision/' );

View File

@@ -1,96 +0,0 @@
<?php
/**
* Tests for PRV_Cron_Guard: self-healing cron scheduling.
*
* @package PrVision
*/
declare(strict_types=1);
require_once __DIR__ . '/../bootstrap.php';
echo "=== PRV Cron Guard Tests ===\n";
// ── Test: ensure_scheduled schedules prv_weekly_probe when missing ──────
prv_test_reset();
prv_assert( false === wp_next_scheduled( PRV_CRON_HOOK ), 'guard/weekly: not scheduled before call' );
prv_assert( false === wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK ), 'guard/prune: not scheduled before call' );
$guard = new PRV_Cron_Guard();
$guard->ensure_scheduled();
prv_assert( false !== wp_next_scheduled( PRV_CRON_HOOK ), 'guard/weekly: prv_weekly_probe scheduled after ensure_scheduled()' );
// ── Test: ensure_scheduled schedules prv_daily_prune when missing ────────
prv_assert( false !== wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK ), 'guard/prune: prv_daily_prune scheduled after ensure_scheduled()' );
// ── Test: idempotent -- called twice, still only one event each ──────────
$ts_weekly_1 = wp_next_scheduled( PRV_CRON_HOOK );
$ts_prune_1 = wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK );
$guard->ensure_scheduled(); // Second call.
$ts_weekly_2 = wp_next_scheduled( PRV_CRON_HOOK );
$ts_prune_2 = wp_next_scheduled( PRV_Prune_Cron::PRUNE_HOOK );
prv_assert( $ts_weekly_1 === $ts_weekly_2, 'guard/idempotent: weekly event timestamp unchanged after second call' );
prv_assert( $ts_prune_1 === $ts_prune_2, 'guard/idempotent: prune event timestamp unchanged after second call' );
// Verify the test state has exactly one entry per hook (no duplicates).
$cron_events = $GLOBALS['prv_test_state']['cron_events'];
prv_assert( array_key_exists( PRV_CRON_HOOK, $cron_events ), 'guard/idempotent: exactly one weekly event entry' );
prv_assert( array_key_exists( PRV_Prune_Cron::PRUNE_HOOK, $cron_events ), 'guard/idempotent: exactly one prune event entry' );
prv_assert( 2 === count( $cron_events ), 'guard/idempotent: only two cron event entries total' );
// ── Test: guard uses same recurrence as canonical schedulers ─────────────
prv_test_reset();
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
PRV_Cron::schedule();
$canonical_weekly = $GLOBALS['prv_test_state']['cron_events'][ PRV_CRON_HOOK ]['schedule'] ?? null;
prv_test_reset();
PRV_Prune_Cron::schedule();
$canonical_prune = $GLOBALS['prv_test_state']['cron_events'][ PRV_Prune_Cron::PRUNE_HOOK ]['schedule'] ?? null;
// Now verify guard-scheduled events share the same recurrences.
prv_test_reset();
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
$guard2 = new PRV_Cron_Guard();
$guard2->ensure_scheduled();
$guard_weekly = $GLOBALS['prv_test_state']['cron_events'][ PRV_CRON_HOOK ]['schedule'] ?? null;
$guard_prune = $GLOBALS['prv_test_state']['cron_events'][ PRV_Prune_Cron::PRUNE_HOOK ]['schedule'] ?? null;
prv_assert_equals( $canonical_weekly, $guard_weekly, 'guard/recurrence: weekly recurrence matches canonical PRV_Cron::schedule()' );
prv_assert_equals( $canonical_prune, $guard_prune, 'guard/recurrence: prune recurrence matches canonical PRV_Prune_Cron::schedule()' );
// ── Test: no-op when both events already scheduled ───────────────────────
prv_test_reset();
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
PRV_Cron::schedule();
PRV_Prune_Cron::schedule();
$before = $GLOBALS['prv_test_state']['cron_events'];
$guard3 = new PRV_Cron_Guard();
$guard3->ensure_scheduled();
$after = $GLOBALS['prv_test_state']['cron_events'];
prv_assert( $before === $after, 'guard/noop: cron_events unchanged when all events already scheduled' );
// ── Test: register_hooks adds the init action ────────────────────────────
prv_test_reset();
$guard4 = new PRV_Cron_Guard();
$guard4->register_hooks();
$hooks = array_column( $GLOBALS['prv_test_state']['actions'], 'hook' );
prv_assert( in_array( 'init', $hooks, true ), 'guard/register_hooks: init action registered' );
exit( prv_test_summary() );