fix(maintenance): v0.18.2 — four confirmed-live fixes

1. Cost-reporter SQL: phpcs:ignore annotations were embedded inside two SQL
   string literals (get_daily_spend, get_spend_by_stage), causing MariaDB to
   reject both queries and break the Metrics & Costs admin page. Moved annotations
   above the prepare() calls. Regression tests added to CostReporterTest.

2. Collector failure observability: catch block in collect_from_all_sources() now
   logs structured meta (model slug, HTTP status parsed from exception message,
   first 200 chars of error body) so outage incidents are diagnosable post-hoc.

3. run_stages.agent_role back-write: Pipeline_Runner, Article_Worker, and
   Content_Generator were calling Run_Stage_State::start() with agent_role=''.
   All start() call sites now pass Stage_Display_Map::default_agent_role(stage).
   Role written at INSERT only; ON DUPLICATE KEY UPDATE does not include
   agent_role — idempotency-key semantics on resume are unchanged.

4. Runware float deprecation: normalize_seed() applied % to a float
   (microtime(true) * 1000), triggering PHP 8 deprecation on every image
   generation. Fixed with (int) round(...) before the modulo operator.

Refs: convo/prautoblogger/threads/2026-06-pipeline-v2-phase1/25-cto-v0182-maintenance.md
Agent-Session: prab-v0182-maintenance-20260612
This commit is contained in:
peptiderepo
2026-06-12 01:42:00 +00:00
committed by peptiderepo
parent b902216497
commit 6b1908d628
9 changed files with 123 additions and 12 deletions

View File

@@ -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

View File

@@ -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 );

View File

@@ -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

View File

@@ -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",

View File

@@ -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 ) );

View File

@@ -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.
}
}

View File

@@ -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;

View File

@@ -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__ ) );

View File

@@ -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'
);
}
}