Requirements 1-8 + all non-negotiables:
1. Model manager (CRUD): PRV_Model_Registry, prv_models v2 option, versioned
idempotent migration from v0.1.x flat-string format, Settings page CRUD.
2. Per-model run-health: PRV_Model_Registry::update_health() after each run;
health badges (Healthy/Retired?/Disabled/Unknown) in model table + dashboard.
3. Metric comparability: PRV_Config_Version stamps every run with config_version;
save-bar warning on scoring-relevant changes; orphaned rows excluded by version.
4. Projected cost at save: PRV_Config::get_projected_cost(); over-cap warning;
prv_last_run_truncated option + visible notice when cap hit mid-run.
5. Config surface: peptide set, intents, cadence (reschedules cron), budget cap;
all validated + sanitized; next-run timestamp shown.
6. API-key three-state: not-defined / defined-ok / defined-failed; derived from
ledger; never displays the key.
7. Run-lock: PRV_Run_Lock (transient); cron tick skips if locked; Run-now
refuses with clear message; lock released in finally block.
8. Design fixes: dark-ink focus ring on bright fills; disabled-row full AA
contrast (no opacity); toggle disabled on retired rows; save-bar-level
warning (role=alert); chip text #B4BDC7 6.83:1 AA; keyboard-sortable headers
(<button>+aria-sort); aria-live on Test chip; z-index:50 save-bar;
run-now double-fire guard; @media print light overrides.
New: PRV_Upgrader (runs migrations on plugins_loaded, not just activation),
PRV_Config_Version, PRV_Run_Lock, PRV_Settings_Page, PRV_Settings_Renderer,
PRV_Model_Manager_Table, PRV_Model_Test_Ajax.
Tests: test-model-registry, test-run-lock, test-config-version,
test-projected-cost, test-cron-reschedule (+ updated probe-runner,
uninstall, bootstrap).
Schema: config_version INT column added via dbDelta (idempotent).
Uninstall: purges all v0.2 prv_* options + transients.
178 lines
5.4 KiB
PHP
178 lines
5.4 KiB
PHP
<?php
|
|
/**
|
|
* Scoring-config versioning: stamps each run with the model*peptide*intent set.
|
|
*
|
|
* @package PrVision
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Tracks the scored config (models x peptides x intents) as a version record.
|
|
*
|
|
* Whenever the scoring-relevant config changes, a new config-version is stamped.
|
|
* Every run is linked to the active config-version at run time. The dashboard
|
|
* trendline uses this to draw break annotations at config changes, and the
|
|
* score query excludes rows from orphaned models/peptides outside the current
|
|
* config-version.
|
|
*
|
|
* Option layout:
|
|
* prv_config_versions: array of {version: int, hash: string, timestamp: int,
|
|
* models: string[], peptides: string[], intents: string[]}
|
|
* prv_active_config_version: int (the current version number)
|
|
*
|
|
* Who triggers: PRV_Upgrader::run() for initial seed; PRV_Settings_Page when
|
|
* scoring-relevant settings are saved.
|
|
* Dependencies: PRV_Model_Registry, PRV_Config.
|
|
*
|
|
* @see class-prv-upgrader.php -- Calls maybe_seed_initial_version().
|
|
* @see class-prv-settings-page.php -- Calls bump_version_if_changed() on save.
|
|
* @see class-prv-probe-runner.php -- Reads get_active_version() for run stamp.
|
|
* @package PrVision
|
|
*/
|
|
class PRV_Config_Version {
|
|
|
|
/**
|
|
* Option key for the version history array.
|
|
*/
|
|
const VERSIONS_KEY = 'prv_config_versions';
|
|
|
|
/**
|
|
* Option key for the currently active version number.
|
|
*/
|
|
const ACTIVE_KEY = 'prv_active_config_version';
|
|
|
|
/**
|
|
* Seed the initial version record if none exists yet.
|
|
*
|
|
* Safe to call repeatedly -- no-op when a version already exists.
|
|
*
|
|
* Side effects: May write prv_config_versions and prv_active_config_version.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function maybe_seed_initial_version(): void {
|
|
$versions = get_option( self::VERSIONS_KEY, array() );
|
|
if ( ! empty( $versions ) ) {
|
|
return;
|
|
}
|
|
self::create_version_record();
|
|
}
|
|
|
|
/**
|
|
* Compute the config hash from the current model/peptide/intent set.
|
|
*
|
|
* @return string MD5 of the sorted, serialised config components.
|
|
*/
|
|
public static function compute_hash(): string {
|
|
$models = PRV_Model_Registry::get_enabled_slugs();
|
|
$peptides = array_column( PRV_Config::get_peptides(), 'slug' );
|
|
$intents = PRV_Config::get_prompt_intents();
|
|
|
|
sort( $models );
|
|
sort( $peptides );
|
|
sort( $intents );
|
|
|
|
return md5( serialize( array( $models, $peptides, $intents ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
|
|
}
|
|
|
|
/**
|
|
* Bump to a new config version if the scoring config has changed.
|
|
*
|
|
* Call this after saving any scoring-relevant setting. Returns the new
|
|
* version number if a bump occurred, null otherwise.
|
|
*
|
|
* Side effects: May write prv_config_versions and prv_active_config_version.
|
|
*
|
|
* @return int|null New version number if bumped, null if unchanged.
|
|
*/
|
|
public static function bump_version_if_changed(): ?int {
|
|
$new_hash = self::compute_hash();
|
|
$versions = get_option( self::VERSIONS_KEY, array() );
|
|
$active = (int) get_option( self::ACTIVE_KEY, 0 );
|
|
|
|
foreach ( $versions as $v ) {
|
|
if ( isset( $v['version'] ) && (int) $v['version'] === $active && isset( $v['hash'] ) && $v['hash'] === $new_hash ) {
|
|
return null; // Config unchanged.
|
|
}
|
|
}
|
|
|
|
return self::create_version_record();
|
|
}
|
|
|
|
/**
|
|
* Get the active config version number.
|
|
*
|
|
* @return int Version number (1-based); 0 if not yet seeded.
|
|
*/
|
|
public static function get_active_version(): int {
|
|
return (int) get_option( self::ACTIVE_KEY, 0 );
|
|
}
|
|
|
|
/**
|
|
* Get all version records, newest first.
|
|
*
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public static function get_all_versions(): array {
|
|
$versions = get_option( self::VERSIONS_KEY, array() );
|
|
if ( ! is_array( $versions ) ) {
|
|
return array();
|
|
}
|
|
$sorted = $versions;
|
|
usort( $sorted, static fn( $a, $b ) => (int) ( $b['version'] ?? 0 ) - (int) ( $a['version'] ?? 0 ) );
|
|
return $sorted;
|
|
}
|
|
|
|
/**
|
|
* Check if the given hash differs from the current active config.
|
|
*
|
|
* Used by the Settings page to warn before saving a scoring-relevant change.
|
|
*
|
|
* @param string $proposed_hash Hash to check.
|
|
*
|
|
* @return bool True when the config would change.
|
|
*/
|
|
public static function would_change( string $proposed_hash ): bool {
|
|
$versions = get_option( self::VERSIONS_KEY, array() );
|
|
$active = (int) get_option( self::ACTIVE_KEY, 0 );
|
|
foreach ( $versions as $v ) {
|
|
if ( isset( $v['version'] ) && (int) $v['version'] === $active ) {
|
|
return isset( $v['hash'] ) && $v['hash'] !== $proposed_hash;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Create a new version record and activate it.
|
|
*
|
|
* Side effects: Updates prv_config_versions and prv_active_config_version.
|
|
*
|
|
* @return int The new version number.
|
|
*/
|
|
private static function create_version_record(): int {
|
|
$versions = get_option( self::VERSIONS_KEY, array() );
|
|
if ( ! is_array( $versions ) ) {
|
|
$versions = array();
|
|
}
|
|
$new_version = count( $versions ) + 1;
|
|
$models = PRV_Model_Registry::get_enabled_slugs();
|
|
$peptides = array_column( PRV_Config::get_peptides(), 'slug' );
|
|
$intents = PRV_Config::get_prompt_intents();
|
|
|
|
$versions[] = array(
|
|
'version' => $new_version,
|
|
'hash' => self::compute_hash(),
|
|
'timestamp' => time(),
|
|
'models' => $models,
|
|
'peptides' => $peptides,
|
|
'intents' => $intents,
|
|
);
|
|
|
|
update_option( self::VERSIONS_KEY, $versions );
|
|
update_option( self::ACTIVE_KEY, $new_version );
|
|
return $new_version;
|
|
}
|
|
}
|