Files
prautoblogger/includes/core/class-prompt-registry-writer.php
peptiderepo c723af927b feat(prompts): versioned prompt registry, v1 seed, pinning + stamping
Pipeline v2 Phase 1, commit 2/4. Prompt_Registry (read) +
Prompt_Registry_Writer (immutable create/activate/seed) over the new
prompts table; canonical v1 bodies extracted verbatim into
Prompt_Defaults(+Editorial) — single source for BOTH the seed migration
(Migrate_Prompt_Seed_V0180, flag-gated, self-healing) and the in-code
fallback, so a missing table renders byte-identical prompts and never
fatals. Call sites swapped: Content_Prompts, Analysis_Prompts,
Chief_Editor, LLM_Research_Provider. Includes the image-composer seam
keys image.rewriter_system + image.style_template (composer PR consumes,
must not add its own prompt storage).

Pinning: Cost_Tracker::set_run_id() registers the process run context,
ensures the runs ledger row, and pins active versions once per run;
log_api_call/log_image_generation stamp prompt_version (run pins) +
agent_role (stage map) on every generation_log row. Single-pass keeps
stage 'draft' and stamps content.single_pass explicitly.

Under-300 refactors: Content_Generator's four duplicated span/log blocks
consolidated into execute_stage() (301 -> 224 lines, behavior
identical); get_avg_tokens_for_stages body moved to Cost_Reporter with a
delegating wrapper kept for API compatibility.

Tests: byte-identical fallback renders (single_pass/outline/polish/
editor.system), token filling, key coverage incl. stage-map integrity.

Agent-Session: pipeline-v2-phase1-engineer-20260612
2026-06-11 16:42:56 +07:00

144 lines
5.5 KiB
PHP

<?php
declare(strict_types=1);
/**
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
*
* Write side of the versioned prompt registry: create / activate / seed.
*
* What: Companion to PRAutoBlogger_Prompt_Registry (split for the 300-line
* cap). Owns every mutation of `wp_prautoblogger_prompts` and
* enforces the immutability rule: a stored body is NEVER updated —
* a change is a new (prompt_key, version) row, and `activate()`
* flips the single active flag per key. `seed_v1()` writes the
* canonical v0.16.0 texts as version 1 (idempotent — keys that
* already have rows keep their history). This is the API surface
* the Phase-2 in-admin Prompts editor will call; Phase 1 ships no UI.
* Who triggers it: PRAutoBlogger_Activator (seed migration on db 1.2.0),
* Phase-2 admin (create/activate), WP-CLI (future).
* Dependencies: WordPress $wpdb, PRAutoBlogger_Prompt_Registry (table name,
* availability probe, defs, cache flush).
*
* @see core/class-prompt-registry.php — Read side (render, pins, fallback).
* @see class-activator.php — Calls seed_v1() once, flag-gated.
*/
class PRAutoBlogger_Prompt_Registry_Writer {
/**
* All versions of a key, newest first (Phase-2 list/diff support).
*
* @param string $key Registry key.
* @return array<int, array<string, mixed>>
*/
public static function list_versions( string $key ): array {
if ( ! PRAutoBlogger_Prompt_Registry::is_available() ) {
return array();
}
global $wpdb;
$table = PRAutoBlogger_Prompt_Registry::table_name();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$rows = $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$table} WHERE prompt_key = %s ORDER BY version DESC", $key ),
ARRAY_A
);
return is_array( $rows ) ? $rows : array();
}
/**
* Create a NEW immutable version of a key and (optionally) activate it.
* Never mutates an existing row's body — that is the whole point.
*
* @param string $key Registry key.
* @param string $body Template body.
* @param string|null $model Optional model hint.
* @param array<string, mixed>|null $params Optional params snapshot.
* @param string $author Who created it (seed tag, login, agent).
* @param bool $activate Flip active to this version (default true).
* @return int New version number, or 0 on failure / table unavailable.
*/
public static function create_version( string $key, string $body, ?string $model, ?array $params, string $author, bool $activate = true ): int {
if ( ! PRAutoBlogger_Prompt_Registry::is_available() ) {
return 0;
}
global $wpdb;
$table = PRAutoBlogger_Prompt_Registry::table_name();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$max = (int) $wpdb->get_var(
$wpdb->prepare( "SELECT MAX(version) FROM {$table} WHERE prompt_key = %s", $key )
);
$next = $max + 1;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$ok = $wpdb->insert(
$table,
array(
'prompt_key' => $key,
'version' => $next,
'body' => $body,
'model' => $model,
'params_json' => null !== $params ? wp_json_encode( $params ) : null,
'author' => $author,
'created_at' => current_time( 'mysql' ),
'active' => 0,
)
);
if ( false === $ok ) {
return 0;
}
if ( $activate ) {
self::activate( $key, $next );
}
return $next;
}
/**
* Make one version the single active version of its key.
*
* @param string $key Registry key.
* @param int $version Version to activate.
* @return bool True when the version is now active.
*/
public static function activate( string $key, int $version ): bool {
if ( ! PRAutoBlogger_Prompt_Registry::is_available() ) {
return false;
}
global $wpdb;
$table = PRAutoBlogger_Prompt_Registry::table_name();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( $wpdb->prepare( "UPDATE {$table} SET active = 0 WHERE prompt_key = %s", $key ) );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$updated = $wpdb->query(
$wpdb->prepare( "UPDATE {$table} SET active = 1 WHERE prompt_key = %s AND version = %d", $key, $version )
);
PRAutoBlogger_Prompt_Registry::flush_cache();
return 1 === (int) $updated;
}
/**
* Seed every registry key with its canonical v1 body. Idempotent: keys
* that already have rows are left untouched (their history is theirs).
*
* Side effects: up to one INSERT + two UPDATEs per missing key.
*
* @param string $author Audit tag for the seed rows (e.g. 'seed:v0.18.0').
* @return int Number of keys seeded.
*/
public static function seed_v1( string $author = 'seed:v0.18.0' ): int {
if ( ! PRAutoBlogger_Prompt_Registry::is_available() ) {
return 0;
}
$seeded = 0;
foreach ( PRAutoBlogger_Prompt_Registry::defs() as $key => $def ) {
if ( ! empty( self::list_versions( $key ) ) ) {
continue;
}
$model = null !== $def['model_option'] ? (string) get_option( $def['model_option'], '' ) : null;
$version = self::create_version( $key, $def['body'], '' !== (string) $model ? $model : null, $def['params'], $author, true );
if ( $version > 0 ) {
++$seeded;
}
}
PRAutoBlogger_Prompt_Registry::flush_cache();
return $seeded;
}
}