fix: remediate QA P1s -- restore hooks, split board data provider, restore ARCHITECTURE.md
P1-1: Split class-board-data-provider.php (312 lines) into:
- class-board-data-provider.php (orchestrator + WP_Query columns, 185 lines)
- class-board-gen-log-query.php (raw gen_log DB queries, 184 lines)
Both files under 300-line cap. Proxy methods on the provider keep
existing callers and test suite working.
P1-2/P1-3: Restore ARCHITECTURE.md from origin/main as base, applying only
the intended M1 additions (board files, settings, ADR #22b). Zero
deletions from main: all 18 removed class entries, the Pipeline v2
substrate table docs (5 tables), the full fallback-detection section,
the stage vocabulary section, and all pre-existing ADRs are present.
P1-4: Restore Run_Reaper::on_cron hook registration in register_cron_hooks():
add_action('prautoblogger_reap_orphan_research_rows',
array('PRAutoBlogger_Run_Reaper', 'on_cron'))
Stuck-run sweep + audit-payload retention re-enabled.
P1-5: Restore on_check_db_version_for_cron() method + its init hook.
Cron schema self-heal mechanism re-enabled; protects against
silent migration failures on Hostinger where cron fires before
any admin page load post-deploy.
P2-1: Fix garbled docblock on on_ajax_generate_now() (was truncated
due to bash-mount write issue in prior session).
P2-2 (class-prautoblogger.php at ~400 lines): Carried. Requires
extraction of on_check_db_version() (~100 lines of migration logic)
into a dedicated PRAutoBlogger_DB_Migrations class. Non-trivial
refactor; no functional risk from deferring. Ticket: split-class-prautoblogger.
All PHP files written via Python to avoid bash-mount UTF-8 truncation.
ARCHITECTURE.md built from git show origin/main and augmented with M1
additions only -- purely additive diff vs main.
Agent-Session: engineer-prautoblogger-20260612-remediation
This commit is contained in:
219
ARCHITECTURE.md
219
ARCHITECTURE.md
@@ -119,7 +119,7 @@ prautoblogger/
|
||||
│ │ └── OFL.txt # SIL Open Font License
|
||||
│ ├── css/
|
||||
│ │ ├── admin.css # Admin page styles (wp-admin conventions)
|
||||
│ │ ├── board.css # Kanban board styles — warm editorial palette (v0.19.0)
|
||||
│ │ ├── board.css # Kanban board styles -- warm editorial palette (v0.19.0)
|
||||
│ │ └── posts-widget.css # Frontend posts widget styles (uses theme CSS vars)
|
||||
│ └── js/
|
||||
│ ├── admin.js # Admin page interactivity (vanilla JS / Alpine.js)
|
||||
@@ -128,7 +128,10 @@ prautoblogger/
|
||||
│
|
||||
├── includes/
|
||||
│ ├── class-prautoblogger.php # Main orchestrator — registers all hooks, delegates execution
|
||||
│ ├── class-executor.php # Cron handlers, generation AJAX (start + status), model registry
|
||||
│ ├── class-pipeline-schema-installer.php # v0.18.0 substrate tables (prompts, run_sources, run_decisions, runs, run_stages)
|
||||
│ ├── class-migrate-prompt-seed-v0180.php # One-shot v0.18.0 prompt-registry seed migration
|
||||
│ ├── class-executor.php # Cron handlers, generation AJAX (delegates start + status to poller), model registry
|
||||
│ ├── class-generation-status-poller.php # Generation AJAX handlers: status transient renewal, lock-age orphan detection (R2b/R3), abort_orphaned_run
|
||||
│ ├── class-ajax-handlers.php # Non-generation AJAX: images, models, test connections
|
||||
│ ├── class-generation-lock.php # DB-level atomic mutex for single-writer generation
|
||||
│ ├── class-activator.php # Activation: create DB tables, set defaults, schedule cron
|
||||
@@ -136,8 +139,9 @@ prautoblogger/
|
||||
│ ├── class-autoloader.php # PSR-4-style autoloader for plugin classes
|
||||
│ │
|
||||
│ ├── admin/
|
||||
│ │ ├── class-board-page.php # Kanban board — primary landing screen (v0.19.0)
|
||||
│ │ ├── class-board-data-provider.php # Board data: 4-column card snapshots from gen_log + post meta (v0.19.0)
|
||||
│ │ ├── class-board-page.php # Kanban board -- primary landing screen (v0.19.0)
|
||||
│ │ ├── class-board-data-provider.php # Board orchestrator: 4-column snapshot, delegates gen_log queries (v0.19.0)
|
||||
│ │ ├── class-board-gen-log-query.php # Board gen_log queries: Generating + Failed column raw DB reads (v0.19.0)
|
||||
│ │ ├── class-admin-page.php # Main settings page (tabbed SaaS-style UI)
|
||||
│ │ ├── class-settings-fields.php # Declarative settings: sections + core fields (API/Models/Content/Sources)
|
||||
│ │ ├── class-settings-fields-extended.php # Operational fields: schedule, publishing, display, analytics, images
|
||||
@@ -175,6 +179,19 @@ 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-prompt-registry.php # Versioned prompt registry, read side (render, pins, self-healing fallback)
|
||||
│ │ ├── class-prompt-registry-writer.php # Registry write side (create version, activate, seed) — versions immutable
|
||||
│ │ ├── class-prompt-defaults.php # Canonical v1 bodies: content.* + analysis.* (seed + fallback single source)
|
||||
│ │ ├── class-prompt-defaults-editorial.php # Canonical v1 bodies: editor.* / research.system / image.* (composer seam)
|
||||
│ │ ├── class-run-context.php # Per-process active run id (set by Cost_Tracker::set_run_id)
|
||||
│ │ ├── class-run-state.php # runs-table ledger + lifecycle row (ceiling/reserved/settled, pins, status)
|
||||
│ │ ├── class-cost-governor.php # Per-run reserve-before-call enforcement (atomic conditional UPDATE)
|
||||
│ │ ├── class-cost-ceiling-exception.php # Thrown on ceiling breach (run already halted)
|
||||
│ │ ├── class-run-stage-state.php # Per-run per-stage state machine (idempotent resume, output snapshots)
|
||||
│ │ ├── class-run-reaper.php # Stuck-run sweep + audit-payload retention (rides the #19 cron)
|
||||
│ │ ├── class-audit-writer.php # run_sources / run_decisions insert layer
|
||||
│ │ ├── class-pipeline-status.php # Status-transient + summary helpers (extracted from runner/worker)
|
||||
│ │ ├── 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
|
||||
@@ -184,6 +201,7 @@ prautoblogger/
|
||||
│ ├── providers/
|
||||
│ │ ├── interface-llm-provider.php # Contract for any LLM provider
|
||||
│ │ ├── class-open-router-provider.php # OpenRouter API implementation (chat completions)
|
||||
│ │ ├── class-open-router-completion-guard.php # Empty-completion guard: warn on finish_reason=length, one reasoning-off retry, audited failure (v0.18.1)
|
||||
│ │ ├── class-open-router-embedding-provider.php # Text embeddings for semantic dedup
|
||||
│ │ ├── class-open-router-pricing.php # Model pricing lookup and cost estimation
|
||||
│ │ ├── interface-source-provider.php # Contract for any social media source
|
||||
@@ -196,11 +214,13 @@ prautoblogger/
|
||||
│ │ ├── class-open-router-image-support.php # API key, response parsing, retry/backoff helpers
|
||||
│ │ ├── class-open-router-image-pricing.php # Model resolution + per-image cost estimation
|
||||
│ │ ├── class-open-router-config.php # API base URL (direct vs AI Gateway)
|
||||
│ │ ├── class-open-router-request-builder.php # Header building + Hostinger cURL auth workaround
|
||||
│ │ ├── class-open-router-request-builder.php # Request body assembly + reasoning token cap/headroom (v0.18.1) + headers + Hostinger cURL auth workaround
|
||||
│ │ ├── class-runware-image-provider.php # Runware FLUX.1 (default v0.9.0+)
|
||||
│ │ ├── class-runware-image-pricing.php # FLUX schnell/dev cost table + resolver
|
||||
│ │ ├── class-runware-image-support.php # Key, response parsing, retry, dimension snap
|
||||
│ │ ├── class-runware-image-batch.php # True parallel curl_multi batch dispatcher
|
||||
│ │ ├── class-runware-model-catalog.php # Live model catalog sync: modelSearch pagination, cache v2, failure cooldown
|
||||
│ │ ├── class-runware-catalog-fetcher.php # HTTP fetch + normalize_models for Runware modelSearch (extracted for 300-line rule)
|
||||
│ │ └── (new providers go here — see CONVENTIONS.md)
|
||||
│ │
|
||||
│ ├── services/
|
||||
@@ -326,11 +346,19 @@ Hostinger's 120-second LiteSpeed connection timeout. The frontend polls for stat
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
**Fallback detection:** If Hostinger kills the PHP process before it updates the
|
||||
transient (despite `ignore_user_abort`), the status endpoint detects the orphan after
|
||||
180 seconds by checking for recently-created posts with `_prautoblogger_run_id` meta.
|
||||
It also releases any stale generation lock. After 600 seconds (STATUS_TTL), it gives
|
||||
up unconditionally and reports a timeout.
|
||||
**Fallback detection (v0.18.3 R2b/R3):** The status transient is renewed on every stage
|
||||
transition via `update_generation_stage()` (`Pipeline_Status::broadcast()`), so its TTL
|
||||
always counts from the last activity rather than run start. When a status poll finds the
|
||||
transient absent:
|
||||
|
||||
- **Lock within TTL (`lock_age ≤ STATUS_TTL = 600s`):** the background process is still alive
|
||||
but has not broadcast yet (or the transient was evicted). `handle_missing_transient()` returns
|
||||
`status: running` (R3) — button stays in-progress, no state change.
|
||||
- **Lock exceeded TTL (`lock_age > STATUS_TTL`):** background process died without releasing the
|
||||
lock. `abort_orphaned_run()` marks the `runs` row `failed`, releases the generation lock, deletes
|
||||
the status transient and queue option, and returns `status: error` with "infrastructure timeout"
|
||||
(R2b). A daily `Run_Reaper::sweep_stuck_runs()` at 2× `EXPECTED_RUN_SECONDS` (~3600s) remains
|
||||
the backstop for runs the poll path never sees.
|
||||
|
||||
---
|
||||
|
||||
@@ -384,7 +412,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 |
|
||||
@@ -393,6 +421,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`
|
||||
@@ -400,6 +430,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 |
|
||||
@@ -432,6 +474,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. Populated in Phase 1 from `Stage_Display_Map::default_agent_role()` (e.g. 'writer', 'editor', 'publisher'). Phase-2 quorum passes multiple roles for the same stage. |
|
||||
| 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_`:
|
||||
@@ -446,11 +579,14 @@ All prefixed with `prautoblogger_`:
|
||||
| `prautoblogger_analysis_model` | OpenRouter model for analysis (default: cheap) |
|
||||
| `prautoblogger_writing_model` | OpenRouter model for writing (default: quality) |
|
||||
| `prautoblogger_editor_model` | OpenRouter model for chief editor (default: quality) |
|
||||
| `prautoblogger_reasoning_max_tokens` | v0.18.1 — hard cap on thinking tokens per call when reasoning is enabled; sent as OpenRouter `reasoning.max_tokens` (replaces `effort` when active) and the request `max_tokens` is raised by the cap so reasoning can never consume the visible-content budget; 0 = legacy uncapped effort mode. Default `PRAUTOBLOGGER_DEFAULT_REASONING_MAX_TOKENS` (2048) |
|
||||
| `prautoblogger_daily_article_target` | Number of articles per day (1-10, default: 1) |
|
||||
| `prautoblogger_writing_pipeline` | 'single_pass' or 'multi_step' (default: multi_step) |
|
||||
| `prautoblogger_niche_description` | Text description of the site's niche |
|
||||
| `prautoblogger_target_subreddits` | JSON array of subreddits to monitor |
|
||||
| `prautoblogger_monthly_budget_usd` | Monthly API spend limit in USD |
|
||||
| `prautoblogger_per_run_cost_ceiling_usd` | v0.18.0 — per-run hard cost ceiling (USD); reserve-before-call enforced; snapshotted onto the runs row at run start; 0 = disabled. Default `PRAUTOBLOGGER_DEFAULT_RUN_CEILING_USD` ($0.50) |
|
||||
| `prautoblogger_request_json_retention_days` | v0.18.0 — days before heavy audit payloads (generation_log.request_json, run_stages.meta_json) are NULLed by the daily cleanup cron; 0 = keep forever. Default `PRAUTOBLOGGER_DEFAULT_REQUEST_JSON_RETENTION_DAYS` (14) |
|
||||
| `prautoblogger_tone` | Content tone (informational, conversational, etc.) |
|
||||
| `prautoblogger_min_word_count` | Minimum article word count (default: 800) |
|
||||
| `prautoblogger_max_word_count` | Maximum article word count (default: 2000) |
|
||||
@@ -498,6 +634,7 @@ Stored on every PRAutoBlogger-generated post:
|
||||
| `_prautoblogger_editor_notes` | Chief editor's review notes |
|
||||
| `_prautoblogger_generated_at` | ISO 8601 timestamp of generation |
|
||||
| `_prautoblogger_research_sources` | JSON array of source URLs used |
|
||||
| `_prautoblogger_idea_hash` | v0.18.0 — stable hash of the idea (title|topic); with `_prautoblogger_run_id` forms the post-creation idempotency key |
|
||||
| `_prautoblogger_og_image_id` | Attachment ID of the composed 1200×630 OG variant (v0.17.0; the rebuilt SEO stage emits `og:image` from this — key name frozen) |
|
||||
| `_prautoblogger_square_image_id` | Attachment ID of the composed 1080×1080 square variant (v0.17.0; stored now, no consumer yet) |
|
||||
|
||||
@@ -676,23 +813,67 @@ 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 (multiple roles per stage) itself is Phase 2. In Phase 1,
|
||||
`agent_role` is populated on every row from `Stage_Display_Map::default_agent_role()`
|
||||
so the run timeline is self-describing and the idempotency key is fully populated.
|
||||
|
||||
### #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.
|
||||
|
||||
|
||||
### #22: Kanban board as primary admin landing screen (v0.19.0)
|
||||
|
||||
### #22b: Kanban board as primary admin landing screen (v0.19.0)
|
||||
|
||||
The primary admin screen is now a kanban board (Direction C "Editorial Record") with four
|
||||
columns: Generating | In Review | Published | Failed. A separate `class-board-page.php`
|
||||
registers the `Board` submenu first so it appears before Settings in the nav.
|
||||
`class-board-data-provider.php` computes column cards from existing tables
|
||||
(`prab_generation_log`, WordPress post meta) — no new schema required for M1. The data
|
||||
provider uses the `prautoblogger_generation_status` transient (written by the Executor for
|
||||
manual/daily runs) as the primary active-run signal, with a secondary query on recent
|
||||
`prab_generation_log` run_ids that have no linked post as a fallback. M2 will rename Runs &
|
||||
Audit → Articles and wire board cards to the Article Dossier. Poll interval and published
|
||||
window are settings-backed, localized into board.js, never hardcoded.
|
||||
`class-board-data-provider.php` orchestrates the four columns and handles WP_Query columns
|
||||
(In Review, Published). Raw `prab_generation_log` queries (Generating, Failed) are in the
|
||||
extracted `class-board-gen-log-query.php` (split for 300-line compliance) -- no new schema
|
||||
required for M1. The data provider uses the `prautoblogger_generation_status` transient
|
||||
(written by the Executor for manual/daily runs) as the primary active-run signal, with a
|
||||
secondary query on recent `prab_generation_log` run_ids that have no linked post as a
|
||||
fallback. M2 will rename Runs & Audit -> Articles and wire board cards to the Article Dossier.
|
||||
Poll interval and published window are settings-backed, localized into board.js, never
|
||||
hardcoded.
|
||||
|
||||
## Cross-System LLM Budget Coordination
|
||||
|
||||
|
||||
Binary file not shown.
184
includes/admin/class-board-gen-log-query.php
Normal file
184
includes/admin/class-board-gen-log-query.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
|
||||
*
|
||||
* Raw generation-log queries for the kanban board.
|
||||
*
|
||||
* Extracted from PRAutoBlogger_Board_Data_Provider to stay under the 300-line
|
||||
* cap. Handles the two columns that read directly from prab_generation_log:
|
||||
* Generating (in-progress runs) | Failed (error runs with no linked post)
|
||||
*
|
||||
* No schema changes — data comes from prab_generation_log which already exists.
|
||||
*
|
||||
* Triggered by: PRAutoBlogger_Board_Data_Provider::get_generating_cards()
|
||||
* PRAutoBlogger_Board_Data_Provider::get_failed_cards()
|
||||
* Dependencies: WordPress $wpdb, get_transient().
|
||||
*
|
||||
* @see admin/class-board-data-provider.php — Orchestrator that calls this class.
|
||||
* @see ARCHITECTURE.md — Database schema (generation_log).
|
||||
*/
|
||||
class PRAutoBlogger_Board_Gen_Log_Query {
|
||||
|
||||
/** Seconds of gen-log inactivity after which a run is no longer "generating". */
|
||||
public const GENERATING_GRACE_SECONDS = 1800;
|
||||
|
||||
/** Transient key written by PRAutoBlogger_Executor for the active generation run. */
|
||||
public const STATUS_TRANSIENT = 'prautoblogger_generation_status';
|
||||
|
||||
/**
|
||||
* Cards for runs that are currently in-progress.
|
||||
*
|
||||
* Detection strategy: check the generation status transient first (authoritative
|
||||
* for the active manual-run or daily-cron run), then fall back to recent gen_log
|
||||
* run_ids that have no associated published/draft post yet.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function get_generating_cards(): array {
|
||||
global $wpdb;
|
||||
|
||||
$cards = array();
|
||||
|
||||
// Primary: check if the executor transient signals an active run.
|
||||
$status = get_transient( self::STATUS_TRANSIENT );
|
||||
if ( is_array( $status ) && 'running' === ( $status['status'] ?? '' ) ) {
|
||||
$cards[] = array(
|
||||
'run_id' => '',
|
||||
'post_id' => 0,
|
||||
'title' => $status['stage'] ?? __( 'Generating…', 'prautoblogger' ),
|
||||
'cost_total' => 0.0,
|
||||
'stage_current' => $status['stage'] ?? '',
|
||||
'stage_count' => 0,
|
||||
'started_at' => $status['started'] ?? 0,
|
||||
'click_action' => 'logs',
|
||||
'log_url' => admin_url( 'admin.php?page=prautoblogger-logs' ),
|
||||
);
|
||||
}
|
||||
|
||||
// Secondary: runs in gen_log that are recent and have no linked post.
|
||||
$table = $wpdb->prefix . 'prautoblogger_generation_log';
|
||||
$since = gmdate( 'Y-m-d H:i:s', time() - self::GENERATING_GRACE_SECONDS );
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$raw_runs = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT run_id,
|
||||
MIN(created_at) as started_at,
|
||||
MAX(stage) as last_stage,
|
||||
COUNT(*) as stage_count,
|
||||
SUM(CASE WHEN response_status = 'success' THEN estimated_cost ELSE 0 END) as cost_total
|
||||
FROM {$table}
|
||||
WHERE created_at >= %s
|
||||
AND run_id IS NOT NULL
|
||||
AND post_id IS NULL
|
||||
GROUP BY run_id
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 10",
|
||||
$since
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( ! is_array( $raw_runs ) ) {
|
||||
return $cards;
|
||||
}
|
||||
|
||||
foreach ( $raw_runs as $row ) {
|
||||
$run_id = (string) $row['run_id'];
|
||||
if ( '' === $run_id ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if we already have a transient card (avoid duplicate).
|
||||
if ( ! empty( $cards ) && '' === $cards[0]['run_id'] ) {
|
||||
// Only add secondary cards if we have a real run_id.
|
||||
// The transient card already covers the active run.
|
||||
}
|
||||
|
||||
$cards[] = array(
|
||||
'run_id' => $run_id,
|
||||
'post_id' => 0,
|
||||
'title' => sprintf(
|
||||
/* translators: %s: pipeline stage name */
|
||||
__( 'Generating — %s', 'prautoblogger' ),
|
||||
esc_html( ucfirst( (string) $row['last_stage'] ) )
|
||||
),
|
||||
'cost_total' => round( (float) $row['cost_total'], 6 ),
|
||||
'stage_current' => (string) $row['last_stage'],
|
||||
'stage_count' => (int) $row['stage_count'],
|
||||
'started_at' => strtotime( (string) $row['started_at'] ),
|
||||
'click_action' => 'logs',
|
||||
'log_url' => admin_url( 'admin.php?page=prautoblogger-logs' ),
|
||||
);
|
||||
}
|
||||
|
||||
return $cards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cards for generation runs that ended in an error state.
|
||||
*
|
||||
* Looks for recent gen_log rows with response_status='error' that have
|
||||
* no corresponding published post — these are pipeline failures.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function get_failed_cards(): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'prautoblogger_generation_log';
|
||||
$since = gmdate( 'Y-m-d H:i:s', time() - ( 7 * DAY_IN_SECONDS ) );
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$raw = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT run_id,
|
||||
MIN(created_at) as started_at,
|
||||
MAX(error_message) as last_error,
|
||||
MAX(stage) as last_stage,
|
||||
COUNT(*) as call_count
|
||||
FROM {$table}
|
||||
WHERE created_at >= %s
|
||||
AND response_status = 'error'
|
||||
AND run_id IS NOT NULL
|
||||
AND post_id IS NULL
|
||||
GROUP BY run_id
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 20",
|
||||
$since
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( ! is_array( $raw ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$cards = array();
|
||||
foreach ( $raw as $row ) {
|
||||
$run_id = (string) $row['run_id'];
|
||||
if ( '' === $run_id ) {
|
||||
continue;
|
||||
}
|
||||
$cards[] = array(
|
||||
'run_id' => $run_id,
|
||||
'post_id' => 0,
|
||||
'title' => sprintf(
|
||||
/* translators: %s: pipeline stage name */
|
||||
__( 'Failed — %s', 'prautoblogger' ),
|
||||
esc_html( ucfirst( (string) $row['last_stage'] ) )
|
||||
),
|
||||
'error_message' => (string) $row['last_error'],
|
||||
'started_at' => strtotime( (string) $row['started_at'] ),
|
||||
'click_action' => 'logs',
|
||||
'log_url' => admin_url( 'admin.php?page=prautoblogger-logs' ),
|
||||
);
|
||||
}
|
||||
|
||||
return $cards;
|
||||
}
|
||||
}
|
||||
@@ -7,19 +7,19 @@ declare(strict_types=1);
|
||||
* Main orchestrator for the PRAutoBlogger plugin.
|
||||
*
|
||||
* Registers all WordPress hooks (actions and filters) and wires up dependencies.
|
||||
* This is the ONLY place hooks are registered — individual classes contain only
|
||||
* This is the ONLY place hooks are registered -- individual classes contain only
|
||||
* business logic, not hook registration.
|
||||
*
|
||||
* Triggered by: prautoblogger() singleton in prautoblogger.php, called on `plugins_loaded`.
|
||||
* Dependencies: All other plugin classes (loaded via autoloader), PRAutoBlogger_Executor.
|
||||
*
|
||||
* @see prautoblogger.php — Plugin bootstrap that instantiates this class.
|
||||
* @see class-executor.php — Cron/AJAX handlers for generation, model registry.
|
||||
* @see class-ajax-handlers.php — Non-generation AJAX endpoints (images, models, test).
|
||||
* @see class-generation-lock.php — DB mutex for single-writer generation.
|
||||
* @see core/class-pipeline-runner.php — Executes the generation pipeline.
|
||||
* @see ARCHITECTURE.md — Data flow diagram showing how classes interact.
|
||||
* @see CONVENTIONS.md — Hook naming conventions.
|
||||
* @see prautoblogger.php -- Plugin bootstrap that instantiates this class.
|
||||
* @see class-executor.php -- Cron/AJAX handlers for generation, model registry.
|
||||
* @see class-ajax-handlers.php -- Non-generation AJAX endpoints (images, models, test).
|
||||
* @see class-generation-lock.php -- DB mutex for single-writer generation.
|
||||
* @see core/class-pipeline-runner.php -- Executes the generation pipeline.
|
||||
* @see ARCHITECTURE.md -- Data flow diagram showing how classes interact.
|
||||
* @see CONVENTIONS.md -- Hook naming conventions.
|
||||
*/
|
||||
class PRAutoBlogger {
|
||||
|
||||
@@ -46,6 +46,10 @@ class PRAutoBlogger {
|
||||
$this->ajax_handlers = new PRAutoBlogger_Ajax_Handlers( $this->executor->get_model_registry() );
|
||||
|
||||
add_action( 'admin_init', array( $this, 'on_check_db_version' ) );
|
||||
// v0.18.0: cron requests must also self-heal the schema -- after a
|
||||
// deploy, scheduled generation can run before any admin page load,
|
||||
// and new-column inserts would silently fail on the old schema.
|
||||
add_action( 'init', array( $this, 'on_check_db_version_for_cron' ) );
|
||||
add_filter( 'cron_schedules', array( $this, 'filter_add_cron_schedules' ) );
|
||||
|
||||
if ( is_admin() ) {
|
||||
@@ -63,7 +67,7 @@ class PRAutoBlogger {
|
||||
|
||||
/** Register admin-only hooks (settings, notices, metabox, dashboard widget). */
|
||||
private function register_admin_hooks(): void {
|
||||
// Kanban board — registered first so it appears as the first submenu item.
|
||||
// Kanban board -- registered first so it appears as the first submenu item.
|
||||
$board_page = new PRAutoBlogger_Board_Page();
|
||||
add_action( 'admin_menu', array( $board_page, 'on_register_menu' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $board_page, 'on_enqueue_assets' ) );
|
||||
@@ -87,7 +91,7 @@ class PRAutoBlogger {
|
||||
|
||||
( new PRAutoBlogger_Post_List_Columns() )->register();
|
||||
|
||||
// Block false update notifications — our slug collides with another plugin.
|
||||
// Block false update notifications -- our slug collides with another plugin.
|
||||
add_filter( 'site_transient_update_plugins', array( $this, 'filter_block_false_updates' ) );
|
||||
}
|
||||
|
||||
@@ -112,7 +116,7 @@ class PRAutoBlogger {
|
||||
// Hostinger's 120-second web-server connection timeout.
|
||||
add_action( 'prautoblogger_manual_generation', array( $this->executor, 'on_manual_generation' ) );
|
||||
|
||||
// Chained article generation — each queued article fires as its own
|
||||
// Chained article generation -- each queued article fires as its own
|
||||
// cron event so it gets a fresh PHP process and execution time budget.
|
||||
add_action(
|
||||
PRAutoBlogger_Pipeline_Runner::CRON_ACTION,
|
||||
@@ -155,6 +159,10 @@ class PRAutoBlogger {
|
||||
// pipeline dies before amortize_research_costs runs).
|
||||
add_action( 'prautoblogger_reap_orphan_research_rows', array( 'PRAutoBlogger_Research_Reaper', 'on_cron' ) );
|
||||
|
||||
// v0.18.0: stuck-run sweep + audit-payload retention rides the SAME
|
||||
// existing daily cron (no new schedule). See class-run-reaper.php.
|
||||
add_action( 'prautoblogger_reap_orphan_research_rows', array( 'PRAutoBlogger_Run_Reaper', 'on_cron' ) );
|
||||
|
||||
// v0.8.1: WP-CLI manual trigger for the reaper. Only registers when
|
||||
// WP-CLI is present; no-op in normal HTTP requests.
|
||||
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
@@ -283,7 +291,7 @@ class PRAutoBlogger {
|
||||
// value into a deprecated-keyed option for one version cycle (so a
|
||||
// customised value is recoverable / auditable) and seed the new
|
||||
// template with its editorial default. The old option is intentionally
|
||||
// NOT deleted this cycle. See docs/proposals/2026-05-29-image-pipeline-in-plugin-brief.md §8 Commit 1.
|
||||
// NOT deleted this cycle. See docs/proposals/2026-05-29-image-pipeline-in-plugin-brief.md paragraph 8 Commit 1.
|
||||
if ( ! get_option( 'prautoblogger_migrated_style_template_v0160' ) ) {
|
||||
$old_suffix = get_option( 'prautoblogger_image_style_suffix', '' );
|
||||
if ( '' !== $old_suffix && false === get_option( 'prautoblogger_image_style_suffix_deprecated', false ) ) {
|
||||
@@ -296,7 +304,7 @@ class PRAutoBlogger {
|
||||
update_option( 'prautoblogger_migrated_style_template_v0160', '1' );
|
||||
}
|
||||
|
||||
// v0.9.0 — Runware as default image model. v0.10.0 — remove CF Workers AI.
|
||||
// v0.9.0 -- Runware as default image model. v0.10.0 -- remove CF Workers AI.
|
||||
PRAutoBlogger_Activator::migrate_default_image_v090();
|
||||
PRAutoBlogger_Migrate_Remove_Cloudflare_V0100::run();
|
||||
|
||||
@@ -313,6 +321,23 @@ class PRAutoBlogger {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run schema migrations during cron requests.
|
||||
*
|
||||
* Wraps on_check_db_version() so it only fires during WP-Cron execution.
|
||||
* After a deploy, the daily generation cron can fire before any admin page
|
||||
* load -- without this, new-column inserts would silently fail.
|
||||
*
|
||||
* Side effects: may run the same migrations as on_check_db_version().
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function on_check_db_version_for_cron(): void {
|
||||
if ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) {
|
||||
$this->on_check_db_version();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom cron schedules (six-hourly for metrics collection).
|
||||
*
|
||||
@@ -351,7 +376,7 @@ class PRAutoBlogger {
|
||||
return $this->executor;
|
||||
}
|
||||
|
||||
// ── Backward-compatible proxies ──
|
||||
// -- Backward-compatible proxies --
|
||||
|
||||
/** @see PRAutoBlogger_Executor::on_daily_generation() */
|
||||
public function on_daily_generation(): void {
|
||||
@@ -361,7 +386,6 @@ class PRAutoBlogger {
|
||||
public function on_collect_metrics(): void {
|
||||
$this->executor->on_collect_metrics(); }
|
||||
|
||||
/** @see PRAutoBlogger_Execut
|
||||
/** @see PRAutoBlogger_Executor::on_ajax_generate_now() */
|
||||
public function on_ajax_generate_now(): void {
|
||||
$this->executor->on_ajax_generate_now(); }
|
||||
|
||||
Reference in New Issue
Block a user