true, 'executed' => 0, 'skipped' => 0, 'errors' => array(), 'actions' => array(), ); // Pre-flight: enabled? if ( ! self::is_enabled() ) { $result['ok'] = false; $result['errors'][] = 'automator disabled'; return $result; } // Pre-flight: cool-off? if ( ! self::cool_off_clear() ) { $result['ok'] = false; $result['errors'][] = 'cool-off active (critical health in last ' . self::COOL_OFF_DAYS . ' days)'; return $result; } // Iterate modules. if ( ! class_exists( 'TMDO_Feature_Flags' ) || ! class_exists( 'TMDO_FSM_Advisor' ) ) { $result['ok'] = false; $result['errors'][] = 'dependencies missing'; return $result; } $blacklist = (array) get_option( self::OPT_BLACKLIST, array() ); $last_actions = (array) get_option( self::OPT_LAST_ACTION, array() ); foreach ( TMDO_Feature_Flags::all() as $module => $current_state ) { // Blacklisted? if ( in_array( $module, $blacklist, true ) ) { ++$result['skipped']; continue; } // Get advisor recommendation. $advice = TMDO_FSM_Advisor::advise( $module ); if ( 'PROMOTE' !== ( $advice['action'] ?? '' ) || 'info' !== ( $advice['level'] ?? '' ) ) { ++$result['skipped']; continue; } $next_state = (string) ( $advice['next_state'] ?? '' ); if ( '' === $next_state ) { ++$result['skipped']; continue; } // Forbidden destructive transition? if ( self::is_forbidden_transition( $current_state, $next_state ) ) { ++$result['skipped']; continue; } // Recent action? if ( ! self::interval_clear( $module, $last_actions ) ) { ++$result['skipped']; continue; } // All checks passed — execute. $set_result = TMDO_Feature_Flags::set( $module, $next_state ); if ( true === $set_result ) { ++$result['executed']; $result['actions'][] = array( 'module' => $module, 'from' => $current_state, 'to' => $next_state, ); $last_actions[ $module ] = gmdate( 'Y-m-d H:i:s' ); if ( class_exists( 'TMDO_Logger' ) ) { TMDO_Logger::info( 'automator_promoted', array( 'module' => $module, 'from' => $current_state, 'to' => $next_state, ) ); } do_action( 'wpdo/automator_promoted', $module, $current_state, $next_state ); } else { $result['errors'][] = "{$module}: " . ( $set_result instanceof \WP_Error ? $set_result->get_error_code() : 'unknown' ); } } update_option( self::OPT_LAST_ACTION, $last_actions, false ); return $result; } // ─── settings accessors ───────────────────────────────────────────── /** * Return whether the automator is enabled. * * @return bool */ public static function is_enabled(): bool { return '1' === (string) get_option( self::OPT_ENABLED, '0' ); } /** * Return the list of blacklisted module slugs. * * @return array */ public static function blacklist(): array { return (array) get_option( self::OPT_BLACKLIST, array() ); } /** * Return the map of module → last automated action timestamp. * * @return array */ public static function last_actions(): array { return (array) get_option( self::OPT_LAST_ACTION, array() ); } // ─── private helpers ─────────────────────────────────────────────── /** * Cool-off: false when there's been a critical health alert in last N days. * * @return bool true = OK to act, false = blocked. */ private static function cool_off_clear(): bool { if ( ! class_exists( 'TMDO_Health_Cron' ) ) { return true; // No data — assume OK. } $last = TMDO_Health_Cron::get_last_run(); if ( ! is_array( $last ) ) { return true; } $crit = (int) ( $last['critical_count'] ?? 0 ); if ( 0 === $crit ) { return true; } $ts = isset( $last['ran_at'] ) ? strtotime( (string) $last['ran_at'] . ' UTC' ) : 0; if ( $ts <= 0 ) { return true; } // Critical alert exists; cool-off if within window. return ( time() - $ts ) > ( self::COOL_OFF_DAYS * DAY_IN_SECONDS ); } /** * Per-module interval: ≥ 24h since last automated action on this module. * * @param string $module Module slug. * @param array $last_actions Map of module → timestamp. * @return bool true = OK to act. */ private static function interval_clear( string $module, array $last_actions ): bool { $last = (string) ( $last_actions[ $module ] ?? '' ); if ( '' === $last ) { return true; } $ts = strtotime( $last . ' UTC' ); if ( $ts <= 0 ) { return true; } return ( time() - $ts ) >= ( self::MIN_INTERVAL_HOURS * HOUR_IN_SECONDS ); } /** * Check if (from, to) is in FORBIDDEN_TRANSITIONS. * * @param string $from Current state. * @param string $to Target state. * @return bool */ private static function is_forbidden_transition( string $from, string $to ): bool { foreach ( self::FORBIDDEN_TRANSITIONS as $pair ) { if ( $pair[0] === $from && $pair[1] === $to ) { return true; } } return false; } }