Initial commit: PRAutoBlogger v0.1.0 with frontend posts widget

This commit is contained in:
Terence Ong
2026-04-10 23:33:42 +07:00
parent 68c6f67bcf
commit 88d674d679
65 changed files with 2178 additions and 1084 deletions

View File

@@ -8,21 +8,21 @@ declare(strict_types=1);
* Can approve, request revisions (and produce them), or reject content.
* Rejected content is saved as a draft for human review.
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 5).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 5).
* Dependencies: LLM_Provider_Interface, Cost_Tracker.
*
* @see core/class-content-generator.php — Produces the content we review.
* @see core/class-publisher.php — Publishes approved content.
* @see ARCHITECTURE.md — Data flow step 5.
*/
class Autoblogger_Chief_Editor {
class PRAutoBlogger_Chief_Editor {
private Autoblogger_LLM_Provider_Interface $llm;
private Autoblogger_Cost_Tracker $cost_tracker;
private PRAutoBlogger_LLM_Provider_Interface $llm;
private PRAutoBlogger_Cost_Tracker $cost_tracker;
public function __construct(
Autoblogger_LLM_Provider_Interface $llm,
Autoblogger_Cost_Tracker $cost_tracker
PRAutoBlogger_LLM_Provider_Interface $llm,
PRAutoBlogger_Cost_Tracker $cost_tracker
) {
$this->llm = $llm;
$this->cost_tracker = $cost_tracker;
@@ -38,13 +38,13 @@ class Autoblogger_Chief_Editor {
* Side effects: LLM API call(s), cost logging.
*
* @param string $content The generated HTML content.
* @param Autoblogger_Article_Idea $idea The original article idea.
* @param PRAutoBlogger_Article_Idea $idea The original article idea.
*
* @return Autoblogger_Editorial_Review
* @return PRAutoBlogger_Editorial_Review
*/
public function review( string $content, Autoblogger_Article_Idea $idea ): Autoblogger_Editorial_Review {
$model = get_option( 'autoblogger_editor_model', AUTOBLOGGER_DEFAULT_EDITOR_MODEL );
$niche = get_option( 'autoblogger_niche_description', '' );
public function review( string $content, PRAutoBlogger_Article_Idea $idea ): PRAutoBlogger_Editorial_Review {
$model = get_option( 'prautoblogger_editor_model', PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL );
$niche = get_option( 'prautoblogger_niche_description', '' );
$system_prompt = $this->build_system_prompt( $niche );
$user_prompt = $this->build_review_prompt( $content, $idea );
@@ -114,11 +114,11 @@ class Autoblogger_Chief_Editor {
/**
* @param string $content
* @param Autoblogger_Article_Idea $idea
* @param PRAutoBlogger_Article_Idea $idea
*
* @return string
*/
private function build_review_prompt( string $content, Autoblogger_Article_Idea $idea ): string {
private function build_review_prompt( string $content, PRAutoBlogger_Article_Idea $idea ): string {
return sprintf(
"Review this article draft:\n\n" .
"ORIGINAL BRIEF:\n" .
@@ -142,14 +142,14 @@ class Autoblogger_Chief_Editor {
*
* @param string $content JSON response from LLM.
*
* @return Autoblogger_Editorial_Review
* @return PRAutoBlogger_Editorial_Review
*/
private function parse_review_response( string $content ): Autoblogger_Editorial_Review {
private function parse_review_response( string $content ): PRAutoBlogger_Editorial_Review {
$data = json_decode( $content, true );
if ( ! is_array( $data ) || ! isset( $data['verdict'] ) ) {
Autoblogger_Logger::instance()->error( 'Chief editor response was not valid JSON. Defaulting to rejected.', 'editor' );
return new Autoblogger_Editorial_Review( [
PRAutoBlogger_Logger::instance()->error( 'Chief editor response was not valid JSON. Defaulting to rejected.', 'editor' );
return new PRAutoBlogger_Editorial_Review( [
'verdict' => 'rejected',
'notes' => 'Editor response could not be parsed.',
'quality_score' => 0.0,
@@ -163,7 +163,7 @@ class Autoblogger_Chief_Editor {
$verdict = 'rejected';
}
return new Autoblogger_Editorial_Review( [
return new PRAutoBlogger_Editorial_Review( [
'verdict' => $verdict,
'notes' => sanitize_textarea_field( $data['notes'] ?? '' ),
'revised_content' => isset( $data['revised_content'] ) ? wp_kses_post( $data['revised_content'] ) : null,

View File

@@ -8,7 +8,7 @@ declare(strict_types=1);
* Uses a cheap model (configurable) to keep analysis costs low.
* Batches source data into manageable chunks for the LLM context window.
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 2).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 2).
* Dependencies: LLM_Provider_Interface, Cost_Tracker, WordPress $wpdb.
*
* @see providers/interface-llm-provider.php — LLM used for analysis.
@@ -16,14 +16,14 @@ declare(strict_types=1);
* @see core/class-idea-scorer.php — Consumes analysis results.
* @see ARCHITECTURE.md — Data flow step 2.
*/
class Autoblogger_Content_Analyzer {
class PRAutoBlogger_Content_Analyzer {
private Autoblogger_LLM_Provider_Interface $llm;
private Autoblogger_Cost_Tracker $cost_tracker;
private PRAutoBlogger_LLM_Provider_Interface $llm;
private PRAutoBlogger_Cost_Tracker $cost_tracker;
public function __construct(
Autoblogger_LLM_Provider_Interface $llm,
Autoblogger_Cost_Tracker $cost_tracker
PRAutoBlogger_LLM_Provider_Interface $llm,
PRAutoBlogger_Cost_Tracker $cost_tracker
) {
$this->llm = $llm;
$this->cost_tracker = $cost_tracker;
@@ -37,18 +37,18 @@ class Autoblogger_Content_Analyzer {
*
* Side effects: LLM API call, database reads/writes.
*
* @return Autoblogger_Analysis_Result[] Detected patterns.
* @return PRAutoBlogger_Analysis_Result[] Detected patterns.
*/
public function analyze_recent_data(): array {
$source_items = $this->fetch_recent_source_data();
if ( empty( $source_items ) ) {
Autoblogger_Logger::instance()->info( 'No new source data to analyze.', 'analyzer' );
PRAutoBlogger_Logger::instance()->info( 'No new source data to analyze.', 'analyzer' );
return [];
}
$niche = get_option( 'autoblogger_niche_description', '' );
$model = get_option( 'autoblogger_analysis_model', AUTOBLOGGER_DEFAULT_ANALYSIS_MODEL );
$niche = get_option( 'prautoblogger_niche_description', '' );
$model = get_option( 'prautoblogger_analysis_model', PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL );
// Build the source data summary for the LLM (trim to avoid huge contexts).
$summary = $this->build_source_summary( $source_items );
@@ -98,7 +98,7 @@ class Autoblogger_Content_Analyzer {
*/
private function fetch_recent_source_data(): array {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_source_data';
$table = $wpdb->prefix . 'prautoblogger_source_data';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
return $wpdb->get_results(
@@ -153,7 +153,7 @@ class Autoblogger_Content_Analyzer {
*/
private function get_performance_context(): string {
global $wpdb;
$scores_table = $wpdb->prefix . 'autoblogger_content_scores';
$scores_table = $wpdb->prefix . 'prautoblogger_content_scores';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$top_posts = $wpdb->get_results(
@@ -239,12 +239,12 @@ class Autoblogger_Content_Analyzer {
* @param string $content LLM response content (JSON).
* @param array<int, array<string, mixed>> $source_items Original source data for ID mapping.
*
* @return Autoblogger_Analysis_Result[]
* @return PRAutoBlogger_Analysis_Result[]
*/
private function parse_analysis_response( string $content, array $source_items ): array {
$data = json_decode( $content, true );
if ( ! isset( $data['patterns'] ) || ! is_array( $data['patterns'] ) ) {
Autoblogger_Logger::instance()->error( 'Analysis response was not valid JSON or missing patterns key.', 'analyzer' );
PRAutoBlogger_Logger::instance()->error( 'Analysis response was not valid JSON or missing patterns key.', 'analyzer' );
return [];
}
@@ -252,7 +252,7 @@ class Autoblogger_Content_Analyzer {
$results = [];
foreach ( $data['patterns'] as $pattern ) {
$results[] = new Autoblogger_Analysis_Result( [
$results[] = new PRAutoBlogger_Analysis_Result( [
'analysis_type' => sanitize_text_field( $pattern['type'] ?? 'question' ),
'topic' => sanitize_text_field( $pattern['topic'] ?? '' ),
'summary' => sanitize_textarea_field( $pattern['summary'] ?? '' ),
@@ -276,13 +276,13 @@ class Autoblogger_Content_Analyzer {
*
* Side effects: database inserts.
*
* @param Autoblogger_Analysis_Result[] $results
* @param PRAutoBlogger_Analysis_Result[] $results
*
* @return void
*/
private function store_results( array $results ): void {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_analysis_results';
$table = $wpdb->prefix . 'prautoblogger_analysis_results';
foreach ( $results as $result ) {
$row = $result->to_db_row();

View File

@@ -8,21 +8,21 @@ declare(strict_types=1);
* - single_pass: One LLM call produces the complete article (cheaper, faster).
* - multi_step: Outline → Draft → Polish pipeline (higher quality, more cost).
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 4).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 4).
* Dependencies: LLM_Provider_Interface, Cost_Tracker.
*
* @see core/class-chief-editor.php — Reviews output of this class.
* @see providers/interface-llm-provider.php — LLM used for generation.
* @see ARCHITECTURE.md — Data flow step 4.
*/
class Autoblogger_Content_Generator {
class PRAutoBlogger_Content_Generator {
private Autoblogger_LLM_Provider_Interface $llm;
private Autoblogger_Cost_Tracker $cost_tracker;
private PRAutoBlogger_LLM_Provider_Interface $llm;
private PRAutoBlogger_Cost_Tracker $cost_tracker;
public function __construct(
Autoblogger_LLM_Provider_Interface $llm,
Autoblogger_Cost_Tracker $cost_tracker
PRAutoBlogger_LLM_Provider_Interface $llm,
PRAutoBlogger_Cost_Tracker $cost_tracker
) {
$this->llm = $llm;
$this->cost_tracker = $cost_tracker;
@@ -35,23 +35,23 @@ class Autoblogger_Content_Generator {
*
* Side effects: LLM API calls (1-3 depending on mode), cost logging.
*
* @param Autoblogger_Article_Idea $idea The scored idea to generate content for.
* @param PRAutoBlogger_Article_Idea $idea The scored idea to generate content for.
*
* @return string Generated HTML content.
*
* @throws \RuntimeException On generation failure.
*/
public function generate( Autoblogger_Article_Idea $idea ): string {
$mode = get_option( 'autoblogger_writing_pipeline', 'multi_step' );
public function generate( PRAutoBlogger_Article_Idea $idea ): string {
$mode = get_option( 'prautoblogger_writing_pipeline', 'multi_step' );
$request = new Autoblogger_Content_Request(
$request = new PRAutoBlogger_Content_Request(
$idea,
$mode,
get_option( 'autoblogger_tone', 'informational' ),
absint( get_option( 'autoblogger_min_word_count', 800 ) ),
absint( get_option( 'autoblogger_max_word_count', 2000 ) ),
get_option( 'autoblogger_niche_description', '' ),
json_decode( get_option( 'autoblogger_topic_exclusions', '[]' ), true ) ?: []
get_option( 'prautoblogger_tone', 'informational' ),
absint( get_option( 'prautoblogger_min_word_count', 800 ) ),
absint( get_option( 'prautoblogger_max_word_count', 2000 ) ),
get_option( 'prautoblogger_niche_description', '' ),
json_decode( get_option( 'prautoblogger_topic_exclusions', '[]' ), true ) ?: []
);
if ( 'single_pass' === $mode ) {
@@ -64,12 +64,12 @@ class Autoblogger_Content_Generator {
/**
* Single-pass generation: one LLM call produces the complete article.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
*
* @return string Generated HTML content.
*/
private function generate_single_pass( Autoblogger_Content_Request $request ): string {
$model = get_option( 'autoblogger_writing_model', AUTOBLOGGER_DEFAULT_WRITING_MODEL );
private function generate_single_pass( PRAutoBlogger_Content_Request $request ): string {
$model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL );
$idea = $request->get_idea();
$system = $this->build_writer_system_prompt( $request );
@@ -102,12 +102,12 @@ class Autoblogger_Content_Generator {
/**
* Multi-step generation: Outline → Draft → Polish.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
*
* @return string Generated HTML content.
*/
private function generate_multi_step( Autoblogger_Content_Request $request ): string {
$model = get_option( 'autoblogger_writing_model', AUTOBLOGGER_DEFAULT_WRITING_MODEL );
private function generate_multi_step( PRAutoBlogger_Content_Request $request ): string {
$model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL );
// Step 1: Generate outline.
$outline = $this->stage_outline( $request, $model );
@@ -124,12 +124,12 @@ class Autoblogger_Content_Generator {
/**
* Multi-step stage 1: Generate an article outline.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
* @param string $model
*
* @return string The outline text.
*/
private function stage_outline( Autoblogger_Content_Request $request, string $model ): string {
private function stage_outline( PRAutoBlogger_Content_Request $request, string $model ): string {
$idea = $request->get_idea();
$prompt = sprintf(
@@ -168,13 +168,13 @@ class Autoblogger_Content_Generator {
/**
* Multi-step stage 2: Write the full draft from the outline.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
* @param string $outline
* @param string $model
*
* @return string Draft HTML content.
*/
private function stage_draft( Autoblogger_Content_Request $request, string $outline, string $model ): string {
private function stage_draft( PRAutoBlogger_Content_Request $request, string $outline, string $model ): string {
$prompt = sprintf(
"Using this outline, write the full blog post in HTML format.\n\n" .
"OUTLINE:\n%s\n\n" .
@@ -213,13 +213,13 @@ class Autoblogger_Content_Generator {
/**
* Multi-step stage 3: Polish and refine the draft.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
* @param string $draft
* @param string $model
*
* @return string Polished HTML content.
*/
private function stage_polish( Autoblogger_Content_Request $request, string $draft, string $model ): string {
private function stage_polish( PRAutoBlogger_Content_Request $request, string $draft, string $model ): string {
$prompt = "Review and polish this blog post draft. Improve:\n" .
"1. Flow and readability\n" .
"2. SEO optimization (headings, keyword placement)\n" .
@@ -249,11 +249,11 @@ class Autoblogger_Content_Generator {
/**
* Build the system prompt shared across all writing stages.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
*
* @return string
*/
private function build_writer_system_prompt( Autoblogger_Content_Request $request ): string {
private function build_writer_system_prompt( PRAutoBlogger_Content_Request $request ): string {
$niche = $request->get_niche_description();
$prompt = "You are an expert blog writer";
if ( '' !== $niche ) {
@@ -268,11 +268,11 @@ class Autoblogger_Content_Generator {
/**
* Build the user prompt for single-pass generation.
*
* @param Autoblogger_Content_Request $request
* @param PRAutoBlogger_Content_Request $request
*
* @return string
*/
private function build_single_pass_prompt( Autoblogger_Content_Request $request ): string {
private function build_single_pass_prompt( PRAutoBlogger_Content_Request $request ): string {
$idea = $request->get_idea();
return sprintf(

View File

@@ -11,15 +11,15 @@ declare(strict_types=1);
*
* Triggered by: Every class that makes an LLM API call (Content_Analyzer,
* Content_Generator, Chief_Editor, Metrics_Collector).
* Dependencies: WordPress $wpdb, Autoblogger_OpenRouter_Provider (for cost estimation).
* Dependencies: WordPress $wpdb, PRAutoBlogger_OpenRouter_Provider (for cost estimation).
*
* @see core/class-content-analyzer.php — Calls log_api_call() after analysis.
* @see core/class-content-generator.php — Calls log_api_call() after each stage.
* @see core/class-chief-editor.php — Calls log_api_call() after review.
* @see admin/class-metrics-page.php — Displays cost data from this class.
* @see ARCHITECTURE.md — ab_generation_log table schema.
* @see ARCHITECTURE.md — prab_generation_log table schema.
*/
class Autoblogger_Cost_Tracker {
class PRAutoBlogger_Cost_Tracker {
/**
* Running cost for the current pipeline execution.
@@ -59,7 +59,7 @@ class Autoblogger_Cost_Tracker {
/**
* Log an API call with its token usage and estimated cost.
*
* Side effects: database insert into ab_generation_log.
* Side effects: database insert into prab_generation_log.
*
* @param int|null $post_id WordPress post ID (null during generation, set later).
* @param string $stage Pipeline stage: 'analysis', 'outline', 'draft', 'polish', 'review'.
@@ -82,12 +82,12 @@ class Autoblogger_Cost_Tracker {
string $response_status = 'success',
string $error_message = ''
): void {
$llm = new Autoblogger_OpenRouter_Provider();
$llm = new PRAutoBlogger_OpenRouter_Provider();
$cost = $llm->estimate_cost( $model, $prompt_tokens, $completion_tokens );
$this->current_run_cost += $cost;
$log_entry = new Autoblogger_Generation_Log( [
$log_entry = new PRAutoBlogger_Generation_Log( [
'post_id' => $post_id,
'run_id' => $this->run_id,
'stage' => $stage,
@@ -102,13 +102,13 @@ class Autoblogger_Cost_Tracker {
] );
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_generation_log';
$table = $wpdb->prefix . 'prautoblogger_generation_log';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->insert( $table, $log_entry->to_db_row() );
if ( false === $wpdb->insert_id ) {
Autoblogger_Logger::instance()->error( 'Failed to log API call: ' . $wpdb->last_error, 'cost-tracker' );
PRAutoBlogger_Logger::instance()->error( 'Failed to log API call: ' . $wpdb->last_error, 'cost-tracker' );
}
}
@@ -121,7 +121,7 @@ class Autoblogger_Cost_Tracker {
* @return bool True if monthly spend >= configured budget.
*/
public function is_budget_exceeded(): bool {
$budget = (float) get_option( 'autoblogger_monthly_budget_usd', 50.00 );
$budget = (float) get_option( 'prautoblogger_monthly_budget_usd', 50.00 );
if ( $budget <= 0 ) {
// Budget of 0 means no limit.
return false;
@@ -141,7 +141,7 @@ class Autoblogger_Cost_Tracker {
*/
public function get_monthly_spend(): float {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_generation_log';
$table = $wpdb->prefix . 'prautoblogger_generation_log';
$first_of_month = gmdate( 'Y-m-01 00:00:00' );
@@ -174,7 +174,7 @@ class Autoblogger_Cost_Tracker {
*/
public function get_daily_spend( int $days = 30 ): array {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_generation_log';
$table = $wpdb->prefix . 'prautoblogger_generation_log';
$start_date = gmdate( 'Y-m-d', time() - ( $days * DAY_IN_SECONDS ) );
@@ -209,7 +209,7 @@ class Autoblogger_Cost_Tracker {
*/
public function get_spend_by_stage( string $start_date, string $end_date ): array {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_generation_log';
$table = $wpdb->prefix . 'prautoblogger_generation_log';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$results = $wpdb->get_results(
@@ -245,7 +245,7 @@ class Autoblogger_Cost_Tracker {
* @return float Percentage (0-100+). Can exceed 100 if overspent.
*/
public function get_budget_utilization(): float {
$budget = (float) get_option( 'autoblogger_monthly_budget_usd', 50.00 );
$budget = (float) get_option( 'prautoblogger_monthly_budget_usd', 50.00 );
if ( $budget <= 0 ) {
return 0.0;
}

View File

@@ -7,13 +7,13 @@ declare(strict_types=1);
* Handles OAuth2 service account authentication and the GA4 Data API.
* Decoupled from metrics collection logic for testability and reusability.
*
* Triggered by: Autoblogger_Metrics_Collector::fetch_ga4_data() calls methods here.
* Dependencies: Autoblogger_Encryption (for credentials decryption), wp_remote_post/get().
* Triggered by: PRAutoBlogger_Metrics_Collector::fetch_ga4_data() calls methods here.
* Dependencies: PRAutoBlogger_Encryption (for credentials decryption), wp_remote_post/get().
*
* @see core/class-metrics-collector.php — Instantiates this class.
* @see ARCHITECTURE.md — External API integrations table.
*/
class Autoblogger_GA4_Client {
class PRAutoBlogger_GA4_Client {
/**
* Fetch Google Analytics 4 data for given posts.
@@ -27,17 +27,17 @@ class Autoblogger_GA4_Client {
* @return array<int, array{pageviews: int, avg_time_on_page: float, bounce_rate: float}>
*/
public function fetch_data( array $posts ): array {
$property_id = get_option( 'autoblogger_ga4_property_id', '' );
$property_id = get_option( 'prautoblogger_ga4_property_id', '' );
if ( '' === $property_id ) {
return [];
}
$credentials_encrypted = get_option( 'autoblogger_ga4_credentials_json', '' );
$credentials_encrypted = get_option( 'prautoblogger_ga4_credentials_json', '' );
if ( '' === $credentials_encrypted ) {
return [];
}
$credentials = Autoblogger_Encryption::decrypt( $credentials_encrypted );
$credentials = PRAutoBlogger_Encryption::decrypt( $credentials_encrypted );
if ( '' === $credentials ) {
return [];
}
@@ -61,7 +61,7 @@ class Autoblogger_GA4_Client {
// Using the REST API directly since we don't want a full Google SDK dependency.
$access_token = $this->get_access_token( $credentials );
if ( '' === $access_token ) {
Autoblogger_Logger::instance()->error( 'GA4: Failed to get access token. Possible causes: invalid service account JSON, expired or revoked private key, or network error reaching oauth2.googleapis.com. Verify credentials in AutoBlogger → Settings.', 'ga4' );
PRAutoBlogger_Logger::instance()->error( 'GA4: Failed to get access token. Possible causes: invalid service account JSON, expired or revoked private key, or network error reaching oauth2.googleapis.com. Verify credentials in PRAutoBlogger → Settings.', 'ga4' );
return [];
}
@@ -94,14 +94,14 @@ class Autoblogger_GA4_Client {
);
if ( is_wp_error( $response ) ) {
Autoblogger_Logger::instance()->error( 'GA4 API request failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
PRAutoBlogger_Logger::instance()->error( 'GA4 API request failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
return [];
}
$ga4_status = wp_remote_retrieve_response_code( $response );
if ( 200 !== $ga4_status ) {
$ga4_body = wp_remote_retrieve_body( $response );
Autoblogger_Logger::instance()->error(
PRAutoBlogger_Logger::instance()->error(
sprintf( 'GA4 API request failed (HTTP %d): %s', $ga4_status, substr( $ga4_body, 0, 500 ) ),
'ga4'
);
@@ -159,7 +159,7 @@ class Autoblogger_GA4_Client {
$key = openssl_pkey_get_private( $creds['private_key'] );
if ( false === $key ) {
Autoblogger_Logger::instance()->error( 'GA4: Failed to parse private key from service account credentials. The key may be malformed or the OpenSSL extension may be misconfigured.', 'ga4' );
PRAutoBlogger_Logger::instance()->error( 'GA4: Failed to parse private key from service account credentials. The key may be malformed or the OpenSSL extension may be misconfigured.', 'ga4' );
return '';
}
@@ -181,7 +181,7 @@ class Autoblogger_GA4_Client {
);
if ( is_wp_error( $response ) ) {
Autoblogger_Logger::instance()->error( 'GA4: OAuth token exchange failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
PRAutoBlogger_Logger::instance()->error( 'GA4: OAuth token exchange failed (WP HTTP error): ' . $response->get_error_message(), 'ga4' );
return '';
}
@@ -190,7 +190,7 @@ class Autoblogger_GA4_Client {
if ( 200 !== $token_status || ! isset( $data['access_token'] ) ) {
$error_desc = $data['error_description'] ?? $data['error'] ?? 'unknown';
Autoblogger_Logger::instance()->error(
PRAutoBlogger_Logger::instance()->error(
sprintf( 'GA4: OAuth token exchange failed (HTTP %d): %s', $token_status, $error_desc ),
'ga4'
);

View File

@@ -8,25 +8,25 @@ declare(strict_types=1);
* scores ideas by relevance, frequency, and freshness, then returns
* the top N ideas matching the daily target.
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 3).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 3).
* Dependencies: WordPress $wpdb, WP_Query.
*
* @see core/class-content-analyzer.php — Produces the analysis results we consume.
* @see core/class-content-generator.php — Consumes the ranked ideas we produce.
* @see ARCHITECTURE.md — Data flow step 3.
*/
class Autoblogger_Idea_Scorer {
class PRAutoBlogger_Idea_Scorer {
/**
* Score, deduplicate, and rank article ideas.
*
* @param Autoblogger_Analysis_Result[] $analysis_results Raw analysis output.
* @param PRAutoBlogger_Analysis_Result[] $analysis_results Raw analysis output.
* @param int $target_count Number of ideas to return.
*
* @return Autoblogger_Article_Idea[] Top-ranked ideas, sorted by score descending.
* @return PRAutoBlogger_Article_Idea[] Top-ranked ideas, sorted by score descending.
*/
public function score_and_rank( array $analysis_results, int $target_count ): array {
$exclusions = json_decode( get_option( 'autoblogger_topic_exclusions', '[]' ), true ) ?: [];
$exclusions = json_decode( get_option( 'prautoblogger_topic_exclusions', '[]' ), true ) ?: [];
$ideas = [];
foreach ( $analysis_results as $result ) {
@@ -42,7 +42,7 @@ class Autoblogger_Idea_Scorer {
$metadata = $result->get_metadata() ?? [];
$ideas[] = new Autoblogger_Article_Idea( [
$ideas[] = new PRAutoBlogger_Article_Idea( [
'topic' => $result->get_topic(),
'article_type' => $this->map_type( $result->get_analysis_type() ),
'suggested_title' => $metadata['suggested_title'] ?? $result->get_topic(),
@@ -56,7 +56,7 @@ class Autoblogger_Idea_Scorer {
}
// Sort by score descending.
usort( $ideas, static function ( Autoblogger_Article_Idea $a, Autoblogger_Article_Idea $b ): int {
usort( $ideas, static function ( PRAutoBlogger_Article_Idea $a, PRAutoBlogger_Article_Idea $b ): int {
return $b->get_score() <=> $a->get_score();
} );
@@ -68,11 +68,11 @@ class Autoblogger_Idea_Scorer {
*
* Factors: LLM relevance score (50%), frequency (30%), recency bonus (20%).
*
* @param Autoblogger_Analysis_Result $result
* @param PRAutoBlogger_Analysis_Result $result
*
* @return float Score between 0 and 1.
*/
private function compute_score( Autoblogger_Analysis_Result $result ): float {
private function compute_score( PRAutoBlogger_Analysis_Result $result ): float {
$relevance = min( $result->get_relevance_score(), 1.0 );
$frequency = min( $result->get_frequency() / 10.0, 1.0 );
@@ -114,7 +114,7 @@ class Autoblogger_Idea_Scorer {
$query = new \WP_Query( [
'post_type' => 'post',
'post_status' => [ 'publish', 'draft', 'pending' ],
'meta_key' => '_autoblogger_generated',
'meta_key' => '_prautoblogger_generated',
'meta_value' => '1',
's' => $topic,
'posts_per_page' => 1,

View File

@@ -6,7 +6,7 @@ declare(strict_types=1);
*
* Writes log entries to a custom database table and optionally forwards
* to PHP's error_log() for server-level monitoring. The log level is
* user-configurable via Settings → AutoBlogger → General.
* user-configurable via Settings → PRAutoBlogger → General.
*
* Levels (ascending verbosity): error → warning → info → debug.
* Setting the level to "info" captures error, warning, and info — not debug.
@@ -15,10 +15,10 @@ declare(strict_types=1);
* Dependencies: WordPress $wpdb.
*
* @see admin/class-log-viewer.php — Reads and displays log entries.
* @see class-activator.php — Creates the ab_event_log table.
* @see class-activator.php — Creates the prab_event_log table.
* @see ARCHITECTURE.md — Logging architecture section.
*/
class Autoblogger_Logger {
class PRAutoBlogger_Logger {
/** Log levels, lower = more severe. */
public const LEVEL_ERROR = 0;
@@ -47,7 +47,7 @@ class Autoblogger_Logger {
private int $threshold;
private function __construct() {
$setting = get_option( 'autoblogger_log_level', 'info' );
$setting = get_option( 'prautoblogger_log_level', 'info' );
$this->threshold = self::LEVEL_MAP[ $setting ] ?? self::LEVEL_INFO;
}
@@ -122,11 +122,11 @@ class Autoblogger_Logger {
// Always forward errors to PHP error_log for server-level monitoring.
if ( $level <= self::LEVEL_WARNING ) {
error_log( sprintf( '[AutoBlogger][%s] %s — %s', strtoupper( $label ), $context, $message ) );
error_log( sprintf( '[PRAutoBlogger][%s] %s — %s', strtoupper( $label ), $context, $message ) );
}
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_event_log';
$table = $wpdb->prefix . 'prautoblogger_event_log';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->insert( $table, [
@@ -155,7 +155,7 @@ class Autoblogger_Logger {
int $per_page = 50
): array {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_event_log';
$table = $wpdb->prefix . 'prautoblogger_event_log';
$where = [];
$params = [];
@@ -207,7 +207,7 @@ class Autoblogger_Logger {
*/
public static function prune( int $days = 30 ): int {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_event_log';
$table = $wpdb->prefix . 'prautoblogger_event_log';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
return (int) $wpdb->query(

View File

@@ -5,21 +5,21 @@ declare(strict_types=1);
* Collects post performance metrics from WordPress native data and Google Analytics 4.
*
* Runs on a separate cron schedule (every 6 hours) to gather pageviews, bounce rate,
* time on page, and comments for AutoBlogger-generated posts. Computes a composite
* time on page, and comments for PRAutoBlogger-generated posts. Computes a composite
* "content score" that feeds back into the analysis engine for self-improvement.
*
* Triggered by: WP-Cron hook `autoblogger_collect_metrics` (every 6 hours).
* Dependencies: WordPress $wpdb, Autoblogger_GA4_Client (for GA4 API calls).
* Triggered by: WP-Cron hook `prautoblogger_collect_metrics` (every 6 hours).
* Dependencies: WordPress $wpdb, PRAutoBlogger_GA4_Client (for GA4 API calls).
*
* @see core/class-ga4-client.php — Handles GA4 API authentication and requests.
* @see core/class-content-analyzer.php — Reads scores for self-improvement loop.
* @see admin/class-metrics-page.php — Displays collected metrics.
* @see ARCHITECTURE.md — Data flow step 7.
*/
class Autoblogger_Metrics_Collector {
class PRAutoBlogger_Metrics_Collector {
/**
* Collect metrics for all AutoBlogger-generated posts.
* Collect metrics for all PRAutoBlogger-generated posts.
*
* Gathers WordPress native metrics (comments) and GA4 data (if configured),
* then computes composite scores.
@@ -34,7 +34,7 @@ class Autoblogger_Metrics_Collector {
return 0;
}
$ga4_client = new Autoblogger_GA4_Client();
$ga4_client = new PRAutoBlogger_GA4_Client();
$ga4_data = $ga4_client->fetch_data( $posts );
$scored = 0;
@@ -54,12 +54,12 @@ class Autoblogger_Metrics_Collector {
$scored++;
}
Autoblogger_Logger::instance()->info( sprintf( 'Metrics collected for %d posts.', $scored ), 'metrics' );
PRAutoBlogger_Logger::instance()->info( sprintf( 'Metrics collected for %d posts.', $scored ), 'metrics' );
return $scored;
}
/**
* Get all AutoBlogger-generated published posts.
* Get all PRAutoBlogger-generated published posts.
*
* @return \WP_Post[]
*/
@@ -67,7 +67,7 @@ class Autoblogger_Metrics_Collector {
$query = new \WP_Query( [
'post_type' => 'post',
'post_status' => 'publish',
'meta_key' => '_autoblogger_generated',
'meta_key' => '_prautoblogger_generated',
'meta_value' => '1',
'posts_per_page' => 100,
'orderby' => 'date',
@@ -89,7 +89,7 @@ class Autoblogger_Metrics_Collector {
// Post may have been deleted between the WP_Query and this call.
if ( null === $post ) {
Autoblogger_Logger::instance()->debug( sprintf( 'Metrics: Post ID %d no longer exists, skipping.', $post_id ), 'metrics' );
PRAutoBlogger_Logger::instance()->debug( sprintf( 'Metrics: Post ID %d no longer exists, skipping.', $post_id ), 'metrics' );
return null;
}
@@ -160,9 +160,9 @@ class Autoblogger_Metrics_Collector {
*/
private function store_score( int $post_id, array $wp_metrics, array $ga_metrics, array $score_data ): void {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_content_scores';
$table = $wpdb->prefix . 'prautoblogger_content_scores';
$entry = new Autoblogger_Content_Score( [
$entry = new PRAutoBlogger_Content_Score( [
'post_id' => $post_id,
'pageviews' => $ga_metrics['pageviews'] ?? 0,
'avg_time_on_page' => $ga_metrics['avg_time_on_page'] ?? 0.0,

View File

@@ -7,14 +7,14 @@ declare(strict_types=1);
* Orchestrates: Budget check → Collect source data → Analyze patterns →
* Score ideas → Generate content → Editorial review → Publish.
*
* Triggered by: Autoblogger::on_daily_generation() and Autoblogger::on_ajax_generate_now().
* Triggered by: PRAutoBlogger::on_daily_generation() and PRAutoBlogger::on_ajax_generate_now().
* Dependencies: All core content classes (Collector, Analyzer, Scorer, Generator, Editor, Publisher).
*
* @see class-autoblogger.php — Instantiates this class and calls run().
* @see class-prautoblogger.php — Instantiates this class and calls run().
* @see core/class-cost-tracker.php — Tracks cumulative API costs throughout the run.
* @see ARCHITECTURE.md — Data flow diagram showing pipeline steps.
*/
class Autoblogger_Pipeline_Runner {
class PRAutoBlogger_Pipeline_Runner {
/**
* Execute the full content generation pipeline.
@@ -29,7 +29,7 @@ class Autoblogger_Pipeline_Runner {
* @throws \RuntimeException If budget is exceeded or critical step fails.
*/
public function run(): array {
$cost_tracker = new Autoblogger_Cost_Tracker();
$cost_tracker = new PRAutoBlogger_Cost_Tracker();
// Tag all log entries in this run with a unique ID so link_generation_logs()
// can accurately attribute costs to the correct post in batch runs.
@@ -38,35 +38,35 @@ class Autoblogger_Pipeline_Runner {
// Hard stop if monthly budget is exceeded.
if ( $cost_tracker->is_budget_exceeded() ) {
throw new \RuntimeException(
__( 'Monthly API budget exceeded. Generation halted.', 'autoblogger' )
__( 'Monthly API budget exceeded. Generation halted.', 'prautoblogger' )
);
}
$target_count = absint( get_option( 'autoblogger_daily_article_target', 1 ) );
$target_count = absint( get_option( 'prautoblogger_daily_article_target', 1 ) );
// Step 1: Collect source data.
$collector = new Autoblogger_Source_Collector();
$collector = new PRAutoBlogger_Source_Collector();
$collector->collect_from_all_sources();
// Step 2: Analyze collected data for patterns.
$llm = new Autoblogger_OpenRouter_Provider();
$analyzer = new Autoblogger_Content_Analyzer( $llm, $cost_tracker );
$llm = new PRAutoBlogger_OpenRouter_Provider();
$analyzer = new PRAutoBlogger_Content_Analyzer( $llm, $cost_tracker );
$analysis = $analyzer->analyze_recent_data();
// Step 3: Score and deduplicate ideas.
$scorer = new Autoblogger_Idea_Scorer();
$scorer = new PRAutoBlogger_Idea_Scorer();
$ideas = $scorer->score_and_rank( $analysis, $target_count );
if ( empty( $ideas ) ) {
Autoblogger_Logger::instance()->info( 'No viable article ideas found. Skipping generation.', 'pipeline' );
PRAutoBlogger_Logger::instance()->info( 'No viable article ideas found. Skipping generation.', 'pipeline' );
return [ 'generated' => 0, 'published' => 0, 'rejected' => 0, 'cost' => 0.0 ];
}
// Step 4 & 5: Generate content and run through editor for each idea.
$generator = new Autoblogger_Content_Generator( $llm, $cost_tracker );
$editor = new Autoblogger_Chief_Editor( $llm, $cost_tracker );
$publisher = new Autoblogger_Publisher();
$auto_publish = in_array( get_option( 'autoblogger_auto_publish', '0' ), [ '1', 'yes' ], true );
$generator = new PRAutoBlogger_Content_Generator( $llm, $cost_tracker );
$editor = new PRAutoBlogger_Chief_Editor( $llm, $cost_tracker );
$publisher = new PRAutoBlogger_Publisher();
$auto_publish = in_array( get_option( 'prautoblogger_auto_publish', '0' ), [ '1', 'yes' ], true );
$generated = 0;
$published = 0;
@@ -75,7 +75,7 @@ class Autoblogger_Pipeline_Runner {
foreach ( $ideas as $idea ) {
// Re-check budget before each article — stop early if we hit the limit.
if ( $cost_tracker->is_budget_exceeded() ) {
Autoblogger_Logger::instance()->warning( 'Budget limit reached during generation. Stopping early.', 'pipeline' );
PRAutoBlogger_Logger::instance()->warning( 'Budget limit reached during generation. Stopping early.', 'pipeline' );
break;
}
@@ -103,17 +103,17 @@ class Autoblogger_Pipeline_Runner {
} else {
$publisher->save_as_draft( $content, $idea, $review, $cost_tracker->get_run_id() );
$rejected++;
Autoblogger_Logger::instance()->info( 'Article rejected by editor: ' . $idea->get_topic(), 'pipeline' );
PRAutoBlogger_Logger::instance()->info( 'Article rejected by editor: ' . $idea->get_topic(), 'pipeline' );
}
} catch ( \Exception $e ) {
Autoblogger_Logger::instance()->error( 'Failed to generate article for topic "' . $idea->get_topic() . '": ' . $e->getMessage(), 'pipeline' );
PRAutoBlogger_Logger::instance()->error( 'Failed to generate article for topic "' . $idea->get_topic() . '": ' . $e->getMessage(), 'pipeline' );
// Continue with next idea — don't let one failure stop the batch.
}
}
$total_cost = $cost_tracker->get_current_run_cost();
Autoblogger_Logger::instance()->info(
PRAutoBlogger_Logger::instance()->info(
sprintf( 'Pipeline complete: %d generated, %d published, %d rejected. Cost: $%.4f', $generated, $published, $rejected, $total_cost ),
'pipeline'
);

View File

@@ -7,14 +7,14 @@ declare(strict_types=1);
* Handles both publishing (editor-approved) and saving as draft (editor-rejected).
* Stores all generation metadata as post_meta for transparency and auditability.
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 6).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 6).
* Dependencies: WordPress wp_insert_post(), post meta API.
*
* @see core/class-chief-editor.php — Produces the editorial review we consume.
* @see core/class-content-generator.php — Produces the content we publish.
* @see ARCHITECTURE.md — Data flow step 6.
*/
class Autoblogger_Publisher {
class PRAutoBlogger_Publisher {
/**
* Publish an editor-approved article.
@@ -25,8 +25,8 @@ class Autoblogger_Publisher {
* Side effects: Creates a WordPress post, writes post_meta.
*
* @param string $content The final HTML content.
* @param Autoblogger_Article_Idea $idea The original article idea.
* @param Autoblogger_Editorial_Review $review The editor's review.
* @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.
*
* @return int The created post ID.
@@ -35,8 +35,8 @@ class Autoblogger_Publisher {
*/
public function publish(
string $content,
Autoblogger_Article_Idea $idea,
Autoblogger_Editorial_Review $review,
PRAutoBlogger_Article_Idea $idea,
PRAutoBlogger_Editorial_Review $review,
?string $run_id = null
): int {
return $this->create_post( $content, $idea, $review, 'publish', $run_id );
@@ -46,8 +46,8 @@ class Autoblogger_Publisher {
* Save an editor-rejected article as a draft for human review.
*
* @param string $content The generated HTML content (pre-revision).
* @param Autoblogger_Article_Idea $idea The original article idea.
* @param Autoblogger_Editorial_Review $review The editor's review with rejection notes.
* @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.
*
* @return int The created post ID.
@@ -56,8 +56,8 @@ class Autoblogger_Publisher {
*/
public function save_as_draft(
string $content,
Autoblogger_Article_Idea $idea,
Autoblogger_Editorial_Review $review,
PRAutoBlogger_Article_Idea $idea,
PRAutoBlogger_Editorial_Review $review,
?string $run_id = null
): int {
return $this->create_post( $content, $idea, $review, 'draft', $run_id );
@@ -67,8 +67,8 @@ class Autoblogger_Publisher {
* Create a WordPress post with generation metadata.
*
* @param string $content HTML content.
* @param Autoblogger_Article_Idea $idea Article idea.
* @param Autoblogger_Editorial_Review $review Editorial review.
* @param PRAutoBlogger_Article_Idea $idea Article idea.
* @param PRAutoBlogger_Editorial_Review $review Editorial review.
* @param string $post_status 'publish' or 'draft'.
* @param string|null $run_id Pipeline run ID for log linking.
*
@@ -78,8 +78,8 @@ class Autoblogger_Publisher {
*/
private function create_post(
string $content,
Autoblogger_Article_Idea $idea,
Autoblogger_Editorial_Review $review,
PRAutoBlogger_Article_Idea $idea,
PRAutoBlogger_Editorial_Review $review,
string $post_status,
?string $run_id = null
): int {
@@ -93,15 +93,15 @@ class Autoblogger_Publisher {
];
/**
* Fires before an AutoBlogger post is created.
* Fires before an PRAutoBlogger post is created.
*
* Listeners registered in: class-autoblogger.php (main loader).
* Listeners registered in: class-prautoblogger.php (main loader).
*
* @param array $post_data Post data array.
* @param Autoblogger_Article_Idea $idea The article idea.
* @param Autoblogger_Editorial_Review $review The editorial review.
* @param PRAutoBlogger_Article_Idea $idea The article idea.
* @param PRAutoBlogger_Editorial_Review $review The editorial review.
*/
$post_data = apply_filters( 'autoblogger_filter_post_data', $post_data, $idea, $review );
$post_data = apply_filters( 'prautoblogger_filter_post_data', $post_data, $idea, $review );
$post_id = wp_insert_post( $post_data, true );
@@ -109,7 +109,7 @@ class Autoblogger_Publisher {
throw new \RuntimeException(
sprintf(
/* translators: %s: WordPress error message */
__( 'Failed to create post: %s', 'autoblogger' ),
__( 'Failed to create post: %s', 'prautoblogger' ),
$post_id->get_error_message()
)
);
@@ -121,20 +121,20 @@ class Autoblogger_Publisher {
// Update generation log entries to reference this post.
$this->link_generation_logs( $post_id, $run_id );
Autoblogger_Logger::instance()->info(
PRAutoBlogger_Logger::instance()->info(
sprintf( 'Post created: ID=%d, status=%s, title="%s"', $post_id, $post_status, $idea->get_suggested_title() ),
'publisher'
);
/**
* Fires after an AutoBlogger post is successfully created.
* Fires after an PRAutoBlogger post is successfully created.
*
* @param int $post_id The new post ID.
* @param string $post_status 'publish' or 'draft'.
* @param Autoblogger_Article_Idea $idea The article idea.
* @param Autoblogger_Editorial_Review $review The editorial review.
* @param PRAutoBlogger_Article_Idea $idea The article idea.
* @param PRAutoBlogger_Editorial_Review $review The editorial review.
*/
do_action( 'autoblogger_post_created', $post_id, $post_status, $idea, $review );
do_action( 'prautoblogger_post_created', $post_id, $post_status, $idea, $review );
return $post_id;
}
@@ -142,32 +142,32 @@ class Autoblogger_Publisher {
/**
* Build post_meta array for generation metadata.
*
* @param Autoblogger_Article_Idea $idea
* @param Autoblogger_Editorial_Review $review
* @param PRAutoBlogger_Article_Idea $idea
* @param PRAutoBlogger_Editorial_Review $review
*
* @return array<string, mixed>
*/
private function build_meta(
Autoblogger_Article_Idea $idea,
Autoblogger_Editorial_Review $review
PRAutoBlogger_Article_Idea $idea,
PRAutoBlogger_Editorial_Review $review
): array {
$pipeline_mode = get_option( 'autoblogger_writing_pipeline', 'multi_step' );
$model = get_option( 'autoblogger_writing_model', AUTOBLOGGER_DEFAULT_WRITING_MODEL );
$pipeline_mode = get_option( 'prautoblogger_writing_pipeline', 'multi_step' );
$model = get_option( 'prautoblogger_writing_model', PRAUTOBLOGGER_DEFAULT_WRITING_MODEL );
return [
'_autoblogger_generated' => '1',
'_autoblogger_analysis_id' => $idea->get_analysis_id(),
'_autoblogger_source_ids' => wp_json_encode( $idea->get_source_ids() ),
'_autoblogger_model_used' => $model,
'_autoblogger_pipeline_mode' => $pipeline_mode,
'_autoblogger_editor_verdict' => $review->get_verdict(),
'_autoblogger_editor_notes' => $review->get_notes(),
'_autoblogger_quality_score' => $review->get_quality_score(),
'_autoblogger_seo_score' => $review->get_seo_score(),
'_autoblogger_generated_at' => gmdate( 'c' ),
'_autoblogger_topic' => $idea->get_topic(),
'_autoblogger_article_type' => $idea->get_article_type(),
'_autoblogger_target_keywords' => wp_json_encode( $idea->get_target_keywords() ),
'_prautoblogger_generated' => '1',
'_prautoblogger_analysis_id' => $idea->get_analysis_id(),
'_prautoblogger_source_ids' => wp_json_encode( $idea->get_source_ids() ),
'_prautoblogger_model_used' => $model,
'_prautoblogger_pipeline_mode' => $pipeline_mode,
'_prautoblogger_editor_verdict' => $review->get_verdict(),
'_prautoblogger_editor_notes' => $review->get_notes(),
'_prautoblogger_quality_score' => $review->get_quality_score(),
'_prautoblogger_seo_score' => $review->get_seo_score(),
'_prautoblogger_generated_at' => gmdate( 'c' ),
'_prautoblogger_topic' => $idea->get_topic(),
'_prautoblogger_article_type' => $idea->get_article_type(),
'_prautoblogger_target_keywords' => wp_json_encode( $idea->get_target_keywords() ),
];
}
@@ -175,11 +175,11 @@ class Autoblogger_Publisher {
* Assign category and tags to a generated post.
*
* @param int $post_id
* @param Autoblogger_Article_Idea $idea
* @param PRAutoBlogger_Article_Idea $idea
*
* @return void
*/
private function assign_taxonomy_terms( int $post_id, Autoblogger_Article_Idea $idea ): void {
private function assign_taxonomy_terms( int $post_id, PRAutoBlogger_Article_Idea $idea ): void {
$type_category_map = [
'guide' => 'Guides',
'solution' => 'Solutions',
@@ -222,7 +222,7 @@ class Autoblogger_Publisher {
*/
private function link_generation_logs( int $post_id, ?string $run_id = null ): void {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_generation_log';
$table = $wpdb->prefix . 'prautoblogger_generation_log';
if ( null !== $run_id && '' !== $run_id ) {
// Precise linking via run_id — no risk of cross-article misattribution.
@@ -253,7 +253,7 @@ class Autoblogger_Publisher {
* @return int WordPress user ID.
*/
private function get_default_author_id(): int {
$author_id = absint( get_option( 'autoblogger_default_author', 0 ) );
$author_id = absint( get_option( 'prautoblogger_default_author', 0 ) );
if ( $author_id > 0 ) {
return $author_id;
}

View File

@@ -8,14 +8,14 @@ declare(strict_types=1);
* collect_data() method, and stores results in the ab_source_data table.
* Handles deduplication via UNIQUE index on (source_type, source_id).
*
* Triggered by: Autoblogger::run_generation_pipeline() (step 1).
* Triggered by: PRAutoBlogger::run_generation_pipeline() (step 1).
* Dependencies: Source provider implementations, WordPress $wpdb.
*
* @see providers/interface-source-provider.php — Interface providers implement.
* @see providers/class-reddit-provider.php — Primary source provider.
* @see ARCHITECTURE.md — Data flow step 1.
*/
class Autoblogger_Source_Collector {
class PRAutoBlogger_Source_Collector {
/**
* Collect data from all enabled source providers and store in database.
@@ -25,7 +25,7 @@ class Autoblogger_Source_Collector {
* @return int Total number of new records inserted.
*/
public function collect_from_all_sources(): int {
$enabled = json_decode( get_option( 'autoblogger_enabled_sources', '["reddit"]' ), true );
$enabled = json_decode( get_option( 'prautoblogger_enabled_sources', '["reddit"]' ), true );
if ( ! is_array( $enabled ) ) {
$enabled = [ 'reddit' ];
}
@@ -35,7 +35,7 @@ class Autoblogger_Source_Collector {
foreach ( $enabled as $source_type ) {
$provider = $this->get_provider( $source_type );
if ( null === $provider ) {
Autoblogger_Logger::instance()->warning( "No provider found for source type: {$source_type}", 'collector' );
PRAutoBlogger_Logger::instance()->warning( "No provider found for source type: {$source_type}", 'collector' );
continue;
}
@@ -44,12 +44,12 @@ class Autoblogger_Source_Collector {
$inserted = $this->store_data( $data );
$total_inserted += $inserted;
Autoblogger_Logger::instance()->info(
PRAutoBlogger_Logger::instance()->info(
sprintf( 'Collected %d items from %s (%d new).', count( $data ), $source_type, $inserted ),
'collector'
);
} catch ( \Exception $e ) {
Autoblogger_Logger::instance()->error( "Collection from {$source_type} FAILED: " . $e->getMessage(), 'collector' );
PRAutoBlogger_Logger::instance()->error( "Collection from {$source_type} FAILED: " . $e->getMessage(), 'collector' );
// Continue with other sources — one failure shouldn't stop everything.
}
}
@@ -64,13 +64,13 @@ class Autoblogger_Source_Collector {
*
* Side effects: database inserts.
*
* @param Autoblogger_Source_Data[] $items
* @param PRAutoBlogger_Source_Data[] $items
*
* @return int Number of new records inserted.
*/
private function store_data( array $items ): int {
global $wpdb;
$table = $wpdb->prefix . 'autoblogger_source_data';
$table = $wpdb->prefix . 'prautoblogger_source_data';
$inserted = 0;
foreach ( $items as $item ) {
@@ -110,11 +110,11 @@ class Autoblogger_Source_Collector {
*
* @param string $source_type Source type identifier.
*
* @return Autoblogger_Source_Provider_Interface|null
* @return PRAutoBlogger_Source_Provider_Interface|null
*/
private function get_provider( string $source_type ): ?Autoblogger_Source_Provider_Interface {
private function get_provider( string $source_type ): ?PRAutoBlogger_Source_Provider_Interface {
$providers = [
'reddit' => Autoblogger_Reddit_Provider::class,
'reddit' => PRAutoBlogger_Reddit_Provider::class,
// Future providers: 'tiktok', 'instagram', 'youtube'
];
@@ -123,7 +123,7 @@ class Autoblogger_Source_Collector {
*
* @param array<string, string> $providers Map of source_type => class name.
*/
$providers = apply_filters( 'autoblogger_filter_source_providers', $providers );
$providers = apply_filters( 'prautoblogger_filter_source_providers', $providers );
if ( ! isset( $providers[ $source_type ] ) ) {
return null;
@@ -134,7 +134,7 @@ class Autoblogger_Source_Collector {
// Guard against unloadable provider classes (e.g. stub providers not yet
// autoloadable, or classes removed during development).
if ( ! class_exists( $class ) ) {
Autoblogger_Logger::instance()->error(
PRAutoBlogger_Logger::instance()->error(
sprintf( 'Provider class "%s" for source type "%s" does not exist.', $class, $source_type ),
'collector'
);
@@ -154,7 +154,7 @@ class Autoblogger_Source_Collector {
private function get_config_for_source( string $source_type ): array {
if ( 'reddit' === $source_type ) {
return [
'subreddits' => json_decode( get_option( 'autoblogger_target_subreddits', '[]' ), true ) ?: [],
'subreddits' => json_decode( get_option( 'prautoblogger_target_subreddits', '[]' ), true ) ?: [],
'limit' => 25,
'time_filter' => 'day',
'include_comments' => true,