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:
149
ARCHITECTURE.md
Executable file → Normal file
149
ARCHITECTURE.md
Executable file → Normal file
@@ -126,6 +126,7 @@ prautoblogger/
|
||||
│
|
||||
├── includes/
|
||||
│ ├── class-prautoblogger.php # Main orchestrator — registers all hooks, delegates execution
|
||||
│ ├── class-pipeline-schema-installer.php # v0.18.0 substrate tables (prompts, run_sources, run_decisions, runs, run_stages)
|
||||
│ ├── class-executor.php # Cron handlers, generation AJAX (start + status), model registry
|
||||
│ ├── class-ajax-handlers.php # Non-generation AJAX: images, models, test connections
|
||||
│ ├── class-generation-lock.php # DB-level atomic mutex for single-writer generation
|
||||
@@ -171,6 +172,7 @@ prautoblogger/
|
||||
│ │ ├── class-image-attacher.php # Sideload + cost log + variant meta-linking + caption prepend
|
||||
│ │ ├── class-image-media-sideloader.php # Imports images into WordPress media library
|
||||
│ │ ├── class-cost-tracker.php # Logs all API costs, enforces budget limits
|
||||
│ │ ├── class-stage-display-map.php # Stage vocabulary: labels + default roles + prompt keys (old + new + image stages)
|
||||
│ │ ├── class-logger.php # Structured logging singleton (error/warning/info/debug)
|
||||
│ │ ├── class-article-worker.php # Single-article generation (content + edit + publish)
|
||||
│ │ ├── class-pipeline-runner.php # Orchestrates pipeline; chains per-article cron jobs
|
||||
@@ -379,7 +381,7 @@ All tables use `$wpdb->prefix` + `prautoblogger_` prefix.
|
||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||
| post_id | BIGINT UNSIGNED | WordPress post ID (nullable, set after publish)|
|
||||
| run_id | VARCHAR(36) | UUID linking all entries from one pipeline run |
|
||||
| stage | VARCHAR(50) | 'analysis', 'outline', 'draft', 'edit', 'review' |
|
||||
| stage | VARCHAR(50) | See "Stage vocabulary" below. Canonical edit-stage name is **'polish'** (this doc previously said 'edit'; the code always wrote 'polish') |
|
||||
| provider | VARCHAR(50) | 'openrouter' |
|
||||
| model | VARCHAR(100) | Model identifier used |
|
||||
| prompt_tokens | INT | Input tokens consumed |
|
||||
@@ -388,6 +390,8 @@ All tables use `$wpdb->prefix` + `prautoblogger_` prefix.
|
||||
| request_json | LONGTEXT | Full request payload (for debugging) |
|
||||
| response_status | VARCHAR(20) | 'success', 'error', 'timeout' |
|
||||
| error_message | TEXT | Error details if failed (nullable) |
|
||||
| agent_role | VARCHAR(50) | v0.18.0, nullable. Role that made the call ('writer', 'editor', 'analyst', 'researcher', 'illustrator', …). Historical rows stay NULL |
|
||||
| prompt_version | VARCHAR(20) | v0.18.0, nullable. Pinned prompt-registry version the call rendered with. Historical rows stay NULL |
|
||||
| created_at | DATETIME | When the API call was made |
|
||||
|
||||
- INDEX: `post_id`
|
||||
@@ -395,6 +399,18 @@ All tables use `$wpdb->prefix` + `prautoblogger_` prefix.
|
||||
- INDEX: `created_at`
|
||||
- INDEX: `stage`
|
||||
|
||||
**Stage vocabulary.** `stage` is a VARCHAR, not a SQL enum; the vocabulary lives in
|
||||
`PRAutoBlogger_Stage_Display_Map` (PHP), which renders historical and new values coherently
|
||||
and maps each stage to its default agent role + primary prompt-registry key:
|
||||
|
||||
- **Historical values (exist on prod):** `analysis`, `outline`, `draft`, `polish`, `review`,
|
||||
`llm_research`, `image_a`, `image_b`, `image_prompt_rewrite`, `opik_eval_judge`.
|
||||
Note: single-pass generation logs its one call as `draft` (no `outline` row — that absence
|
||||
is how single-pass and multi-step runs are told apart in cost reporting).
|
||||
- **Pipeline v2 values (new runs may write):** `research`, `curate`, `draft`, `editorial`,
|
||||
`seo`, `publish`.
|
||||
- Unknown values render via a humanizing fallback — the audit view never renders blank.
|
||||
|
||||
#### `prab_event_log` — Structured application logging
|
||||
|
||||
| Column | Type | Description |
|
||||
@@ -427,6 +443,97 @@ All tables use `$wpdb->prefix` + `prautoblogger_` prefix.
|
||||
- INDEX: `measured_at`
|
||||
- INDEX: `composite_score`
|
||||
|
||||
#### Pipeline v2 substrate tables (v0.18.0, db 1.2.0)
|
||||
|
||||
Real table names use the standard prefix, e.g. `wp_prautoblogger_prompts` (the plan-of-record's
|
||||
`prab_*` names are shorthand). Created by `PRAutoBlogger_Pipeline_Schema_Installer`; all are
|
||||
dropped on uninstall.
|
||||
|
||||
##### `prautoblogger_prompts` — versioned prompt registry
|
||||
|
||||
| Column | Type | Description |
|
||||
|-------------|-----------------|----------------------------------------------------------|
|
||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||
| prompt_key | VARCHAR(64) | Registry key, e.g. 'content.single_pass' (`key` is reserved SQL) |
|
||||
| version | INT UNSIGNED | Monotonic per key; **rows are immutable** — edits create new versions |
|
||||
| body | LONGTEXT | Prompt template; `{{ token }}` placeholders filled at render time |
|
||||
| model | VARCHAR(100) | Optional model hint (informational in Phase 1) |
|
||||
| params_json | LONGTEXT | Optional params snapshot (temperature, max_tokens, …) |
|
||||
| author | VARCHAR(100) | Who created the version ('seed:v0.18.0', wp user login, agent) |
|
||||
| created_at | DATETIME | Version creation time |
|
||||
| active | TINYINT(1) | Exactly one active row per key |
|
||||
|
||||
- UNIQUE: `prompt_key, version` · INDEX: `prompt_key, active`
|
||||
|
||||
##### `prautoblogger_run_sources` — per-run source audit
|
||||
|
||||
| Column | Type | Description |
|
||||
|---------------|--------------|----------------------------------------------|
|
||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||
| run_id | VARCHAR(36) | Pipeline run UUID |
|
||||
| agent_role | VARCHAR(50) | Role that surfaced the source |
|
||||
| source_url | VARCHAR(500) | Source URL (nullable) |
|
||||
| doi | VARCHAR(255) | DOI when known (nullable) |
|
||||
| kept | TINYINT(1) | Keep/discard decision |
|
||||
| reason | TEXT | Why kept or discarded (nullable) |
|
||||
| quality_score | FLOAT | Source quality weighting (nullable) |
|
||||
| created_at | DATETIME | Row creation time |
|
||||
|
||||
- INDEX: `run_id` — consolidates the old `source_ids_json` + `_prautoblogger_research_sources`
|
||||
scatter for **new** runs; historical runs are not backfilled.
|
||||
|
||||
##### `prautoblogger_run_decisions` — per-stage decision audit
|
||||
|
||||
| Column | Type | Description |
|
||||
|----------------|-------------|-----------------------------------------------|
|
||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||
| run_id | VARCHAR(36) | Pipeline run UUID |
|
||||
| stage | VARCHAR(50) | Stage that decided (see stage vocabulary) |
|
||||
| verdict | VARCHAR(50) | e.g. 'approved', 'revised', 'rejected', 'halted' |
|
||||
| rationale | TEXT | Decision rationale (nullable) |
|
||||
| citation_score | FLOAT | Nullable until the Phase-2 editorial loop computes it |
|
||||
| created_at | DATETIME | Row creation time |
|
||||
|
||||
- INDEX: `run_id`
|
||||
|
||||
##### `prautoblogger_runs` — run ledger + lifecycle
|
||||
|
||||
| Column | Type | Description |
|
||||
|---------------------|---------------|---------------------------------------------|
|
||||
| run_id | VARCHAR(36) | PK — pipeline run UUID |
|
||||
| status | VARCHAR(20) | pending / running / done / failed / halted |
|
||||
| ceiling_usd | DECIMAL(10,6) | Per-run cost ceiling snapshotted at run start |
|
||||
| reserved_usd | DECIMAL(10,6) | Outstanding reserve-before-call holds |
|
||||
| settled_usd | DECIMAL(10,6) | Actual settled spend |
|
||||
| overage_usd | DECIMAL(10,6) | Amount the breaching reservation exceeded the ceiling by |
|
||||
| pinned_prompts_json | LONGTEXT | Map of prompt_key → version frozen at run start |
|
||||
| started_at | DATETIME | Run start |
|
||||
| updated_at | DATETIME | Last ledger/state write |
|
||||
| finished_at | DATETIME | Set on done/failed/halted (nullable) |
|
||||
|
||||
- INDEX: `status` — the Cost_Governor's reserve is a single conditional UPDATE against this
|
||||
row (same atomic discipline as the #10 generation lock), so concurrent `curl_multi`
|
||||
writers cannot slip past the ceiling between check and call.
|
||||
|
||||
##### `prautoblogger_run_stages` — per-run per-stage state machine
|
||||
|
||||
| Column | Type | Description |
|
||||
|-------------|-----------------|----------------------------------------------------|
|
||||
| id | BIGINT UNSIGNED | Auto-increment PK |
|
||||
| run_id | VARCHAR(36) | Pipeline run UUID |
|
||||
| stage | VARCHAR(50) | Stage name (see stage vocabulary) |
|
||||
| agent_role | VARCHAR(50) | Fan-out dimension for Phase-2 quorum ('' default) |
|
||||
| item_key | VARCHAR(64) | Scopes article-level stages within multi-article runs ('' for run-level) |
|
||||
| status | VARCHAR(20) | pending / running / done / failed / halted |
|
||||
| attempt | SMALLINT UNSIGNED | Re-entry counter |
|
||||
| cost_usd | DECIMAL(10,6) | Cost attributed to the stage |
|
||||
| meta_json | LONGTEXT | Stage output snapshot — lets resume reuse a done stage instead of re-charging it (payload pruned on the retention cron) |
|
||||
| started_at | DATETIME | First entry |
|
||||
| updated_at | DATETIME | Last transition |
|
||||
| finished_at | DATETIME | Set on done/failed (nullable) |
|
||||
|
||||
- UNIQUE: `run_id, stage, agent_role, item_key` (the idempotency key) · INDEX: `status, updated_at`
|
||||
|
||||
### WordPress Options (`wp_options`)
|
||||
|
||||
All prefixed with `prautoblogger_`:
|
||||
@@ -669,6 +776,46 @@ We already use Cloudflare for DNS/CDN on peptiderepo.com, so layering AI Gateway
|
||||
|
||||
---
|
||||
|
||||
### #22: Pipeline v2 Phase 1 substrate (v0.18.0, db 1.2.0, June 2026)
|
||||
|
||||
Phase 1 of the CEO-approved pipeline rebuild (plan of record:
|
||||
`convo/cpo/threads/2026-06-prautoblogger-pipeline-redesign/03-cto-revised-brief.md`), built
|
||||
**on the current pipeline with no behavior change** to the Economy (single-pass) and
|
||||
multi-step publish paths. Four subsystems:
|
||||
|
||||
1. **Versioned prompt registry** (`prautoblogger_prompts`). All pipeline prompt copy
|
||||
(content/analysis/editor/research + the illustration rewriter prompt and the image Style
|
||||
Template) lives as immutable versioned rows; the hardcoded v0.16.0 texts are seeded as v1
|
||||
and remain the in-code fallback, so a missing/empty table can never fatal — call sites
|
||||
render through `PRAutoBlogger_Prompt_Registry` and fall back to the same constants the
|
||||
seed used (byte-identical output). A run pins the active version of every key at start
|
||||
(`runs.pinned_prompts_json`) and every generation_log row is stamped with the pinned
|
||||
`prompt_version`. The image-composer PR **consumes** the `image.*` keys — it must not
|
||||
grow its own prompt storage. No admin UI in Phase 1; the API supports list/activate/
|
||||
create-version (never mutate) for the Phase-2 Prompts screen.
|
||||
2. **Additive audit schema.** `agent_role` + `prompt_version` columns on generation_log;
|
||||
`run_sources` + `run_decisions` child tables; `PRAutoBlogger_Stage_Display_Map` renders
|
||||
historical + image + v2 stage values. `request_json` (and stage output payloads in
|
||||
`run_stages.meta_json`) are nulled after R days — R from the
|
||||
`prautoblogger_request_json_retention_days` setting (default 14), pruned on the existing
|
||||
daily reaper cron.
|
||||
3. **Per-run cost governor** (`PRAutoBlogger_Cost_Governor`). Reserve-before-call against the
|
||||
per-run ledger row using a single conditional UPDATE (the #10 atomic-write discipline);
|
||||
estimates come from the #18 pricing chain; `curl_multi` image batches reserve their summed
|
||||
estimate before dispatch; reservations settle to actuals after each response. Breach →
|
||||
run `halted`, overage recorded, un-dispatched work aborted, surfaced on the Review Queue.
|
||||
Ceiling = `prautoblogger_per_run_cost_ceiling_usd` setting (default $0.50, 0 = disabled).
|
||||
The monthly `Cost_Tracker` cap and the Cloudflare AI Gateway (#15) path are unchanged.
|
||||
4. **Run state machine + idempotency** (`PRAutoBlogger_Run_State`). Per-run per-stage state
|
||||
(pending/running/done/failed/halted) keyed `run_id + stage + agent_role + item_key`
|
||||
(item_key scopes article-level stages because one run_id spans all N articles of a batch);
|
||||
done stages persist output and are reused on re-entry, never re-charged; post creation is
|
||||
keyed by `_prautoblogger_run_id` + `_prautoblogger_idea_hash` (check-before-insert) so a
|
||||
retried run cannot duplicate a post. `PRAutoBlogger_Run_Reaper` (same daily cron as #19)
|
||||
marks runs stuck `running` > 2× expected stage wall-clock as `failed`; such runs render
|
||||
as **"incomplete"** in audit queries. Quorum logic itself is Phase 2 — the schema just
|
||||
reserves its dimensions.
|
||||
|
||||
### #20: PHPUnit test infrastructure + WordPress-Core PHPCS compliance (v0.10.1)
|
||||
|
||||
Tests use Brain\Monkey for WordPress function mocking (no database required). The BaseTestCase singleton handles setup/teardown of Monkey stubs and provides fixture helpers for common data shapes (SourceData, ArticleIdea, GenerationLog, etc.). All WordPress functions used by the plugin are stubbed in BaseTestCase::setUp() so individual test classes only override what they need. In v0.10.1, missing stubs for `post_type_exists()` were added to support PeptideLinker's PR-Core guards. All 1,362 WordPress-Core coding-standards violations (short array syntax, line length, spacing) were auto-fixed via phpcbf and the CI gate was flipped to strict mode — PHPCS failures now block CI. This prevents future regressions on the code style front.
|
||||
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -5,6 +5,33 @@ All notable changes to PRAutoBlogger will be documented in this file.
|
||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project uses [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [0.18.0] - 2026-06-12
|
||||
|
||||
Pipeline v2 — Phase 1 substrate (plan of record: CPO thread
|
||||
`2026-06-prautoblogger-pipeline-redesign`, seq 3). Built on the current pipeline with **no
|
||||
behavior change** to the Economy (single-pass) and multi-step publish paths.
|
||||
|
||||
### Added
|
||||
- **Schema v1.2.0** (`prautoblogger_db_version` 1.1.0 → 1.2.0, idempotent dbDelta
|
||||
migrations, self-healing on version mismatch): new tables
|
||||
`wp_prautoblogger_prompts` (versioned prompt registry),
|
||||
`wp_prautoblogger_run_sources` + `wp_prautoblogger_run_decisions` (audit child tables),
|
||||
`wp_prautoblogger_runs` (per-run cost ledger + lifecycle) and
|
||||
`wp_prautoblogger_run_stages` (per-run per-stage state machine, idempotency key
|
||||
`run_id + stage + agent_role + item_key`); additive nullable columns `agent_role` and
|
||||
`prompt_version` on `wp_prautoblogger_generation_log` (historical rows stay valid).
|
||||
- `PRAutoBlogger_Stage_Display_Map`: single PHP vocabulary for historical stages
|
||||
(`analysis`, `outline`, `draft`, `polish`, `review`, `llm_research`, `image_a`, `image_b`,
|
||||
`image_prompt_rewrite`, `opik_eval_judge`), Pipeline v2 stages (`research`, `curate`,
|
||||
`draft`, `editorial`, `seo`, `publish`) and a humanizing fallback for unknown values, plus
|
||||
per-stage default agent role and primary prompt-registry key.
|
||||
- Uninstall now drops the five substrate tables and clears every plugin cron hook
|
||||
(previously only two of eight were cleared).
|
||||
|
||||
### Fixed
|
||||
- ARCHITECTURE.md documented the multi-step edit stage as `edit`; the code has always
|
||||
written `polish`. Canonicalized to **`polish`** everywhere.
|
||||
|
||||
## [0.17.0] - 2026-06-11
|
||||
|
||||
### Added
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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.17.0
|
||||
* Version: 0.18.0
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* Author: PeptideRepo
|
||||
@@ -35,14 +35,20 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
| Defined here so every file in the plugin can reference paths, versions,
|
||||
| and limits without magic strings.
|
||||
*/
|
||||
define( 'PRAUTOBLOGGER_VERSION', '0.17.0' );
|
||||
define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' );
|
||||
define( 'PRAUTOBLOGGER_VERSION', '0.18.0' );
|
||||
define( 'PRAUTOBLOGGER_DB_VERSION', '1.2.0' );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'PRAUTOBLOGGER_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||
// API and retry limits — named constants instead of magic numbers.
|
||||
define( 'PRAUTOBLOGGER_MAX_RETRIES', 3 );
|
||||
// Pipeline v2 Phase 1 (v0.18.0) defaults. Both are SETTINGS-backed at every
|
||||
// read site (prautoblogger_per_run_cost_ceiling_usd /
|
||||
// prautoblogger_request_json_retention_days); these constants exist only as
|
||||
// the named get_option() fallbacks — never inline the literals.
|
||||
define( 'PRAUTOBLOGGER_DEFAULT_RUN_CEILING_USD', 0.50 );
|
||||
define( 'PRAUTOBLOGGER_DEFAULT_REQUEST_JSON_RETENTION_DAYS', 14 );
|
||||
define( 'PRAUTOBLOGGER_RETRY_BASE_DELAY_SECONDS', 2 );
|
||||
define( 'PRAUTOBLOGGER_API_TIMEOUT_SECONDS', 180 );
|
||||
define( 'PRAUTOBLOGGER_CACHE_TTL_SECONDS', 3600 );
|
||||
|
||||
98
tests/unit/Core/StageDisplayMapTest.php
Normal file
98
tests/unit/Core/StageDisplayMapTest.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for PRAutoBlogger_Stage_Display_Map.
|
||||
*
|
||||
* Locks the stage vocabulary contract from the Pipeline v2 Phase 1 brief:
|
||||
* every historical prod stage value (incl. the image stages) and every
|
||||
* Pipeline v2 stage value must render, carry a default agent role, and —
|
||||
* where applicable — map to a prompt-registry key. Unknown values must
|
||||
* humanize, never render blank.
|
||||
*
|
||||
* @package PRAutoBlogger\Tests\Core
|
||||
*/
|
||||
|
||||
namespace PRAutoBlogger\Tests\Core;
|
||||
|
||||
use PRAutoBlogger\Tests\BaseTestCase;
|
||||
|
||||
class StageDisplayMapTest extends BaseTestCase {
|
||||
|
||||
/**
|
||||
* Every historical stage value observed on prod (90d window, 2026-06-11)
|
||||
* plus the code-only opik_eval_judge must be known to the map.
|
||||
*/
|
||||
public function test_all_historical_stages_are_known(): void {
|
||||
$historical = array(
|
||||
'analysis',
|
||||
'outline',
|
||||
'draft',
|
||||
'polish',
|
||||
'review',
|
||||
'llm_research',
|
||||
'image_a',
|
||||
'image_b',
|
||||
'image_prompt_rewrite',
|
||||
'opik_eval_judge',
|
||||
);
|
||||
foreach ( $historical as $stage ) {
|
||||
$this->assertTrue(
|
||||
\PRAutoBlogger_Stage_Display_Map::is_known( $stage ),
|
||||
"Historical stage '{$stage}' must be in the display map."
|
||||
);
|
||||
$this->assertNotSame( '', \PRAutoBlogger_Stage_Display_Map::label( $stage ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Pipeline v2 vocabulary must be known to the map.
|
||||
*/
|
||||
public function test_all_v2_stages_are_known(): void {
|
||||
foreach ( array( 'research', 'curate', 'draft', 'editorial', 'seo', 'publish' ) as $stage ) {
|
||||
$this->assertTrue( \PRAutoBlogger_Stage_Display_Map::is_known( $stage ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unknown stages humanize instead of rendering blank.
|
||||
*/
|
||||
public function test_unknown_stage_humanizes(): void {
|
||||
$this->assertSame(
|
||||
'Some Future Stage',
|
||||
\PRAutoBlogger_Stage_Display_Map::label( 'some_future_stage' )
|
||||
);
|
||||
$this->assertFalse( \PRAutoBlogger_Stage_Display_Map::is_known( 'some_future_stage' ) );
|
||||
$this->assertNull( \PRAutoBlogger_Stage_Display_Map::default_agent_role( 'some_future_stage' ) );
|
||||
$this->assertNull( \PRAutoBlogger_Stage_Display_Map::default_prompt_key( 'some_future_stage' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage → default agent role mapping (stamped on generation_log rows
|
||||
* when the call site does not pass a role).
|
||||
*/
|
||||
public function test_default_agent_roles(): void {
|
||||
$this->assertSame( 'analyst', \PRAutoBlogger_Stage_Display_Map::default_agent_role( 'analysis' ) );
|
||||
$this->assertSame( 'writer', \PRAutoBlogger_Stage_Display_Map::default_agent_role( 'draft' ) );
|
||||
$this->assertSame( 'writer', \PRAutoBlogger_Stage_Display_Map::default_agent_role( 'polish' ) );
|
||||
$this->assertSame( 'editor', \PRAutoBlogger_Stage_Display_Map::default_agent_role( 'review' ) );
|
||||
$this->assertSame( 'researcher', \PRAutoBlogger_Stage_Display_Map::default_agent_role( 'llm_research' ) );
|
||||
$this->assertSame( 'illustrator', \PRAutoBlogger_Stage_Display_Map::default_agent_role( 'image_a' ) );
|
||||
$this->assertSame( 'illustrator', \PRAutoBlogger_Stage_Display_Map::default_agent_role( 'image_prompt_rewrite' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage → primary prompt-registry key (resolves the pinned
|
||||
* prompt_version stamped on rows). The image keys are the composer-PR
|
||||
* seam and must not change without coordinating on the thread.
|
||||
*/
|
||||
public function test_default_prompt_keys(): void {
|
||||
$this->assertSame( 'analysis.system', \PRAutoBlogger_Stage_Display_Map::default_prompt_key( 'analysis' ) );
|
||||
$this->assertSame( 'content.outline', \PRAutoBlogger_Stage_Display_Map::default_prompt_key( 'outline' ) );
|
||||
$this->assertSame( 'content.draft', \PRAutoBlogger_Stage_Display_Map::default_prompt_key( 'draft' ) );
|
||||
$this->assertSame( 'content.polish', \PRAutoBlogger_Stage_Display_Map::default_prompt_key( 'polish' ) );
|
||||
$this->assertSame( 'editor.system', \PRAutoBlogger_Stage_Display_Map::default_prompt_key( 'review' ) );
|
||||
$this->assertSame( 'research.system', \PRAutoBlogger_Stage_Display_Map::default_prompt_key( 'llm_research' ) );
|
||||
$this->assertSame( 'image.style_template', \PRAutoBlogger_Stage_Display_Map::default_prompt_key( 'image_a' ) );
|
||||
$this->assertSame( 'image.style_template', \PRAutoBlogger_Stage_Display_Map::default_prompt_key( 'image_b' ) );
|
||||
$this->assertSame( 'image.rewriter_system', \PRAutoBlogger_Stage_Display_Map::default_prompt_key( 'image_prompt_rewrite' ) );
|
||||
}
|
||||
}
|
||||
17
uninstall.php
Executable file → Normal file
17
uninstall.php
Executable file → Normal file
@@ -31,6 +31,12 @@ $tables = array(
|
||||
$prefix . 'generation_log',
|
||||
$prefix . 'content_scores',
|
||||
$prefix . 'event_log',
|
||||
// Pipeline v2 substrate (v0.18.0 / db 1.2.0).
|
||||
$prefix . 'prompts',
|
||||
$prefix . 'run_sources',
|
||||
$prefix . 'run_decisions',
|
||||
$prefix . 'runs',
|
||||
$prefix . 'run_stages',
|
||||
);
|
||||
|
||||
foreach ( $tables as $table ) {
|
||||
@@ -81,11 +87,14 @@ $wpdb->query(
|
||||
$hooks = array(
|
||||
'prautoblogger_daily_generation',
|
||||
'prautoblogger_collect_metrics',
|
||||
'prautoblogger_reap_orphan_research_rows',
|
||||
'prautoblogger_sync_runware_models',
|
||||
'prautoblogger_manual_generation',
|
||||
'prautoblogger_generate_queued_article',
|
||||
'prautoblogger_generate_from_idea',
|
||||
'prautoblogger_opik_dispatch',
|
||||
);
|
||||
|
||||
foreach ( $hooks as $hook ) {
|
||||
$timestamp = wp_next_scheduled( $hook );
|
||||
if ( false !== $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, $hook );
|
||||
}
|
||||
wp_clear_scheduled_hook( $hook );
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user