From 809e7176ad2fbb49c6477ec868314c68eb0bdd46 Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Thu, 11 Jun 2026 03:57:36 +0000 Subject: [PATCH] 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 --- ARCHITECTURE.md | 149 +++++++++++++++- CHANGELOG.md | 27 +++ includes/class-activator.php | 9 +- includes/class-pipeline-schema-installer.php | 147 ++++++++++++++++ includes/class-schema-installer.php | 5 + includes/core/class-stage-display-map.php | 172 +++++++++++++++++++ prautoblogger.php | 12 +- tests/unit/Core/StageDisplayMapTest.php | 98 +++++++++++ uninstall.php | 17 +- 9 files changed, 627 insertions(+), 9 deletions(-) mode change 100755 => 100644 ARCHITECTURE.md mode change 100755 => 100644 includes/class-activator.php create mode 100644 includes/class-pipeline-schema-installer.php create mode 100644 includes/core/class-stage-display-map.php create mode 100644 tests/unit/Core/StageDisplayMapTest.php mode change 100755 => 100644 uninstall.php diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md old mode 100755 new mode 100644 index 374c0fb..8ad8766 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ac26d..e55e28b 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/includes/class-activator.php b/includes/class-activator.php old mode 100755 new mode 100644 index 958de6c..11a109b --- a/includes/class-activator.php +++ b/includes/class-activator.php @@ -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(); } /** diff --git a/includes/class-pipeline-schema-installer.php b/includes/class-pipeline-schema-installer.php new file mode 100644 index 0000000..aa76082 --- /dev/null +++ b/includes/class-pipeline-schema-installer.php @@ -0,0 +1,147 @@ +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 ); + } +} diff --git a/includes/class-schema-installer.php b/includes/class-schema-installer.php index 260c295..ec61810 100644 --- a/includes/class-schema-installer.php +++ b/includes/class-schema-installer.php @@ -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), diff --git a/includes/core/class-stage-display-map.php b/includes/core/class-stage-display-map.php new file mode 100644 index 0000000..59355ae --- /dev/null +++ b/includes/core/class-stage-display-map.php @@ -0,0 +1,172 @@ + + */ + 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 + */ + public static function all(): array { + return self::MAP; + } +} diff --git a/prautoblogger.php b/prautoblogger.php index 3b8c519..9da92fd 100644 --- a/prautoblogger.php +++ b/prautoblogger.php @@ -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 ); diff --git a/tests/unit/Core/StageDisplayMapTest.php b/tests/unit/Core/StageDisplayMapTest.php new file mode 100644 index 0000000..a141cc6 --- /dev/null +++ b/tests/unit/Core/StageDisplayMapTest.php @@ -0,0 +1,98 @@ +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' ) ); + } +} diff --git a/uninstall.php b/uninstall.php old mode 100755 new mode 100644 index 9ee44f5..634d8e6 --- a/uninstall.php +++ b/uninstall.php @@ -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 ); }