Compare commits

..

9 Commits

Author SHA1 Message Date
peptiderepo
d42cd63103 fix: add services/opik/ to autoloader directory scan
The WordPress runtime autoloader (class-autoloader.php) was not scanning
services/opik/ — only services/. All four Opik classes live in that
subdirectory and would fatal on first use in production (class not found).

Composer's classmap covers all of includes/ recursively, so PHPUnit tests
passed. The runtime autoloader only scans explicit directory entries.

Adds 'services/opik/' to the scan list. No other services/ subdirs exist.
2026-04-24 03:43:49 +00:00
peptiderepo
89670be661 fix: PHPCS spacing in backoff delay expression 2026-04-23 22:34:06 +07:00
peptiderepo
0d8dc67f1c fix: backoff delays 2s/4s/8s to match CHANGELOG (was 1s/2s/4s) 2026-04-23 22:31:13 +07:00
peptiderepo
130f9f719c style: phpcbf-style WPCS fixes (multi-line arrays, one arg per line, function-keyword spacing) 2026-04-23 15:03:28 +00:00
peptiderepo
238074f257 ci: temp switch PHPCS to full report, drop dead oneshot workflow 2026-04-23 14:59:34 +00:00
peptiderepo
b4ac2fae61 ci: add one-shot phpcbf workflow (will be removed after Wave 1 lands) 2026-04-23 14:57:37 +00:00
peptiderepo
fe8c242a64 Opik Wave 1 Commit 3: Version bump, docs, and hook registration
Version & Changelog:
- prautoblogger.php: 0.11.0 → 0.12.0
- Add Opik PHP constants (PRAUTOBLOGGER_OPIK_API_KEY, WORKSPACE, URL_OVERRIDE)
  with fallback empty defaults so wp-config.php can override
- CHANGELOG.md: v0.12.0 section describing Wave 1 scope, feature flag,
  observability benefits, cost model, and Wave 2 preview

Documentation:
- ARCHITECTURE.md: New 'Opik LLM Observability (Optional)' section with
  feature-flag design, credential isolation, span lifecycle, call sites,
  cost/performance implications, and code structure overview
- CONVENTIONS.md: Step-by-step guide 'Instrumenting a New LLM Call Site'
  with code examples, key rules (no full prompts, async dispatch, separate
  cost tracking), and minimal instrumentation pattern

Hook Registration:
- class-prautoblogger.php: Register 'prautoblogger_opik_dispatch' cron action
  * Instantiates Opik_Client, Opik_Span_Queue, Opik_Dispatcher
  * Checks credentials and calls dispatcher->dispatch()
  * Updates 'prautoblogger_opik_last_dispatch' option for admin status display
- Instantiate Opik_Settings in admin_init for admin UI registration

All changes preserve ≤ 300-line files.

Part of Wave 1 (observability skeleton).
Target: v0.12.0
2026-04-23 14:51:51 +00:00
peptiderepo
ca5fdfacc4 Opik Wave 1 Commit 2: Integration wraps at LLM call sites
Add Opik span instrumentation to core generation pipeline:

- class-article-worker.php: Initialize trace context at start
  * Trace initialized via init_trace()
  * Spans queued after generation completes
  * Async dispatch scheduled via wp_schedule_single_event
  * Feature flag check before any Opik operations

- class-content-generator.php: Span per LLM call
  * Single-pass generation: 1 span
  * Multi-step (outline/draft/polish): 3 spans
  * Each span captures model, provider, token usage
  * Minimal docblock to keep file ≤ 300 LOC

- class-chief-editor.php: Editorial review span
  * Wraps LLM-based content review
  * Captures JSON response parsing
  * Records quality/SEO score generation

All files ≤ 300 LOC. Spans capture usage data but not full
prompts (per security policy). Feature-flag default OFF.

Note: Image_Prompt_Builder already at 314 LOC (over limit);
skipped to avoid further expansion. Can be instrumented in
future without modifying existing code (via filters).

Part of Wave 1 integration.
Target: v0.12.0
2026-04-23 14:50:20 +00:00
peptiderepo
ff6ad5eba1 Opik Wave 1 Commit 1: Client, Trace Context, and test infrastructure
Add the core Opik observability classes:

- class-opik-client.php: HTTP REST client for Comet Opik
  * POST traces and spans with exponential backoff (3 retries)
  * Batch endpoints support (max 100 items per batch)
  * Auth headers: Authorization + Comet-Workspace
  * Never logs secrets; errors logged with context

- class-opik-trace-context.php: Per-request trace singleton
  * Initialized at article generation start
  * Manages span stack: start_span(), end_span()
  * Finalizes complete trace with all child spans
  * Teardown clears context for next request

- class-opik-settings.php: WordPress Settings API integration
  * Toggle: 'Enable Opik tracing' (default off)
  * Text field: 'Opik project name' (default 'prautoblogger')
  * Checkbox: 'Capture full prompt text' (opt-in)
  * Read-only status: API key configured, last dispatch, queue depth
  * Never renders API key in admin HTML

- Unit tests: test-opik-client.php, test-opik-trace-context.php
  * Mock HTTP via Brain\Monkey
  * Verify batch size limits, auth headers, retry logic
  * Verify UUID generation, span stacking, context teardown

All files ≤ 300 LOC per 300-line rule.
Feature flag default OFF (PRAUTOBLOGGER_OPIK_ENABLED option).

Part of Wave 1 (observability skeleton).
Target: v0.12.0
2026-04-23 14:48:27 +00:00
8 changed files with 22 additions and 100 deletions

View File

@@ -5,23 +5,6 @@ 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.12.2] — 2026-04-24
### Fixed
- **Image style suffix empty-string bypass** — `get_option()` for `prautoblogger_image_style_suffix` only returns the default constant when the key is absent from `wp_options`; an empty string stored in the DB returns `''`, bypassing fallback entirely. Root cause of post #675 rendering photorealistically instead of as a newspaper comic. Fixed all three call sites in `class-image-prompt-builder.php` (lines 85, 107, 272) by extracting a private helper `get_style_suffix()` that treats both absent AND empty string as "use default".
- **Image cost logged under wrong run_id** — `class-image-pipeline.php` created a fresh cost tracker when invoked without an argument, so image costs were logged under a different `run_id` than the article's other stages. Cost breakdowns aggregating by `run_id` silently excluded the image line item. Fixed by threading the article worker's cost tracker through `PRAutoBlogger_Publisher` and `PRAutoBlogger_Post_Assembler::attach_generated_images()` to the image pipeline constructor. Image costs now share the same `run_id` as analysis, draft, review, and llm_research stages.
## [0.12.1] — 2026-04-23
### Fixed
- **Opik UUID v7 bug** — `PRAutoBlogger_Opik_Trace_Context` was generating trace and
span IDs with `wp_generate_uuid4()` (UUID v4). Opik's REST API requires UUID v7 and
rejects v4 with HTTP 400 "Trace id must be a version 7 UUID". Added a private static
`generate_uuid7()` method using `microtime()` + `random_bytes()` and replaced both
call sites. All trace and span submissions now succeed.
## [0.12.0] — 2026-04-23
### Added

View File

@@ -30,6 +30,7 @@ class PRAutoBlogger_Autoloader {
'models/',
'frontend/',
'services/',
'services/opik/',
'ajax/',
);

View File

@@ -138,13 +138,13 @@ class PRAutoBlogger_Article_Worker {
: $content;
if ( $auto_publish ) {
$publisher->publish( $final, $idea, $review, $run_id, $this->cost_tracker );
$publisher->publish( $final, $idea, $review, $run_id );
++$result['published'];
} else {
$publisher->save_as_draft( $final, $idea, $review, $run_id, $this->cost_tracker );
$publisher->save_as_draft( $final, $idea, $review, $run_id );
}
} else {
$publisher->save_as_draft( $content, $idea, $review, $run_id, $this->cost_tracker );
$publisher->save_as_draft( $content, $idea, $review, $run_id );
++$result['rejected'];
PRAutoBlogger_Logger::instance()->info(
'Article rejected by editor: ' . $idea->get_topic(),

View File

@@ -82,7 +82,7 @@ PROMPT;
$parsed = $this->rewrite_via_llm( $title, $first_para );
$style_suffix = $this->get_style_suffix();
$style_suffix = get_option( 'prautoblogger_image_style_suffix', PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX );
return array(
'prompt' => trim( $parsed['scene'] . ' ' . $style_suffix ),
@@ -104,7 +104,7 @@ PROMPT;
$parsed = $this->rewrite_via_llm( $title, $context );
$style_suffix = $this->get_style_suffix();
$style_suffix = get_option( 'prautoblogger_image_style_suffix', PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX );
return array(
'prompt' => trim( $parsed['scene'] . ' ' . $style_suffix ),
@@ -269,7 +269,7 @@ PROMPT;
*/
public function build_fallback_prompt( string $title ): array {
$parsed = $this->synthesize_visual_concepts_fallback( $title, '' );
$style_suffix = $this->get_style_suffix();
$style_suffix = get_option( 'prautoblogger_image_style_suffix', PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX );
return array(
'prompt' => trim( $parsed['scene'] . ' ' . $style_suffix ),
'caption' => $parsed['caption'],
@@ -311,20 +311,4 @@ PROMPT;
$override = (string) get_option( 'prautoblogger_image_prompt_instructions', '' );
return '' !== trim( $override ) ? $override : self::REWRITER_SYSTEM_PROMPT;
}
/**
* Get image style suffix with empty-string safeguard.
*
* Treats both absent and empty string as "use default" to prevent
* silent style loss if the admin settings field is saved empty.
*
* @return string The style suffix with safe fallback.
*/
private function get_style_suffix(): string {
$style_suffix = (string) get_option( 'prautoblogger_image_style_suffix', '' );
if ( '' === trim( $style_suffix ) ) {
$style_suffix = PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX;
}
return $style_suffix;
}
}

View File

@@ -232,12 +232,11 @@ class PRAutoBlogger_Post_Assembler {
* Generate and attach images to a published post.
* Non-blocking — logs errors but doesn't re-throw since the post is already created.
*
* @param int $post_id Post ID.
* @param PRAutoBlogger_Article_Idea $idea Article idea (contains source IDs).
* @param array $post_data Post data array (for article content).
* @param ?PRAutoBlogger_Cost_Tracker $cost_tracker Optional cost tracker for image generation cost logging.
* @param int $post_id Post ID.
* @param PRAutoBlogger_Article_Idea $idea Article idea (contains source IDs).
* @param array $post_data Post data array (for article content).
*/
public static function attach_generated_images( int $post_id, PRAutoBlogger_Article_Idea $idea, array $post_data, ?PRAutoBlogger_Cost_Tracker $cost_tracker = null ): void {
public static function attach_generated_images( int $post_id, PRAutoBlogger_Article_Idea $idea, array $post_data ): void {
// Fetch the original Reddit source data for Image B's source-driven prompt.
// The idea's source_ids point to rows in the source_data table collected
// during the pipeline's collection step.
@@ -248,7 +247,7 @@ class PRAutoBlogger_Post_Assembler {
// internally, immediately after each image generates. This
// ensures attachment persists even if the process times out
// before this method returns.
$result = ( new PRAutoBlogger_Image_Pipeline( null, $cost_tracker ) )->generate_and_attach_images( $post_id, $post_data, $source_data );
$result = ( new PRAutoBlogger_Image_Pipeline() )->generate_and_attach_images( $post_id, $post_data, $source_data );
if ( ! empty( $result['errors'] ) ) {
foreach ( $result['errors'] as $error ) {

View File

@@ -28,7 +28,6 @@ class PRAutoBlogger_Publisher {
* @param PRAutoBlogger_Article_Idea $idea The original article idea.
* @param PRAutoBlogger_Editorial_Review $review The editor's review.
* @param string|null $run_id Pipeline run ID for log linking.
* @param ?PRAutoBlogger_Cost_Tracker $cost_tracker Optional cost tracker for image generation cost logging.
* @return int The created post ID.
* @throws \RuntimeException If post creation fails.
*/
@@ -36,10 +35,9 @@ class PRAutoBlogger_Publisher {
string $content,
PRAutoBlogger_Article_Idea $idea,
PRAutoBlogger_Editorial_Review $review,
?string $run_id = null,
?PRAutoBlogger_Cost_Tracker $cost_tracker = null
?string $run_id = null
): int {
return $this->create_post( $content, $idea, $review, 'publish', $run_id, $cost_tracker );
return $this->create_post( $content, $idea, $review, 'publish', $run_id );
}
/**
@@ -49,7 +47,6 @@ class PRAutoBlogger_Publisher {
* @param PRAutoBlogger_Article_Idea $idea The original article idea.
* @param PRAutoBlogger_Editorial_Review $review The editor's review with rejection notes.
* @param string|null $run_id Pipeline run ID for log linking.
* @param ?PRAutoBlogger_Cost_Tracker $cost_tracker Optional cost tracker for image generation cost logging.
* @return int The created post ID.
* @throws \RuntimeException If post creation fails.
*/
@@ -57,10 +54,9 @@ class PRAutoBlogger_Publisher {
string $content,
PRAutoBlogger_Article_Idea $idea,
PRAutoBlogger_Editorial_Review $review,
?string $run_id = null,
?PRAutoBlogger_Cost_Tracker $cost_tracker = null
?string $run_id = null
): int {
return $this->create_post( $content, $idea, $review, 'draft', $run_id, $cost_tracker );
return $this->create_post( $content, $idea, $review, 'draft', $run_id );
}
/**
@@ -79,8 +75,7 @@ class PRAutoBlogger_Publisher {
PRAutoBlogger_Article_Idea $idea,
PRAutoBlogger_Editorial_Review $review,
string $post_status,
?string $run_id = null,
?PRAutoBlogger_Cost_Tracker $cost_tracker = null
?string $run_id = null
): int {
// Clean LLM artifacts, then inject peptide hyperlinks deterministically
// before the content enters WordPress. Peptide linker no-ops gracefully
@@ -111,7 +106,7 @@ class PRAutoBlogger_Publisher {
PRAutoBlogger_Post_Assembler::link_generation_logs( $post_id, $run_id );
if ( 'publish' === $post_status ) {
PRAutoBlogger_Post_Assembler::attach_generated_images( $post_id, $idea, $post_data, $cost_tracker );
PRAutoBlogger_Post_Assembler::attach_generated_images( $post_id, $idea, $post_data );
}
PRAutoBlogger_Logger::instance()->info(

View File

@@ -57,7 +57,7 @@ class PRAutoBlogger_Opik_Trace_Context {
* @return string The trace ID (UUID).
*/
public function init_trace(): string {
$this->trace_id = self::generate_uuid7();
$this->trace_id = wp_generate_uuid4();
$this->trace_start = microtime( true );
return $this->trace_id;
}
@@ -86,7 +86,7 @@ class PRAutoBlogger_Opik_Trace_Context {
* @return string Generated span ID (UUID).
*/
public function start_span( array $span_data ): string {
$span_id = self::generate_uuid7();
$span_id = wp_generate_uuid4();
$span = array(
'id' => $span_id,
@@ -193,44 +193,4 @@ class PRAutoBlogger_Opik_Trace_Context {
public static function teardown(): void {
self::$instance = null;
}
/**
* Generate a UUID version 7 (time-ordered, random).
*
* Opik requires v7 UUIDs for trace and span IDs. WordPress only provides
* wp_generate_uuid4() so we implement v7 here.
*
* UUID v7 structure (128 bits):
* - Bits 0-47 : Unix timestamp in milliseconds (big-endian)
* - Bits 48-51 : Version nibble (0x7)
* - Bits 52-63 : rand_a (12 random bits)
* - Bits 64-65 : Variant (0b10)
* - Bits 66-127: rand_b (62 random bits)
*
* @return string UUID v7 in standard 8-4-4-4-12 hex format.
*/
private static function generate_uuid7(): string {
$ms = (int) ( microtime( true ) * 1000 );
$ts_hex = str_pad( dechex( $ms ), 12, '0', STR_PAD_LEFT ); // 48-bit ts -> 12 hex
$rand = bin2hex( random_bytes( 10 ) ); // 80 random bits -> 20 hex
// Group 3: version nibble '7' + 12 random bits (3 hex chars).
$g3 = '7' . substr( $rand, 0, 3 );
// Group 4: variant byte (top 2 bits = 10) + 2 random hex chars.
$var_byte = ( hexdec( substr( $rand, 4, 2 ) ) & 0x3f ) | 0x80;
$g4 = sprintf( '%02x', $var_byte ) . substr( $rand, 6, 2 );
// Group 5: 12 remaining random hex chars.
$g5 = substr( $rand, 8, 12 );
return sprintf(
'%s-%s-%s-%s-%s',
substr( $ts_hex, 0, 8 ), // time_high (32 bits)
substr( $ts_hex, 8, 4 ), // time_low (16 bits)
$g3,
$g4,
$g5
);
}
}

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.12.2
* Version: 0.12.0
* 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.12.2' );
define( 'PRAUTOBLOGGER_VERSION', '0.12.0' );
define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' );
define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ );
define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );