feat(schema): v1.2.0 substrate schema + stage display map (v0.18.0)
Pipeline v2 Phase 1, commit 1/4. Five new dbDelta-idempotent tables (prompts, run_sources, run_decisions, runs, run_stages), additive nullable agent_role + prompt_version columns on generation_log, PRAUTOBLOGGER_DB_VERSION 1.1.0 -> 1.2.0 (existing on_check_db_version auto-migration re-runs the installers on mismatch), Stage_Display_Map covering historical + image + v2 stage vocabulary with humanizing fallback, uninstall coverage for all new tables + full cron hook list, ARCHITECTURE edit->polish canonicalization + schema docs + decision #22. No behavior change: nothing reads the new tables yet. Agent-Session: pipeline-v2-phase1-engineer-20260612
This commit is contained in:
9
includes/class-activator.php
Executable file → Normal file
9
includes/class-activator.php
Executable file → Normal file
@@ -36,9 +36,16 @@ class PRAutoBlogger_Activator {
|
||||
update_option( 'prautoblogger_db_version', PRAUTOBLOGGER_DB_VERSION, false );
|
||||
}
|
||||
|
||||
/** Delegate to `PRAutoBlogger_Schema_Installer::install()` — see that class. */
|
||||
/**
|
||||
* Delegate to the schema installers — see those classes.
|
||||
*
|
||||
* `Schema_Installer` owns the v1.1.0 tables; `Pipeline_Schema_Installer`
|
||||
* owns the v0.18.0 / db-1.2.0 Pipeline v2 substrate tables. Both are
|
||||
* dbDelta-idempotent and safe to re-run on any version mismatch.
|
||||
*/
|
||||
private static function create_tables(): void {
|
||||
PRAutoBlogger_Schema_Installer::install();
|
||||
PRAutoBlogger_Pipeline_Schema_Installer::install();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
147
includes/class-pipeline-schema-installer.php
Normal file
147
includes/class-pipeline-schema-installer.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
|
||||
*
|
||||
* Creates / updates the Pipeline v2 substrate tables via `dbDelta` (db 1.2.0).
|
||||
*
|
||||
* What: Owns the five v0.18.0 substrate tables — the versioned prompt
|
||||
* registry (`prompts`), the audit child tables (`run_sources`,
|
||||
* `run_decisions`), and the run ledger / state machine tables
|
||||
* (`runs`, `run_stages`). Kept separate from the original
|
||||
* PRAutoBlogger_Schema_Installer so both classes stay under the
|
||||
* 300-line cap and the v1.1.0 schema remains byte-stable.
|
||||
* Who triggers it: PRAutoBlogger_Activator::activate() — on activation and
|
||||
* on every `prautoblogger_db_version` mismatch (self-healing
|
||||
* auto-migration via PRAutoBlogger::on_check_db_version()).
|
||||
* Dependencies: WordPress $wpdb, dbDelta() from wp-admin/includes/upgrade.php.
|
||||
*
|
||||
* @see class-schema-installer.php — v1.1.0 tables (source data, logs, scores).
|
||||
* @see class-activator.php — Sole caller.
|
||||
* @see core/class-prompt-registry.php — Reads/writes the prompts table.
|
||||
* @see core/class-run-state.php — Reads/writes runs + run_stages.
|
||||
* @see ARCHITECTURE.md — Database schema section documents each table.
|
||||
*/
|
||||
class PRAutoBlogger_Pipeline_Schema_Installer {
|
||||
|
||||
/**
|
||||
* Create (or upgrade) the Pipeline v2 substrate tables.
|
||||
*
|
||||
* `dbDelta` is idempotent: it creates missing tables, adds missing
|
||||
* columns and indexes, and never drops anything — matching the
|
||||
* plugin's forward-only schema policy. Safe to re-run on every
|
||||
* version mismatch.
|
||||
*
|
||||
* Side effects: up to five CREATE TABLE / ALTER TABLE statements.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install(): void {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
$prefix = $wpdb->prefix . 'prautoblogger_';
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
// Versioned prompt registry. Versions are immutable: writes create
|
||||
// new (prompt_key, version) rows; exactly one row per key has
|
||||
// active = 1. `prompt_key` (not `key`) because KEY is reserved SQL.
|
||||
$sql_prompts = "CREATE TABLE {$prefix}prompts (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
prompt_key VARCHAR(64) NOT NULL,
|
||||
version INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
body LONGTEXT NOT NULL,
|
||||
model VARCHAR(100) DEFAULT NULL,
|
||||
params_json LONGTEXT DEFAULT NULL,
|
||||
author VARCHAR(100) NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL,
|
||||
active TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY key_version (prompt_key, version),
|
||||
KEY key_active (prompt_key, active)
|
||||
) {$charset_collate};";
|
||||
|
||||
// Research-source audit rows: one row per source a run considered,
|
||||
// with the keep/discard decision. Consolidates the old
|
||||
// source_ids_json + _prautoblogger_research_sources scatter for
|
||||
// new runs (no backfill of historical runs).
|
||||
$sql_run_sources = "CREATE TABLE {$prefix}run_sources (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
run_id VARCHAR(36) NOT NULL,
|
||||
agent_role VARCHAR(50) NOT NULL DEFAULT '',
|
||||
source_url VARCHAR(500) DEFAULT NULL,
|
||||
doi VARCHAR(255) DEFAULT NULL,
|
||||
kept TINYINT(1) NOT NULL DEFAULT 0,
|
||||
reason TEXT DEFAULT NULL,
|
||||
quality_score FLOAT DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY run_id (run_id)
|
||||
) {$charset_collate};";
|
||||
|
||||
// Stage-decision audit rows: verdict + rationale per pipeline
|
||||
// stage. citation_score is nullable until the Phase-2 editorial
|
||||
// loop computes it.
|
||||
$sql_run_decisions = "CREATE TABLE {$prefix}run_decisions (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
run_id VARCHAR(36) NOT NULL,
|
||||
stage VARCHAR(50) NOT NULL,
|
||||
verdict VARCHAR(50) NOT NULL,
|
||||
rationale TEXT DEFAULT NULL,
|
||||
citation_score FLOAT DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY run_id (run_id)
|
||||
) {$charset_collate};";
|
||||
|
||||
// Per-run ledger + lifecycle row. reserved/settled/ceiling drive the
|
||||
// Cost_Governor's atomic reserve-before-call check; pinned_prompts_json
|
||||
// freezes the prompt versions a run uses at start; status feeds the
|
||||
// run state machine (pending|running|done|failed|halted).
|
||||
$sql_runs = "CREATE TABLE {$prefix}runs (
|
||||
run_id VARCHAR(36) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
ceiling_usd DECIMAL(10,6) NOT NULL DEFAULT 0,
|
||||
reserved_usd DECIMAL(10,6) NOT NULL DEFAULT 0,
|
||||
settled_usd DECIMAL(10,6) NOT NULL DEFAULT 0,
|
||||
overage_usd DECIMAL(10,6) NOT NULL DEFAULT 0,
|
||||
pinned_prompts_json LONGTEXT DEFAULT NULL,
|
||||
started_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
finished_at DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (run_id),
|
||||
KEY status (status)
|
||||
) {$charset_collate};";
|
||||
|
||||
// Per-run per-stage state rows. The idempotency key is
|
||||
// (run_id, stage, agent_role, item_key): agent_role is the Phase-2
|
||||
// fan-out dimension (quorum members), item_key scopes article-level
|
||||
// stages within multi-article runs ('' for run-level stages).
|
||||
// meta_json holds the stage output for resume-without-recharge.
|
||||
$sql_run_stages = "CREATE TABLE {$prefix}run_stages (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
run_id VARCHAR(36) NOT NULL,
|
||||
stage VARCHAR(50) NOT NULL,
|
||||
agent_role VARCHAR(50) NOT NULL DEFAULT '',
|
||||
item_key VARCHAR(64) NOT NULL DEFAULT '',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
attempt SMALLINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
cost_usd DECIMAL(10,6) NOT NULL DEFAULT 0,
|
||||
meta_json LONGTEXT DEFAULT NULL,
|
||||
started_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
finished_at DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY run_stage_role_item (run_id, stage, agent_role, item_key),
|
||||
KEY status_updated (status, updated_at)
|
||||
) {$charset_collate};";
|
||||
|
||||
dbDelta( $sql_prompts );
|
||||
dbDelta( $sql_run_sources );
|
||||
dbDelta( $sql_run_decisions );
|
||||
dbDelta( $sql_runs );
|
||||
dbDelta( $sql_run_stages );
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,9 @@ class PRAutoBlogger_Schema_Installer {
|
||||
// Generation log — every API call with cost tracking. `run_id`
|
||||
// groups all entries from a single pipeline execution so
|
||||
// `link_generation_logs()` can accurately attribute costs to a post.
|
||||
// v0.18.0 adds the nullable audit columns `agent_role` and
|
||||
// `prompt_version` (dbDelta adds them additively on upgrade;
|
||||
// historical rows stay valid with NULL).
|
||||
$sql_generation_log = "CREATE TABLE {$prefix}generation_log (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
post_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
@@ -90,6 +93,8 @@ class PRAutoBlogger_Schema_Installer {
|
||||
request_json LONGTEXT DEFAULT NULL,
|
||||
response_status VARCHAR(20) NOT NULL DEFAULT 'success',
|
||||
error_message TEXT DEFAULT NULL,
|
||||
agent_role VARCHAR(50) DEFAULT NULL,
|
||||
prompt_version VARCHAR(20) DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY post_id (post_id),
|
||||
|
||||
172
includes/core/class-stage-display-map.php
Normal file
172
includes/core/class-stage-display-map.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
|
||||
*
|
||||
* Display map for every generation_log / run_stages `stage` value, old and new.
|
||||
*
|
||||
* What: Single source of truth that renders historical stage values
|
||||
* (analysis, outline, draft, polish, review, llm_research, image_a,
|
||||
* image_b, image_prompt_rewrite, opik_eval_judge), the Pipeline v2
|
||||
* vocabulary (research, curate, draft, editorial, seo, publish), and
|
||||
* any unknown value (humanized fallback) coherently. Also maps each
|
||||
* stage to its default agent role and primary prompt-registry key so
|
||||
* audit stamping never hardcodes those associations at call sites.
|
||||
* The `stage` column is VARCHAR — this PHP map is the vocabulary,
|
||||
* there is no SQL enum.
|
||||
* Who triggers it: Cost_Tracker (stamping), audit/metrics rendering, Run_State.
|
||||
* Dependencies: none — pure static lookup.
|
||||
*
|
||||
* @see core/class-cost-tracker.php — Derives agent_role / prompt key when not passed.
|
||||
* @see core/class-prompt-registry.php — Owns the prompt keys referenced here.
|
||||
* @see ARCHITECTURE.md — Stage vocabulary (canonical edit-stage name: polish).
|
||||
*/
|
||||
class PRAutoBlogger_Stage_Display_Map {
|
||||
|
||||
/**
|
||||
* Stage definitions: label (translated lazily), default agent role,
|
||||
* and the primary prompt-registry key the stage renders with (null
|
||||
* when the stage has no registry-managed prompt).
|
||||
*
|
||||
* @var array<string, array{label: string, role: ?string, prompt_key: ?string}>
|
||||
*/
|
||||
private const MAP = array(
|
||||
// ── Historical vocabulary (rows that exist on prod) ─────────────
|
||||
'analysis' => array(
|
||||
'label' => 'Topic Analysis',
|
||||
'role' => 'analyst',
|
||||
'prompt_key' => 'analysis.system',
|
||||
),
|
||||
'outline' => array(
|
||||
'label' => 'Outline',
|
||||
'role' => 'writer',
|
||||
'prompt_key' => 'content.outline',
|
||||
),
|
||||
'draft' => array(
|
||||
'label' => 'Draft',
|
||||
'role' => 'writer',
|
||||
'prompt_key' => 'content.draft',
|
||||
),
|
||||
'polish' => array(
|
||||
'label' => 'Polish',
|
||||
'role' => 'writer',
|
||||
'prompt_key' => 'content.polish',
|
||||
),
|
||||
'review' => array(
|
||||
'label' => 'Editorial Review',
|
||||
'role' => 'editor',
|
||||
'prompt_key' => 'editor.system',
|
||||
),
|
||||
'llm_research' => array(
|
||||
'label' => 'LLM Research',
|
||||
'role' => 'researcher',
|
||||
'prompt_key' => 'research.system',
|
||||
),
|
||||
'image_a' => array(
|
||||
'label' => 'Image A',
|
||||
'role' => 'illustrator',
|
||||
'prompt_key' => 'image.style_template',
|
||||
),
|
||||
'image_b' => array(
|
||||
'label' => 'Image B',
|
||||
'role' => 'illustrator',
|
||||
'prompt_key' => 'image.style_template',
|
||||
),
|
||||
'image_prompt_rewrite' => array(
|
||||
'label' => 'Image Prompt Rewrite',
|
||||
'role' => 'illustrator',
|
||||
'prompt_key' => 'image.rewriter_system',
|
||||
),
|
||||
'opik_eval_judge' => array(
|
||||
'label' => 'Opik Eval Judge',
|
||||
'role' => 'judge',
|
||||
'prompt_key' => null,
|
||||
),
|
||||
// ── Pipeline v2 vocabulary (new runs may write these) ───────────
|
||||
'research' => array(
|
||||
'label' => 'Research',
|
||||
'role' => 'researcher',
|
||||
'prompt_key' => 'research.system',
|
||||
),
|
||||
'curate' => array(
|
||||
'label' => 'Curate',
|
||||
'role' => 'curator',
|
||||
'prompt_key' => null,
|
||||
),
|
||||
'editorial' => array(
|
||||
'label' => 'Editorial Loop',
|
||||
'role' => 'editor',
|
||||
'prompt_key' => null,
|
||||
),
|
||||
'seo' => array(
|
||||
'label' => 'SEO',
|
||||
'role' => 'seo',
|
||||
'prompt_key' => null,
|
||||
),
|
||||
'publish' => array(
|
||||
'label' => 'Publish',
|
||||
'role' => 'publisher',
|
||||
'prompt_key' => null,
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Human-readable label for a stage value. Unknown stages are humanized
|
||||
* (underscores to spaces, words capitalized) so the audit view never
|
||||
* renders blank for a value this map predates or postdates.
|
||||
*
|
||||
* @param string $stage Raw stage value from the database.
|
||||
* @return string Translated display label.
|
||||
*/
|
||||
public static function label( string $stage ): string {
|
||||
if ( isset( self::MAP[ $stage ] ) ) {
|
||||
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- labels are a fixed const set.
|
||||
return __( self::MAP[ $stage ]['label'], 'prautoblogger' );
|
||||
}
|
||||
return ucwords( str_replace( array( '_', '-' ), ' ', $stage ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Default agent role recorded for a stage when the call site does not
|
||||
* pass one explicitly. Null for unknown stages (column stays NULL).
|
||||
*
|
||||
* @param string $stage Raw stage value.
|
||||
* @return string|null Agent role slug, or null when unknown.
|
||||
*/
|
||||
public static function default_agent_role( string $stage ): ?string {
|
||||
return self::MAP[ $stage ]['role'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary prompt-registry key a stage renders with. Used to resolve the
|
||||
* pinned prompt_version stamped on generation_log rows when the call
|
||||
* site does not pass an explicit key (e.g. single-pass passes
|
||||
* 'content.single_pass' for its 'draft' row).
|
||||
*
|
||||
* @param string $stage Raw stage value.
|
||||
* @return string|null Registry key, or null when the stage has none.
|
||||
*/
|
||||
public static function default_prompt_key( string $stage ): ?string {
|
||||
return self::MAP[ $stage ]['prompt_key'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the stage value is part of the known vocabulary.
|
||||
*
|
||||
* @param string $stage Raw stage value.
|
||||
* @return bool True when the stage is a known historical or v2 value.
|
||||
*/
|
||||
public static function is_known( string $stage ): bool {
|
||||
return isset( self::MAP[ $stage ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* The full vocabulary, for audit listings and tests.
|
||||
*
|
||||
* @return array<string, array{label: string, role: ?string, prompt_key: ?string}>
|
||||
*/
|
||||
public static function all(): array {
|
||||
return self::MAP;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user