'Daily', self::WEEKLY => 'Weekly']; public static function register(): void { add_action(self::HOOK, [self::class, 'run_scheduled']); add_filter('cron_schedules', [self::class, 'cron_schedules']); } public static function cron_schedules(array $schedules): array { $schedules[self::WEEKLY] = ['interval' => WEEK_IN_SECONDS, 'display' => __('Once weekly', 'agentic-bridge')]; return $schedules; } public static function settings(): array { $v = TheDotDevAgenticBridge_Storage::get()['local_rechecks'] ?? []; return is_array($v) ? $v : []; } public static function configure(bool $enabled, string $frequency): void { $frequency = array_key_exists($frequency, self::FREQUENCIES) ? $frequency : self::WEEKLY; $prior = self::settings(); if (function_exists('wp_clear_scheduled_hook')) wp_clear_scheduled_hook(self::HOOK); $next = 0; if ($enabled && function_exists('wp_schedule_event')) { $next = time() + HOUR_IN_SECONDS; $scheduled = wp_schedule_event($next, $frequency, self::HOOK); if ($scheduled === false) { TheDotDevAgenticBridge_Storage::update(['local_rechecks' => array_merge($prior, [ 'enabled' => false, 'frequency' => $frequency, 'nextRun' => 0, 'lastError' => 'schedule_failed', 'lastOutcome' => 'failed', 'lastFailureAt' => TheDotDevAgenticBridge_Readiness_Clock::iso(), ])]); return; } } TheDotDevAgenticBridge_Storage::update(['local_rechecks' => array_merge($prior, ['enabled' => $enabled, 'frequency' => $frequency, 'nextRun' => $next, 'lastRun' => (string) ($prior['lastRun'] ?? ''), 'lastError' => ''])]); } public static function run_scheduled(): void { self::run('scheduled_local'); } public static function run(string $source = 'manual_local'): void { $settings = self::settings(); $now = time(); if (!empty($settings['lockUntil']) && (int) $settings['lockUntil'] > $now) return; $settings['lockUntil'] = $now + 300; $settings['lockOwner'] = $source; TheDotDevAgenticBridge_Storage::update(['local_rechecks' => $settings]); try { $before = TheDotDevAgenticBridge_Readiness_Store::latest(); $scanner = new TheDotDevAgenticBridge_Readiness_Scanner(); $after = apply_filters('thedotdev_agentic_bridge_local_recheck_result', $scanner->run_all(), $source); if (!is_array($after)) { throw new UnexpectedValueException('Local recheck result was not an array.'); } $after['recheckSource'] = $source; $after['drift'] = self::classify($before, $after); TheDotDevAgenticBridge_Readiness_Store::save_result($after); $settings['lastRun'] = TheDotDevAgenticBridge_Readiness_Clock::iso(); $settings['lastError'] = ''; $settings['lastOutcome'] = 'completed'; $settings['lastSnapshot'] = self::snapshot($after, $source); $history = is_array($settings['snapshots'] ?? null) ? $settings['snapshots'] : []; array_unshift($history, $settings['lastSnapshot']); $settings['snapshots'] = array_slice($history, 0, self::MAX_SNAPSHOTS); } catch (Throwable $e) { $settings['lastError'] = 'local_recheck_failed'; $settings['lastOutcome'] = 'failed'; $settings['lastFailureAt'] = TheDotDevAgenticBridge_Readiness_Clock::iso(); } $settings['lockUntil'] = 0; $settings['lockOwner'] = ''; $settings['nextRun'] = function_exists('wp_next_scheduled') ? (int) wp_next_scheduled(self::HOOK) : 0; TheDotDevAgenticBridge_Storage::update(['local_rechecks' => $settings]); } public static function deactivate(): void { if (function_exists('wp_clear_scheduled_hook')) wp_clear_scheduled_hook(self::HOOK); } /** @return array */ private static function snapshot(array $after, string $source): array { return [ 'at' => TheDotDevAgenticBridge_Readiness_Clock::iso(), 'source' => $source, 'scope' => 'this_wordpress_site', 'freshness' => (string) ($after['scannedAt'] ?? ''), 'exclusions' => [], 'score' => $after['score'] ?? null, 'findings' => is_array($after['findings'] ?? null) ? $after['findings'] : [], 'drift' => is_array($after['drift'] ?? null) ? $after['drift'] : self::empty_drift(), ]; } /** @return array{new:list, changed:list, resolved:list, unchanged:list} */ private static function empty_drift(): array { return ['new' => [], 'changed' => [], 'resolved' => [], 'unchanged' => []]; } /** @return array{new:list, changed:list, resolved:list, unchanged:list} */ private static function classify(?array $before, array $after): array { $old=[]; foreach (($before['findings'] ?? []) as $f) if(is_array($f)) $old[(string)($f['id']??'')] = (string)($f['status']??''); $new=[]; foreach (($after['findings'] ?? []) as $f) if(is_array($f)) $new[(string)($f['id']??'')] = (string)($f['status']??''); $out=self::empty_drift(); foreach($new as $id=>$s) { if(!isset($old[$id])) $out['new'][]=$id; elseif($old[$id]!==$s) $out['changed'][]=$id; else $out['unchanged'][]=$id; } foreach($old as $id=>$s) if(!isset($new[$id])) $out['resolved'][]=$id; return $out; } }