diff --git a/CHANGELOG.md b/CHANGELOG.md index c3a17f2..82ca62d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,33 @@ All notable changes to PRAutoBlogger will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project uses [Semantic Versioning](https://semver.org/). +## [0.18.2] - 2026-06-12 + +Four confirmed-live maintenance fixes (CTO thread `2026-06-pipeline-v2-phase1` seq 25). + +### Fixed +- **Cost-reporter SQL (P2)**: `phpcs:ignore` annotations were embedded inside two SQL string + literals in `class-cost-reporter.php` (`get_daily_spend` and `get_spend_by_stage`). MariaDB + received the comment text as literal SQL and rejected both queries, breaking the Metrics & + Costs admin page. Annotations moved above the `$wpdb->prepare()` calls. Regression tests + added to `CostReporterTest` asserting no `// phpcs:ignore` text appears in the SQL passed + to `prepare()`. +- **Collector failure observability (P2)**: `RuntimeException` catch in + `Source_Collector::collect_from_all_sources()` now logs structured meta — model slug, + HTTP status parsed from the exception message, and the first 200 chars of the error body — + so a 27-day outage like the seq-22 llm_research 404 incident is diagnosable after the fact. + No behaviour change; logging only. +- **`run_stages.agent_role` back-write (P3)**: `Pipeline_Runner`, `Article_Worker`, and + `Content_Generator` were calling `Run_Stage_State::start()` with `agent_role = ''`. All four + stage-start call sites now pass `Stage_Display_Map::default_agent_role( $stage )` so the run + timeline is self-describing. Role is written at INSERT only (the `start()` call); the + `ON DUPLICATE KEY UPDATE` path does not include `agent_role`, preserving the + `run_id+stage+agent_role+item_key` idempotency-key semantics on resume. +- **Runware float deprecation (P3)**: `normalize_seed()` in `class-runware-image-support.php` + applied the modulo operator to a float (`microtime(true) * 1000`), triggering + `Deprecated: Implicit conversion from float … to int` on every image generation. Fixed + with `(int) round(...)` before the `%` operator. + ## [0.18.1] - 2026-06-11 Empty-draft hotfix (CTO diagnosis: convo thread `2026-06-pipeline-v2-phase1` seq 12; prod run diff --git a/includes/core/class-article-worker.php b/includes/core/class-article-worker.php index 7d707be..ab56994 100644 --- a/includes/core/class-article-worker.php +++ b/includes/core/class-article-worker.php @@ -103,7 +103,7 @@ class PRAutoBlogger_Article_Worker { $review = $this->review_with_state( $editor, $content, $idea, $run_id, $item ); PRAutoBlogger_Pipeline_Status::broadcast( __( 'Saving and publishing…', 'prautoblogger' ) ); - PRAutoBlogger_Run_Stage_State::start( $run_id, 'publish', '', $item ); + PRAutoBlogger_Run_Stage_State::start( $run_id, 'publish', (string) PRAutoBlogger_Stage_Display_Map::default_agent_role( 'publish' ), $item ); $this->publish_or_draft( $content, $idea, @@ -174,7 +174,7 @@ class PRAutoBlogger_Article_Worker { return new PRAutoBlogger_Editorial_Review( $data ); } } - PRAutoBlogger_Run_Stage_State::start( $run_id, 'review', '', $item ); + PRAutoBlogger_Run_Stage_State::start( $run_id, 'review', (string) PRAutoBlogger_Stage_Display_Map::default_agent_role( 'review' ), $item ); } $review = $editor->review( $content, $idea ); diff --git a/includes/core/class-content-generator.php b/includes/core/class-content-generator.php index 18c1c50..8c67a8a 100644 --- a/includes/core/class-content-generator.php +++ b/includes/core/class-content-generator.php @@ -194,7 +194,7 @@ class PRAutoBlogger_Content_Generator { ); return $cached; } - PRAutoBlogger_Run_Stage_State::start( $this->run_id, $stage, '', $this->item_key ); + PRAutoBlogger_Run_Stage_State::start( $this->run_id, $stage, (string) PRAutoBlogger_Stage_Display_Map::default_agent_role( $stage ), $this->item_key ); } // v0.18.1: stage metadata for the provider's empty-completion diff --git a/includes/core/class-cost-reporter.php b/includes/core/class-cost-reporter.php index 845ded2..c0cccbe 100644 --- a/includes/core/class-cost-reporter.php +++ b/includes/core/class-cost-reporter.php @@ -65,8 +65,9 @@ class PRAutoBlogger_Cost_Reporter { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $results = $wpdb->get_results( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->prepare( - "SELECT DATE(created_at) as day, SUM(estimated_cost) as total_cost // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT DATE(created_at) as day, SUM(estimated_cost) as total_cost FROM {$table} WHERE created_at >= %s AND response_status = 'success' GROUP BY DATE(created_at) @@ -102,11 +103,12 @@ class PRAutoBlogger_Cost_Reporter { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $results = $wpdb->get_results( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->prepare( "SELECT stage, SUM(estimated_cost) as total_cost, SUM(prompt_tokens + completion_tokens) as total_tokens, - COUNT(*) as call_count // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + COUNT(*) as call_count FROM {$table} WHERE created_at BETWEEN %s AND %s AND response_status = 'success' GROUP BY stage", diff --git a/includes/core/class-pipeline-runner.php b/includes/core/class-pipeline-runner.php index f34f86d..426c373 100644 --- a/includes/core/class-pipeline-runner.php +++ b/includes/core/class-pipeline-runner.php @@ -185,14 +185,14 @@ class PRAutoBlogger_Pipeline_Runner { $source_labels = is_array( $enabled ) ? implode( ', ', $enabled ) : 'reddit'; /* translators: %s is a comma-separated list of enabled source names, e.g. "reddit, llm_research". */ PRAutoBlogger_Pipeline_Status::broadcast( sprintf( __( 'Collecting sources from %s…', 'prautoblogger' ), $source_labels ) ); - PRAutoBlogger_Run_Stage_State::start( $run_id, 'research' ); + PRAutoBlogger_Run_Stage_State::start( $run_id, 'research', (string) PRAutoBlogger_Stage_Display_Map::default_agent_role( 'research' ) ); ( new PRAutoBlogger_Source_Collector() ) ->set_cost_tracker( $cost_tracker ) ->collect_from_all_sources(); PRAutoBlogger_Run_Stage_State::done( $run_id, 'research' ); PRAutoBlogger_Pipeline_Status::broadcast( __( 'Analyzing topics and scoring…', 'prautoblogger' ) ); - PRAutoBlogger_Run_Stage_State::start( $run_id, 'analysis' ); + PRAutoBlogger_Run_Stage_State::start( $run_id, 'analysis', (string) PRAutoBlogger_Stage_Display_Map::default_agent_role( 'analysis' ) ); $llm = new PRAutoBlogger_OpenRouter_Provider(); $analyzer = new PRAutoBlogger_Content_Analyzer( $llm, $cost_tracker ); $analysis = $analyzer->analyze_recent_data( max( $target * 2, 6 ) ); diff --git a/includes/core/class-source-collector.php b/includes/core/class-source-collector.php index 3138696..62a486e 100755 --- a/includes/core/class-source-collector.php +++ b/includes/core/class-source-collector.php @@ -76,8 +76,33 @@ class PRAutoBlogger_Source_Collector { 'collector' ); } catch ( \Throwable $e ) { - PRAutoBlogger_Logger::instance()->error( sprintf( 'Collection %s from %s: %s', get_class( $e ), $source_type, $e->getMessage() ), 'collector' ); - // Continue with other sources — one failure shouldn't stop everything. + // Structured failure meta: model slug, HTTP status (parsed from message), + // and first ~200 chars of the error body — enables post-incident diagnosis + // without needing to re-run the failing collection. Added v0.18.2. + $config_for_log = $this->get_config_for_source( $source_type ); + $model_slug = (string) ( $config_for_log['model'] ?? '' ); + $err_msg = $e->getMessage(); + // Extract HTTP status from "OpenRouter API error (HTTP NNN): ..." pattern. + $http_status = 0; + if ( preg_match( '/HTTP (\d{3})/', $err_msg, $m ) ) { + $http_status = (int) $m[1]; + } + PRAutoBlogger_Logger::instance()->error( + sprintf( + 'Collection %s from %s: %s', + get_class( $e ), + $source_type, + $e->getMessage() + ), + 'collector', + array( + 'source' => $source_type, + 'model' => $model_slug, + 'http_status' => $http_status, + 'error_body' => mb_substr( $err_msg, 0, 200 ), + ) + ); + // Continue with other sources — one failure should not stop everything. } } diff --git a/includes/providers/class-runware-image-support.php b/includes/providers/class-runware-image-support.php index e876a24..28e9ecb 100644 --- a/includes/providers/class-runware-image-support.php +++ b/includes/providers/class-runware-image-support.php @@ -181,7 +181,8 @@ class PRAutoBlogger_Runware_Image_Support { */ public function normalize_seed( ?int $seed ): int { if ( null === $seed || $seed < 1 ) { - $derived = (int) ( ( microtime( true ) * 1000 ) % 2147483647 ); + // Cast to int before modulo — float % int is deprecated in PHP 8 (v0.18.2). + $derived = (int) round( microtime( true ) * 1000 ) % 2147483647; return $derived > 0 ? $derived : 1; } return $seed; diff --git a/prautoblogger.php b/prautoblogger.php index 35561d7..dfec06e 100644 --- a/prautoblogger.php +++ b/prautoblogger.php @@ -9,7 +9,7 @@ * Plugin Name: PRAutoBlogger * Plugin URI: https://peptiderepo.com/prautoblogger * Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics. - * Version: 0.18.1 + * Version: 0.18.2 * Requires at least: 6.0 * Requires PHP: 7.4 * Author: PeptideRepo @@ -35,7 +35,7 @@ if ( ! defined( 'ABSPATH' ) ) { | Defined here so every file in the plugin can reference paths, versions, | and limits without magic strings. */ -define( 'PRAUTOBLOGGER_VERSION', '0.18.1' ); +define( 'PRAUTOBLOGGER_VERSION', '0.18.2' ); define( 'PRAUTOBLOGGER_DB_VERSION', '1.2.0' ); define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ ); define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); diff --git a/tests/unit/Core/CostReporterTest.php b/tests/unit/Core/CostReporterTest.php index a613758..bb96082 100644 --- a/tests/unit/Core/CostReporterTest.php +++ b/tests/unit/Core/CostReporterTest.php @@ -90,4 +90,60 @@ class CostReporterTest extends BaseTestCase { $this->assertIsFloat( $utilization ); $this->assertGreaterThanOrEqual( 0.0, $utilization ); } + + /** + * Regression: get_daily_spend SQL must not embed phpcs:ignore inside the + * string literal (MariaDB would receive the comment as literal SQL text and + * reject the query). Captures the first argument passed to prepare() and + * asserts the SQL is clean. + * + * @see includes/core/class-cost-reporter.php — fixed in v0.18.2 + */ + public function test_get_daily_spend_sql_contains_no_inline_phpcs_ignore(): void { + $captured_sql = null; + $this->wpdb->method( 'prepare' )->willReturnCallback( + function ( $sql, ...$args ) use ( &$captured_sql ) { + $captured_sql = $sql; + return 'prepared'; + } + ); + $this->wpdb->method( 'get_results' )->willReturn( [] ); + + $reporter = new \PRAutoBlogger_Cost_Reporter(); + $reporter->get_daily_spend( 7 ); + + $this->assertNotNull( $captured_sql, 'prepare() was not called' ); + $this->assertStringNotContainsString( + '// phpcs:ignore', + (string) $captured_sql, + 'SQL string must not contain phpcs:ignore comment — MariaDB rejects it' + ); + } + + /** + * Regression: get_spend_by_stage SQL must not embed phpcs:ignore inside + * the string literal. + * + * @see includes/core/class-cost-reporter.php — fixed in v0.18.2 + */ + public function test_get_spend_by_stage_sql_contains_no_inline_phpcs_ignore(): void { + $captured_sql = null; + $this->wpdb->method( 'prepare' )->willReturnCallback( + function ( $sql, ...$args ) use ( &$captured_sql ) { + $captured_sql = $sql; + return 'prepared'; + } + ); + $this->wpdb->method( 'get_results' )->willReturn( [] ); + + $reporter = new \PRAutoBlogger_Cost_Reporter(); + $reporter->get_spend_by_stage( '2026-06-01', '2026-06-30' ); + + $this->assertNotNull( $captured_sql, 'prepare() was not called' ); + $this->assertStringNotContainsString( + '// phpcs:ignore', + (string) $captured_sql, + 'SQL string must not contain phpcs:ignore comment — MariaDB rejects it' + ); + } }