|null */ public static function latest(): ?array { $scan = self::scan_settings(); $pruned = self::prune_scan_data($scan); if ($pruned !== $scan) { TheDotDevAgenticBridge_Storage::update(['readiness_scan' => $pruned]); } $latest = $pruned['latest'] ?? null; if (!is_array($latest)) { return null; } $adjusted = TheDotDevAgenticBridge_Readiness_Verification::sanitize_scan_scope( TheDotDevAgenticBridge_Readiness_Verification::apply_scan_staleness($latest), ); if ($adjusted !== $latest) { $pruned['latest'] = $adjusted; TheDotDevAgenticBridge_Storage::update(['readiness_scan' => $pruned]); $latest = $adjusted; } return $latest; } /** @return list> */ public static function history(): array { $scan = self::scan_settings(); $pruned = self::prune_scan_data($scan); if ($pruned !== $scan) { TheDotDevAgenticBridge_Storage::update(['readiness_scan' => $pruned]); } $history = $pruned['history'] ?? []; return is_array($history) ? $history : []; } /** @param array $result */ public static function save_result(array $result): void { $scan = self::scan_settings(); $history = is_array($scan['history'] ?? null) ? $scan['history'] : []; array_unshift($history, $result); $history = self::prune_history($history); TheDotDevAgenticBridge_Storage::update([ 'readiness_scan' => self::prune_scan_data([ 'latest' => $result, 'history' => $history, ]), ]); } /** * @param list> $findings * @return array */ public static function build_result(array $findings, string $run_id, string $recheck_source = 'manual_full', ?array $prior_scan = null): array { $normalized = []; foreach ($findings as $finding) { if (!is_array($finding)) { continue; } $normalized[] = TheDotDevAgenticBridge_Readiness_Methodology::normalize_finding($finding); } return self::assemble_result($normalized, $run_id, $recheck_source, $prior_scan); } /** * @param array $latest * @param array $replacement */ public static function recheck_finding(array $latest, array $replacement): array { $findings = is_array($latest['findings'] ?? null) ? $latest['findings'] : []; $id = (string) ($replacement['id'] ?? ''); $updated = []; $replaced = false; foreach ($findings as $finding) { if (!is_array($finding)) { continue; } if ((string) ($finding['id'] ?? '') === $id) { $replacement['lastChecked'] = TheDotDevAgenticBridge_Readiness_Clock::iso(); $updated[] = TheDotDevAgenticBridge_Readiness_Methodology::normalize_finding($replacement); $replaced = true; continue; } $updated[] = $finding; } if (!$replaced) { $replacement['lastChecked'] = TheDotDevAgenticBridge_Readiness_Clock::iso(); $updated[] = TheDotDevAgenticBridge_Readiness_Methodology::normalize_finding($replacement); } return self::assemble_result($updated, self::new_run_id(), 'manual_targeted', $latest, $replacement); } /** * @param list> $findings * @param array $target_replacement */ private static function assemble_result( array $findings, string $run_id, string $recheck_source, ?array $prior_scan, ?array $target_replacement = null, ): array { $normalized = []; foreach ($findings as $finding) { if (!is_array($finding)) { continue; } $normalized[] = TheDotDevAgenticBridge_Readiness_Methodology::normalize_finding($finding); } usort($normalized, static fn(array $a, array $b): int => strcmp((string) $a['id'], (string) $b['id'])); $score = TheDotDevAgenticBridge_Readiness_Methodology::score_findings($normalized); if ($recheck_source === 'manual_targeted' && $prior_scan !== null && $target_replacement !== null) { $verification = TheDotDevAgenticBridge_Readiness_Verification::reconcile_targeted_recheck( $prior_scan, TheDotDevAgenticBridge_Readiness_Methodology::normalize_finding($target_replacement), $normalized, ); } else { $verification = TheDotDevAgenticBridge_Readiness_Verification::reconcile_scan($normalized, $recheck_source, $prior_scan); } return [ 'runId' => $run_id, 'methodologyVersion' => TheDotDevAgenticBridge_Readiness_Methodology::VERSION, 'scannedAt' => TheDotDevAgenticBridge_Readiness_Clock::iso(), 'score' => $score['score'], 'categories' => $score['categories'], 'findings' => $verification['findings'], 'observations' => $verification['observations'], 'verifications' => $verification['verifications'], 'verificationHistory' => $verification['verificationHistory'], 'networkBoundary' => 'same-origin-only', 'scoreCap' => TheDotDevAgenticBridge_Readiness_Methodology::OVERALL_SCORE_CAP, 'actionsCap' => TheDotDevAgenticBridge_Readiness_Methodology::ACTIONS_CATEGORY_CAP, 'verificationScheduled' => false, ]; } /** * @param array $latest * @param array $replacement * @deprecated Use recheck_finding() which assigns a new run id. */ public static function replace_finding(array $latest, array $replacement): array { return self::recheck_finding($latest, $replacement); } /** @return array{latest?: array, history?: list>} */ private static function scan_settings(): array { $settings = TheDotDevAgenticBridge_Storage::get(); $scan = $settings['readiness_scan'] ?? []; return is_array($scan) ? $scan : []; } /** * @param array{latest?: mixed, history?: mixed} $scan * @return array{latest?: array, history?: list>} */ private static function prune_scan_data(array $scan): array { $history = is_array($scan['history'] ?? null) ? $scan['history'] : []; $pruned_history = self::prune_history($history); $latest = is_array($scan['latest'] ?? null) ? $scan['latest'] : null; if ($latest !== null && !self::is_valid_history_entry($latest)) { $latest = $pruned_history[0] ?? null; } return [ 'latest' => $latest, 'history' => $pruned_history, ]; } /** * @param list> $history * @return list> */ private static function prune_history(array $history): array { $kept = []; foreach ($history as $entry) { if (!self::is_valid_history_entry($entry)) { continue; } $kept[] = $entry; if (count($kept) >= self::MAX_HISTORY_RUNS) { break; } } return $kept; } /** @param mixed $entry */ private static function is_valid_history_entry(mixed $entry): bool { if (!is_array($entry)) { return false; } $scanned_at = strtotime((string) ($entry['scannedAt'] ?? '')); if ($scanned_at === false) { return false; } $cutoff = TheDotDevAgenticBridge_Readiness_Clock::now() - self::MAX_HISTORY_AGE_SECONDS; return $scanned_at >= $cutoff; } private static function new_run_id(): string { if (function_exists('wp_generate_uuid4')) { return wp_generate_uuid4(); } return 'run-' . gmdate('YmdHis', TheDotDevAgenticBridge_Readiness_Clock::now()) . '-' . wp_rand(1000, 9999); } }