Files
pr-vision/CHANGELOG.md
peptiderepo 39dbe8fcc1 Adopt reusable CI + PHPUnit + fold onto shared deploy (#9)
* v0.3.2 — adopt estate CI: PHPUnit suite + reusable pipeline + governance workflows

- Convert 17 hand-rolled tests to PHPUnit TestCases (tests: stubs pattern)
  P0 security tests preserved: key-leak/encryption (KeyStoreTest),
  capture-allowlist/executor-path sentinel (CaptureWriterTest)
- Add phpunit.xml.dist + update tests/bootstrap.php (Composer autoloader first)
- Add phpunit/phpunit ^9.6 + yoast/phpunit-polyfills ^2.0 to require-dev
- composer test -> vendor/bin/phpunit --configuration phpunit.xml.dist
- ci.yml: thin caller of peptide-e2e reusable CI (tests: stubs, has_js: true,
  workflow_call trigger, permissions: contents: write)
- deploy.yml: estate pattern caller of peptide-e2e deploy-app.yml@main
  (target_path: wp-content/plugins/pr-vision, gated on reusable CI)
  Pull-deploy on Hostinger stays as break-glass
- Add cto-review.yml (Haiku-only, A/B panel dropped 2026-06-12)
- Add main-push-audit.yml (direct-push tripwire)

Agent-Session: claude-adopt-ci-20260616

* style: auto-fix PHPCS violations via phpcbf

* fix: isolate sentinel-constant test + disable js-lint

Two CI failures fixed:

1. PHPUnit constant-scoping bleed: test_p0_executor_path_sentinel_key_does_not_
   leak defines PRV_OPENROUTER_API_KEY as a process-scoped PHP constant.
   Running after CaptureWriter (alphabetically earlier), KeyStoreTest's
   resolver tests saw the canary value instead of empty/option.
   Fix: @runInSeparateProcess + @preserveGlobalState disabled so the
   constant is confined to its own forked PHP process.

2. JS lint job failure: has_js: true caused the reusable CI's lint-js job
   to attempt npm ci, but pr-vision has no package.json.
   Fixed: has_js: false.

Agent-Session: claude/adopt-ci-20260616

* fix: resolve all PHPUnit constant-scoping + crypto + collector test failures

Root causes identified and fixed:

1. Missing salts in bootstrap: derive_encryption_key() returned '' (no 32-byte
   key) because AUTH_KEY/SECURE_AUTH_KEY were undefined. All crypto group tests
   in KeyStoreTest failed as a result. Fix: define AUTH_KEY + SECURE_AUTH_KEY
   in tests/bootstrap.php with deterministic 64-char test salts.

2. PHP constant mid-run redefinition: ConfigVersionTest.setUp() defined
   PRV_OPENROUTER_API_KEY='sk-or-test-key', which bled into KeyStoreTest
   (runs after ConfigVersionTest alphabetically). Same constant was defined in
   5 test classes (ConfigVersion, ProbeRunner, ProjectedCost, RunLock, Uninstall).
   Fix: define PRV_OPENROUTER_API_KEY once in bootstrap; remove per-class defines.
   Update KeyStoreTest expectations to reflect constant-is-always-set reality:
   get_source() => SOURCE_CONSTANT, get_key() => 'sk-or-test-key'.

3. Remaining @runInSeparateProcess complexity: CaptureWriterTest sentinel test
   was using @runInSeparateProcess to avoid constant bleed, but that caused
   error_log() calls inside the executor to surface as PHPUnit\Framework\Exception
   in the child process output. Now that constant is defined in bootstrap, the
   sentinel test runs in the main process and just asserts 'sk-or-test-key'
   doesn't appear in stored rows (equivalent P0 security coverage).

4. DashboardPanelTest collector calls get_results() twice (trendline + standings).
   The wpdb stub returned the same array for both calls; the second call expected
   peptide_slug keys but got trendline keys, causing 'Undefined array key' error.
   Fix: add wpdb_results_queue support to the stub (shifts one result set per
   call when queue is non-empty); update collector tests to use the queue.

Agent-Session: claude/adopt-ci-20260616

* fix: update KeyStoreTest Group 2 for always-defined constant

Group 2 tests (test_get_source_none_when_no_key, test_store_key_writes_encrypted_option,
test_clear_key_removes_option) were still expecting SOURCE_NONE and SOURCE_OPTION.
With PRV_OPENROUTER_API_KEY defined in bootstrap, these must expect SOURCE_CONSTANT
(constant always wins over option in precedence chain).

- Renamed test_get_source_none_when_no_key -> test_get_source_constant_when_constant_defined
- Updated test_store_key_writes_encrypted_option: SOURCE_OPTION -> SOURCE_CONSTANT assertion
- Updated test_clear_key_removes_option: SOURCE_NONE -> SOURCE_CONSTANT assertion

Agent-Session: claude/adopt-ci-20260616

---------

Co-authored-by: peptiderepo <peptiderepo@users.noreply.github.com>
2026-06-17 01:16:37 +07:00

24 KiB
Raw Permalink Blame History

Changelog — PR Vision

All notable changes to this project will be documented in this file.

Format: Keep a Changelog Versioning: Semantic Versioning


[0.3.2] — 2026-06-16

CI/CD adoption: PHPUnit test suite (WP stubs), estate reusable pipeline, and governance workflows.

Added

  • PHPUnit test suite (tests: stubs): 17 hand-rolled test files converted to 17 tests/unit/*Test.php PHPUnit TestCases. All security assertions preserved (P0 key-leak / encryption / capture-allowlist tests in CaptureWriterTest, KeyStoreTest). composer test runs vendor/bin/phpunit.
  • phpunit.xml.dist — PHPUnit 9.x configuration; discovers tests/unit/*Test.php.
  • tests/bootstrap.php updated to load Composer autoloader before stubs (PHPUnit stubs pattern; no WP install required).
  • composer.json — added phpunit/phpunit ^9.6 and yoast/phpunit-polyfills ^2.0 to require-dev; added composer test script.
  • .github/workflows/ci.yml — thin caller of peptiderepo/peptide-e2e/.github/workflows/ci.yml@main (tests: stubs, has_js: true, workflow_call trigger, permissions: contents: write). Replaces the hand-rolled inline CI.
  • .github/workflows/deploy.yml — estate pattern: thin caller of peptiderepo/peptide-e2e/.github/workflows/deploy-app.yml@main with target_path: wp-content/plugins/pr-vision, gated on the reusable CI job. Replaces the standalone inline rsync. Pull-deploy on Hostinger stays as break-glass.
  • .github/workflows/cto-review.yml — Haiku-only automated PR reviewer (estate standard, A/B panel dropped 2026-06-12).
  • .github/workflows/main-push-audit.yml — direct-push-to-main audit tripwire (estate standard).

[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

Per-call cost accountability + LLM call audit trail. Every probe call now produces an inspectable record with cost, tokens, latency, and the exact rendered prompt + raw response. Two new admin pages (Costs, Call Log) with a non-modal detail drawer. Raw I/O is pruned on a configurable retention window (default 90 days); cost/metadata kept indefinitely.

Fixed (pre-launch P2 cleanup)

  • io_captured TINYINT(1) NOT NULL DEFAULT 0 column added to prv_call_meta CREATE (baked into initial schema — table is new in v0.3.0). Set to 1 by PRV_Capture_Writer::write_io() on success, remains 0 for error rows and legacy/pre-feature rows. Drawer state now driven by io_captured instead of the config_version < 3 heuristic: 0 = Legacy/not-captured; 1 + io row present = Normal; 1 + io row absent = Pruned (aged out). Removes a fragile inference.

Security (P0)

  • PRV_Gateway_Client::post_to_gateway() — removed response body from RuntimeException message; exception now carries HTTP status code only. Closes the pre-existing 401-body → error_log → key-leak vector.
  • PRV_Capture_Writer — allowlist-only capture; NEVER receives $headers, Authorization, or any raw HTTP request object. Structurally impossible for a key to enter a stored row.
  • Mandatory capture ordering enforced in PRV_Probe_Run_Executor: can_afford → probe → persist_result → update_row_cost → write_meta → capture_io. capture_io is last and wrapped in a swallowing try/catch.

Added

  • PRV_Call_Meta_Table — per-call cost/metadata table (kept indefinitely).
  • PRV_Call_Io_Table — per-call I/O table (pruned on retention window).
  • PRV_Capture_Writer — writes metadata + I/O rows; allowlist-only.
  • 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 call-log with filters; per-call I/O fetch.
  • PRV_Costs_Page — "PR Vision > Costs" admin sub-page.
  • PRV_Costs_Renderer — cap progress, bento, model table, drill-down; includes render_subnav() shared helper.
  • PRV_Call_Log_Page — "PR Vision > Call Log" admin sub-page with filter bar.
  • PRV_Call_Log_Table_Renderer — paginated table; cited% 0 = muted, >0 = lime; WCAG AA pagination.
  • PRV_Call_Drawer_Renderer — non-modal role="complementary" drawer; 5 states (Normal, Pruned, Legacy, Failed, Loading). Focus-managed; Esc closes.
  • PRV_Call_Drawer_Script — inline JS for drawer open/close/fetch/copy split for 300-line compliance.
  • PRV_Call_Detail_Ajax — wp_ajax_prv_call_detail AJAX handler for drawer content.
  • PRV_Admin_Page_Css — CSS provider split from PRV_Admin_Page for 300-line compliance.
  • PRV_Probe_Run_Executor — inner probe loop split from PRV_Probe_Runner.
  • PRV_Probe_Result — added tokens_in/tokens_out params + getters.
  • Both providers — report tokens_in/tokens_out from usage.prompt_tokens/completion_tokens.
  • PRV_Config::get_io_retention_days() — reads prv_io_retention_days option.
  • Retention days field added to Settings form + saved by controller.
  • Subnav 4-tab strip added to all PR Vision pages (Dashboard | Costs | Call Log | Settings).
  • Constants PRV_IO_RETENTION_DEFAULT_DAYS, PRV_DAILY_PRUNE_HOOK.
  • Tests: test-capture-writer.php, test-cost-audit.php.

Changed

  • PRV_Probe_Runner — slimmed to lock + UUID + delegation; inner loop moved to PRV_Probe_Run_Executor. Constructor now also accepts PRV_Capture_Writer.
  • PRV_Upgrader::run() — creates both new tables on every plugins_loaded.
  • PRV_Activator::activate() — creates new tables + schedules prv_daily_prune cron.
  • PRV_Deactivator::deactivate() — clears prv_daily_prune cron.
  • PRV_Plugin::init() — registers PRV_Costs_Page, PRV_Call_Log_Page, PRV_Call_Detail_Ajax, PRV_Prune_Cron.
  • PRV_Settings_Renderer — adds subnav + retention days field.
  • PRV_Settings_Controller::handle_save() — saves prv_io_retention_days.
  • uninstall.php — DROPs prv_call_io + prv_call_meta; clears prv_daily_prune cron.
  • Schema version bumped to 3.

Security

  • See P0 section above.

[0.2.3] — 2026-06-15

UI-editable provider API key: encrypted at rest, write-only, wp-config precedence. Output-escaping hardening in key manager renderer (esc_url/esc_attr/literal-conditional on all echoed values).

Added

  • PRV_Key_Store (includes/core/class-prv-key-store.php): single key resolver with precedence chain — PRV_OPENROUTER_API_KEY constant → encrypted admin option prv_provider_key_enc → none. get_key() is the sole key-access point for all probe and test paths.
  • PRV_Crypto_Helper (includes/core/class-prv-crypto-helper.php): low-level symmetric encryption/decryption. Prefers libsodium sodium_crypto_secretbox (XSalsa20-Poly1305); falls back to OpenSSL AES-256-GCM. Encryption key is derived from WP salts (AUTH_KEY + SECURE_AUTH_KEY) via SHA-256 — environment-specific, never stored. Storage format: hex(nonce):hex(ciphertext).
  • PRV_Key_Manager_Renderer (includes/core/class-prv-key-manager-renderer.php): renders the write-only "Provider API Key" card in Settings. Shows source status only ("Set via wp-config (takes precedence)" / "Set via admin" / "Not set") plus last-run health. Password input always renders empty. Constant defined → input disabled with note.
  • PRV_Key_Test_Ajax (includes/core/class-prv-key-test-ajax.php): AJAX handler for the "Test key" button. Resolves key via PRV_Key_Store::get_key(), runs one cheap probe, reports valid/invalid with aria-live result; key never in response.
  • Key set/remove POST actions in PRV_Settings_Controller: handle_key_set() (encrypts + stores; empty input = no-op) and handle_key_remove().
  • prv_key_set, prv_key_remove, prv_key_error notices in PRV_Settings_Renderer.
  • wp_ajax_prv_test_key hook registered in PRV_Settings_Page → dispatches to PRV_Key_Test_Ajax.
  • Tests (tests/unit/test-key-store.php): encrypt↔decrypt round-trip, wrong-key fails silently, resolver precedence (constant > option > none), write-only render emits no key, Remove clears option, constant takes precedence.

Changed

  • PRV_OpenRouter_Provider: removed direct PRV_OPENROUTER_API_KEY constant reads; probe() now calls PRV_Key_Store::get_key(). New probe_with_key(string $q, string $key) method for the test path (PRV_Key_Test_Ajax injects the resolved key). is_configured() now checks PRV_Key_Store::get_key() !== ''.
  • PRV_Perplexity_Provider: same key-resolver migration — probe() calls PRV_Key_Store::get_key(). is_configured() checks the store. Direct constant reads removed.
  • PRV_Model_Test_Ajax: resolves key via PRV_Key_Store::get_key() instead of checking the constant directly. Returns a specific "key not configured — set via Settings" message when no key is available.
  • PRV_Settings_Renderer: replaced read-only render_api_key_status() with a call to PRV_Key_Manager_Renderer::render(). Added key-action notices. Provider API Key card is now separate from the Probe Configuration form.
  • PRV_Settings_Page: added NONCE_KEY + NONCE_KEY_TEST constants; registered admin_post_prv_key_set, admin_post_prv_key_remove, wp_ajax_prv_test_key.
  • uninstall.php: updated comment to explicitly list prv_provider_key_enc as a v0.2.3 addition purged by the prv_ wildcard DELETE.
  • tests/unit/test-uninstall.php: seeds + asserts prv_provider_key_enc purge.

Security

  • Plaintext API key never stored in any option, transient, log, or echoed to the browser.
  • The encrypted option (prv_provider_key_enc) is ciphertext only; decryption is server-side at probe time only. The encryption key is derived from WP salts — not stored.
  • manage_options + nonce required for all key actions (set, remove, test).
  • wp-config constant always takes precedence; admin path is only active when absent.

[0.2.2] — 2026-06-14

Kill white WP admin gutters around the dark "Assay" UI — CSS only, no logic change.

Fixed

  • Admin chrome (dashboard + settings): #wpcontent, #wpbody, and #wpbody-content now receive the dark page background (#14181C) on both PR Vision screens. WP's default 20px left padding on #wpcontent is removed so no light gutter appears between the left menu and the dark content area.
  • WP admin bar / first-element gap: #wpbody dark fill removes the white strip between the admin bar and the first card.
  • Page <h1> header: .wrap > h1 colour set to the light text token (#EEF2F5) so the dashboard heading renders on dark, not white.
  • Proxy-note banner (dashboard): .notice.prv-proxy-note overridden to dark surface (#1C2228) with teal left border — banner now sits on dark bg matching the surrounding cards.
  • Scope: entire CSS block delivered via wp_add_inline_style('wp-admin', …) inside PRV_Admin_Page::enqueue_assets(), which is already guarded by strpos($hook, 'pr-vision'). Styles are absent on every other WP admin page. WP admin bar and left menu are not touched.

[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

  • 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).
  • Seed config: 12 peptides, 3 prompt intents, Perplexity sonar + GPT-4o-search + Gemini-2.0-Flash.

[0.2.0] — 2026-06-14

Admin/Settings interface + correctness fixes. All adversarial-QA must-fixes addressed.

Added

  • Model manager (prv_models v2): CRUD for models via Settings page — add, enable/disable, remove, edit, without a code deploy. Per-model: provider, slug, enabled flag, operator note, run-health, Test button.
  • PRV_Model_Registry: manages the prv_models option in v2 rich-object format; migration from v0.1.x flat-string format is versioned, idempotent, and tested.
  • PRV_Upgrader: runs all pending migrations on every plugins_loaded (not just activation) so live-upgraded installs get migrated automatically.
  • PRV_Config_Version: stamps every run with the scored config (models × peptides × intents); bumps on scoring-relevant saves; get_all_versions() for trendline annotations.
  • PRV_Run_Lock: transient-based distributed lock preventing concurrent cron + "Run now" executions; acquire() / release() / is_locked().
  • PRV_Settings_Page: "PR Vision → Settings" sub-menu with POST handlers for settings save, model CRUD, Run now, Test model AJAX. All actions manage_options + nonce gated.
  • PRV_Settings_Renderer: renders the dark "Assay" settings page (CSS variables, sticky save-bar with scoring-change warning, projected cost inset, API-key status, Run now).
  • PRV_Model_Manager_Table: renders the model manager table with health badges, keyboard-sortable <button aria-sort> headers, disabled-toggle for retired rows, Test chips with aria-live.
  • PRV_Model_Test_Ajax: rate-limited AJAX handler for point-in-time model-slug validation.
  • Per-model run-health: PRV_Model_Registry::update_health() called at end of each run; "Retired?" badge when probed=0 && errors>0.
  • Metric comparability: config-version stamp on every DB row (config_version INT column); bump + warning on scoring-relevant save; break annotation hook for dashboard trendline.
  • Projected cost at save: PRV_Config::get_projected_cost() returns per_run / per_month / probe_count / over_cap; displayed live on settings page.
  • Budget truncation state: prv_last_run_truncated option set when a run hits the cap mid-way; surfaced in run-done notices and dashboard data.
  • Cadence reschedule: PRV_Cron::reschedule() clears + re-adds the WP-Cron event on cadence change; next-run timestamp shown.
  • API-key three-state: "Not defined" / "Defined — last run OK" / "Defined — last run failed" derived from prv_api_key_status ledger option; never displays the key.
  • Design criticals (v2 mockups): dark ink focus ring on bright fills; disabled-row text at full AA (no opacity collapse), toggle genuinely disabled; save-bar-level warning with role=alert; "Not yet" chip #B4BDC7 (6.83:1 AA); keyboard-operable sortable headers (<button> + aria-sort); aria-live="polite" on Test chip + Run-now status; sticky save-bar z-index:50; Run-now double-fire guard; @media print light overrides.
  • New unit tests: test-model-registry.php (migration, CRUD, health), test-run-lock.php (acquire/release/sentinel), test-config-version.php (hash, bump, stamp), test-projected-cost.php (probe_count, over_cap, truncation), test-cron-reschedule.php (reschedule, idempotent, tick guard).

Changed

  • PRV_Config::get_models() now delegates to PRV_Model_Registry::get_enabled_slugs().
  • PRV_Config::seed_defaults() no longer seeds prv_models directly (handled by PRV_Upgrader).
  • PRV_Config adds CADENCE_KEY constant, get_cadence(), get_projected_cost().
  • PRV_Probe_Runner::run() acquires run-lock, stamps config_version on each row, updates model health, sets truncation option, returns truncated + run_id keys.
  • PRV_Table_Manager::create_table() adds config_version INT NULL column; schema version bumped to 2.
  • PRV_Cron adds reschedule(), next_run_timestamp(), cadence-aware schedule(); handle_cron_tick() skips if run-lock held.
  • PRV_Plugin::init() boots PRV_Upgrader::run() first and registers PRV_Settings_Page.
  • uninstall.php also purges prv_ transients (run-lock etc.).
  • Version bumped to 0.2.0.

CI

  • P1-A: Removed function defined() and function constant() declarations from tests/bootstrap.php — they shadowed PHP built-ins and caused Cannot redeclare fatal on PHP 8.x, blocking all unit-test CI steps.
  • P1-B: Re-escaped pre-escaped variables at echo site in class-prv-model-manager-table.php; added targeted phpcs:ignore with justification for render_health_badge() (returns pre-escaped HTML).
  • P1-C: Added phpcs:disable/enable WordPress.Security.NonceVerification.Missing around $_POST blocks in all four handle_*() methods in class-prv-settings-page.php; nonce is verified via require_admin_nonce() private helper.
  • P1-D/E: Expanded inline associative arrays to multi-line in class-prv-config.php, class-prv-probe-runner.php, class-prv-settings-renderer.php, class-prv-model-test-ajax.php; fixed param-comment spacing in class-prv-model-registry.php; capitalised docblock long-description first word.
  • PHPCS compliance: Expanded remaining 5 inline associative arrays in class-prv-settings-page.php (add_query_arg calls + PRV_Model_Registry::update data array) to multi-line with trailing comma; corrected @param column alignment in class-prv-probe-runner.php:update_api_key_status and class-prv-model-registry.php:update_health to match longest-type rule. PHPCS: 0 errors / 0 warnings.
  • 300-line split: extracted handle_save, handle_model_add, handle_model_update, handle_model_remove from class-prv-settings-page.php (346 lines) into new class-prv-settings-controller.php (PRV_Settings_Controller); settings-page delegates via nonce callback. All phpcs:disable/enable WordPress.Security.NonceVerification.Missing wrappers carried with the moved handlers. Both files now ≤213 lines; no file exceeds 300 lines.

[0.2.1] — 2026-06-14

Dashboard visuals + deploy pipeline.

Added

  • KPI bento bar on dashboard: Three-tile band replacing the flat meta-bar. Score tile carries a run-health pill (dot + word: "All models healthy" / "N model(s) degraded" / "No run yet") sourced from PRV_Model_Registry health fields via PRV_Ai_Visibility_Collector::collect_model_health(). Cost tile carries the MTD meter + optional truncation badge. Last-run tile shows timestamp.
  • Config-change trendline marker: Inline Chart.js plugin (configMarker) draws a vertical dashed orange line + pin circle at every config_version boundary in the trendline data. A legend swatch and a plain-language annotation note appear below the chart when breaks exist.
  • PRV_Dashboard_Renderer: New class (includes/panel/class-prv-dashboard-renderer.php) owns the bento tiles and trendline rendering; PRV_Ai_Visibility_Panel delegates to it. Split keeps all files ≤ 300 lines.
  • Collector extensions: PRV_Ai_Visibility_Collector::collect() now returns two new keys: last_run_counts (per-model health map from PRV_Model_Registry) and config_versions (all version records from PRV_Config_Version). The trendline query now includes MIN(config_version) per run_id.
  • Admin page CSS: PRV_Admin_Page::get_dashboard_css() returns the "Assay" dark palette CSS (CSS custom-property palette, bento grid, health pills, chart legend swatches, cfg-note, standings status chips) scoped to .prv-* selectors.
  • .github/workflows/deploy.yml: Standalone rsync deploy — on push to main: runs CI as prerequisite, rsyncs the plugin to ~/domains/peptiderepo.com/public_html/wp-content/plugins/pr-vision/ (excludes .git .github tests phpunit.xml.dist composer.json composer.lock vendor phpcs.xml.dist *.md), purges LiteSpeed cache via WP-CLI, health-checks (200 or 403). Uses repo secrets SSH_HOST/SSH_USERNAME/SSH_PRIVATE_KEY/SSH_PORT. Standalone — does NOT wire into the smoke-gated shared pipeline.
  • tests/unit/test-dashboard-panel.php: 27 assertions covering: derive_health_pill_state (all states + edge cases), PRV_Dashboard_Renderer::build_delta_html (up/down/flat/null), collector config_version field on trendline rows, collector last_run_counts and config_versions keys, null config_version graceful handling, model health payload from registry, trendline output capture (marker present/absent, noscript fallback element).

Changed

  • PRV_Ai_Visibility_Panel refactored: bento and trendline rendering extracted to PRV_Dashboard_Renderer; panel is now a thin orchestrator (237 lines). No interface change.
  • PRV_Admin_Page::enqueue_assets() now calls get_dashboard_css() for the "Assay" palette CSS instead of the legacy inline style block.
  • Version bumped to 0.2.1.