*/ public static function finding_to_resolver_map(): array { return TheDotDevAgenticBridge_Resolver_Registry::engine_finding_map(); } /** @return array|null */ public static function coverage_for_finding(string $finding_id): ?array { return TheDotDevAgenticBridge_Resolver_Registry::coverage_for_finding($finding_id); } public static function apply_available_for_finding(string $finding_id): bool { $coverage = self::coverage_for_finding($finding_id); return self::is_enabled() && is_array($coverage) && !empty($coverage['applyAvailable']); } public static function register_runtime_hooks(): void { add_action('wp_head', [self::class, 'render_schema_head'], 2); } public static function is_enabled(): bool { return class_exists('TheDotDevAgenticBridge_Entitlements') && TheDotDevAgenticBridge_Entitlements::can_suggested_fixes(); } /** @return array{ok: bool, error: string, preview: array} */ public static function preview_for_finding(string $finding_id): array { if (!self::is_enabled()) { return ['ok' => false, 'error' => 'engine_disabled', 'preview' => []]; } $resolver_id = self::finding_to_resolver_map()[$finding_id] ?? ''; if ($resolver_id === '') { return ['ok' => false, 'error' => 'unsupported_finding', 'preview' => []]; } $preview = match ($resolver_id) { 'publish_artifacts' => self::preview_publish_artifacts($finding_id), 'add_home_schema' => self::preview_home_schema($finding_id), default => ['ok' => false, 'error' => 'unsupported_resolver'], }; if (empty($preview['ok'])) { return ['ok' => false, 'error' => (string) ($preview['error'] ?? 'preview_failed'), 'preview' => []]; } $id = function_exists('wp_generate_uuid4') ? wp_generate_uuid4() : ('preview-' . gmdate('YmdHis')); $body = wp_json_encode($preview['changes'] ?? [], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $fingerprint = hash('sha256', is_string($body) ? $body : ''); $record = [ 'id' => $id, 'state' => 'ready', 'fingerprint' => $fingerprint, 'findingId' => $finding_id, 'resolverId' => $resolver_id, 'generatedAt' => gmdate('c'), 'summary' => (string) ($preview['summary'] ?? ''), 'changes' => $preview['changes'] ?? [], 'deferred' => !empty($preview['deferred']), 'deferReason' => (string) ($preview['deferReason'] ?? ''), 'currentFingerprint' => self::snapshot_fingerprint(self::capture_undo_snapshot()), ]; TheDotDevAgenticBridge_Storage::update(['fix_engine_pending_preview' => $record]); return ['ok' => true, 'error' => '', 'preview' => $record]; } /** @return array{ok: bool, error: string} */ public static function approve_preview(string $preview_id, string $fingerprint, int $actor_id): array { $settings = TheDotDevAgenticBridge_Storage::get(); $preview = is_array($settings['fix_engine_pending_preview'] ?? null) ? $settings['fix_engine_pending_preview'] : null; if (!is_array($preview) || !hash_equals((string) ($preview['id'] ?? ''), $preview_id) || !hash_equals((string) ($preview['fingerprint'] ?? ''), $fingerprint)) { return ['ok' => false, 'error' => 'preview_mismatch']; } if (!hash_equals((string) ($preview['currentFingerprint'] ?? ''), self::snapshot_fingerprint(self::capture_undo_snapshot()))) { return ['ok' => false, 'error' => 'stale_preview']; } if (!empty($preview['deferred'])) { return ['ok' => false, 'error' => 'deferred']; } if (($preview['state'] ?? 'ready') === 'approved') { return ['ok' => false, 'error' => 'already_approved']; } $preview['state'] = 'approved'; $preview['approvedBy'] = $actor_id; $preview['approvedAt'] = gmdate('c'); TheDotDevAgenticBridge_Storage::update(['fix_engine_pending_preview' => $preview]); return ['ok' => true, 'error' => '']; } /** @return array{ok: bool, error: string} */ public static function apply_preview(string $preview_id, string $fingerprint): array { if (!self::is_enabled()) { return ['ok' => false, 'error' => 'engine_disabled']; } $settings = TheDotDevAgenticBridge_Storage::get(); $preview = is_array($settings['fix_engine_pending_preview'] ?? null) ? $settings['fix_engine_pending_preview'] : null; if (!is_array($preview)) { return ['ok' => false, 'error' => 'missing_preview']; } if (!hash_equals((string) ($preview['id'] ?? ''), $preview_id)) { return ['ok' => false, 'error' => 'preview_mismatch']; } if (!hash_equals((string) ($preview['fingerprint'] ?? ''), $fingerprint)) { return ['ok' => false, 'error' => 'preview_mismatch']; } if (!empty($preview['deferred'])) { return ['ok' => false, 'error' => 'deferred']; } if (($preview['state'] ?? 'ready') !== 'approved' || empty($preview['approvedAt'])) { return ['ok' => false, 'error' => 'approval_required']; } if (!hash_equals((string) ($preview['currentFingerprint'] ?? ''), self::snapshot_fingerprint(self::capture_undo_snapshot()))) { return ['ok' => false, 'error' => 'stale_preview']; } $resolver_id = (string) ($preview['resolverId'] ?? ''); $snapshot = self::capture_undo_snapshot(); $result = match ($resolver_id) { 'publish_artifacts' => self::apply_publish_artifacts(), 'add_home_schema' => self::apply_home_schema(), default => ['ok' => false, 'error' => 'unsupported_resolver'], }; if (empty($result['ok'])) { return ['ok' => false, 'error' => (string) ($result['error'] ?? 'apply_failed')]; } $verification = self::rescan((string) ($preview['findingId'] ?? '')); $audit = self::append_audit([ 'findingId' => (string) ($preview['findingId'] ?? ''), 'resolverId' => $resolver_id, 'summary' => (string) ($preview['summary'] ?? ''), 'changes' => is_array($preview['changes'] ?? null) ? $preview['changes'] : [], 'undoSnapshot' => $snapshot, 'actorId' => get_current_user_id(), 'approvedBy' => (int) ($preview['approvedBy'] ?? 0), 'approvedAt' => (string) ($preview['approvedAt'] ?? ''), 'appliedFingerprint' => self::snapshot_fingerprint(self::capture_undo_snapshot()), 'verification' => $verification, ]); TheDotDevAgenticBridge_Storage::update(['fix_engine_pending_preview' => null]); return ['ok' => true, 'error' => '', 'auditId' => $audit]; } /** @return array{ok: bool, error: string} */ public static function undo(string $audit_id): array { $settings = TheDotDevAgenticBridge_Storage::get(); $log = is_array($settings['fix_engine_audit_log'] ?? null) ? $settings['fix_engine_audit_log'] : []; foreach ($log as $idx => $entry) { if (!is_array($entry)) { continue; } if (!hash_equals((string) ($entry['id'] ?? ''), $audit_id)) { continue; } if (!empty($entry['undoneAt'])) { return ['ok' => false, 'error' => 'already_undone']; } $snapshot = is_array($entry['undoSnapshot'] ?? null) ? $entry['undoSnapshot'] : []; if (!hash_equals((string) ($entry['appliedFingerprint'] ?? ''), self::snapshot_fingerprint(self::capture_undo_snapshot()))) { return ['ok' => false, 'error' => 'restore_refused']; } self::restore_undo_snapshot($snapshot); $log[$idx]['undoneAt'] = gmdate('c'); TheDotDevAgenticBridge_Storage::update(['fix_engine_audit_log' => $log]); self::rescan((string) ($entry['findingId'] ?? '')); return ['ok' => true, 'error' => '']; } return ['ok' => false, 'error' => 'audit_not_found']; } /** @return list> */ public static function audit_log(): array { $log = TheDotDevAgenticBridge_Storage::get()['fix_engine_audit_log'] ?? []; return is_array($log) ? $log : []; } /** @param array $entry */ public static function restore_available(array $entry): bool { if (!empty($entry['undoneAt']) || empty($entry['id'])) { return false; } return hash_equals((string) ($entry['appliedFingerprint'] ?? ''), self::snapshot_fingerprint(self::capture_undo_snapshot())); } /** @return array|null */ public static function pending_preview(): ?array { $preview = TheDotDevAgenticBridge_Storage::get()['fix_engine_pending_preview'] ?? null; return is_array($preview) ? $preview : null; } public static function render_schema_head(): void { $settings = TheDotDevAgenticBridge_Storage::get(); if (empty($settings['fix_engine_schema_enabled'])) { return; } $nodes = is_array($settings['fix_engine_schema_nodes'] ?? null) ? $settings['fix_engine_schema_nodes'] : []; if ($nodes === []) { return; } $graph = ['@context' => 'https://schema.org', '@graph' => array_values($nodes)]; $json = wp_json_encode($graph, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_INVALID_UTF8_SUBSTITUTE); if (!is_string($json) || $json === '') { return; } // wp_get_inline_script_tag retains valid JSON-LD while its JSON_HEX payload cannot terminate the script element. echo wp_get_inline_script_tag($json, ['type' => 'application/ld+json']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WordPress-generated script element with JSON_HEX payload. } /** @return array */ private static function preview_publish_artifacts(string $finding_id): array { $draft = TheDotDevAgenticBridge_Artifacts::generate_preview_draft(); if (!$draft['ok']) { return ['ok' => false, 'error' => 'artifact_preview_failed']; } $payload = is_array($draft['draft'] ?? null) ? $draft['draft'] : []; return [ 'ok' => true, 'summary' => __('Publish local /llms.txt and /agent-map.json from reviewed content metadata.', 'agentic-bridge'), 'changes' => [ 'findingId' => $finding_id, 'llmsPreviewBytes' => strlen((string) ($payload['llms_txt'] ?? '')), 'agentMapEntryCount' => count(is_array($payload['agent_map']['entries'] ?? null) ? $payload['agent_map']['entries'] : []), 'routes' => ['/llms.txt', '/agent-map.json'], ], ]; } /** @return array */ private static function preview_home_schema(string $finding_id): array { $owner = TheDotDevAgenticBridge_Resolver_Registry::detect_seo_owner_vocabulary(); if (($owner['owner'] ?? 'none') !== 'none') { return ['ok' => true, 'summary' => __('Homepage schema is deferred because another SEO plugin owns this output.', 'agentic-bridge'), 'changes' => ['findingId' => $finding_id, 'seoOwner' => $owner['owner']], 'deferred' => true, 'deferReason' => 'seo_plugin_owner']; } $analysis = self::homepage_schema_analysis(); $types = is_array($analysis['types'] ?? null) ? $analysis['types'] : []; $normalized = array_map(static fn($item): string => strtolower((string) $item), $types); $missing = []; if (!in_array('organization', $normalized, true)) { $missing[] = 'Organization'; } if (!in_array('website', $normalized, true)) { $missing[] = 'WebSite'; } if ($missing === []) { return ['ok' => false, 'error' => 'schema_already_present']; } return [ 'ok' => true, 'summary' => __('Add missing homepage Organization/WebSite JSON-LD without changing existing SEO plugin schema.', 'agentic-bridge'), 'changes' => [ 'findingId' => $finding_id, 'missingTypes' => $missing, 'seoOwner' => (string) ($owner['owner'] ?? 'none'), 'coexistOnly' => true, ], ]; } /** @return array{ok: bool, error: string} */ private static function apply_publish_artifacts(): array { $result = TheDotDevAgenticBridge_Artifacts::publish_from_preview(); if (!$result['ok']) { return ['ok' => false, 'error' => (string) ($result['error'] ?? 'publish_failed')]; } flush_rewrite_rules(); return ['ok' => true, 'error' => '']; } /** @return array{ok: bool, error: string} */ private static function apply_home_schema(): array { $analysis = self::homepage_schema_analysis(); $types = is_array($analysis['types'] ?? null) ? $analysis['types'] : []; $normalized = array_map(static fn($item): string => strtolower((string) $item), $types); $nodes = []; if (!in_array('organization', $normalized, true)) { $nodes[] = [ '@type' => 'Organization', '@id' => untrailingslashit(home_url('/')) . '#organization', 'name' => get_bloginfo('name'), 'url' => home_url('/'), ]; } if (!in_array('website', $normalized, true)) { $nodes[] = [ '@type' => 'WebSite', '@id' => untrailingslashit(home_url('/')) . '#website', 'name' => get_bloginfo('name'), 'url' => home_url('/'), ]; } if ($nodes === []) { return ['ok' => false, 'error' => 'schema_already_present']; } TheDotDevAgenticBridge_Storage::update([ 'fix_engine_schema_enabled' => true, 'fix_engine_schema_nodes' => $nodes, ]); return ['ok' => true, 'error' => '']; } /** @return array */ private static function capture_undo_snapshot(): array { $settings = TheDotDevAgenticBridge_Storage::get(); return [ 'artifacts_published' => !empty($settings['artifacts_published']), 'last_known_good' => is_array($settings['last_known_good'] ?? null) ? $settings['last_known_good'] : [], 'artifact_preview_draft' => $settings['artifact_preview_draft'] ?? null, 'artifact_revisions' => is_array($settings['artifact_revisions'] ?? null) ? $settings['artifact_revisions'] : [], 'fix_engine_schema_enabled' => !empty($settings['fix_engine_schema_enabled']), 'fix_engine_schema_nodes' => is_array($settings['fix_engine_schema_nodes'] ?? null) ? $settings['fix_engine_schema_nodes'] : [], ]; } /** @param array $snapshot */ private static function restore_undo_snapshot(array $snapshot): void { TheDotDevAgenticBridge_Storage::update([ 'artifacts_published' => !empty($snapshot['artifacts_published']), 'last_known_good' => is_array($snapshot['last_known_good'] ?? null) ? $snapshot['last_known_good'] : [], 'artifact_preview_draft' => $snapshot['artifact_preview_draft'] ?? null, 'artifact_revisions' => is_array($snapshot['artifact_revisions'] ?? null) ? $snapshot['artifact_revisions'] : [], 'fix_engine_schema_enabled' => !empty($snapshot['fix_engine_schema_enabled']), 'fix_engine_schema_nodes' => is_array($snapshot['fix_engine_schema_nodes'] ?? null) ? $snapshot['fix_engine_schema_nodes'] : [], ]); } /** @param array $entry */ private static function append_audit(array $entry): string { $settings = TheDotDevAgenticBridge_Storage::get(); $log = is_array($settings['fix_engine_audit_log'] ?? null) ? $settings['fix_engine_audit_log'] : []; $id = function_exists('wp_generate_uuid4') ? wp_generate_uuid4() : ('audit-' . gmdate('YmdHis')); array_unshift($log, [ 'id' => $id, 'appliedAt' => gmdate('c'), 'findingId' => (string) ($entry['findingId'] ?? ''), 'resolverId' => (string) ($entry['resolverId'] ?? ''), 'summary' => (string) ($entry['summary'] ?? ''), 'changes' => $entry['changes'] ?? [], 'undoSnapshot' => $entry['undoSnapshot'] ?? [], 'undoneAt' => '', 'actorId' => (int) ($entry['actorId'] ?? 0), 'approvedBy' => (int) ($entry['approvedBy'] ?? 0), 'approvedAt' => (string) ($entry['approvedAt'] ?? ''), 'appliedFingerprint' => (string) ($entry['appliedFingerprint'] ?? ''), 'verification' => is_array($entry['verification'] ?? null) ? $entry['verification'] : ['result' => 'unknown', 'checkedAt' => ''], ]); TheDotDevAgenticBridge_Storage::update(['fix_engine_audit_log' => array_slice($log, 0, self::MAX_AUDIT_LOG)]); return $id; } /** @return array{result: string, checkedAt: string} */ private static function rescan(string $finding_id): array { $scanner = new TheDotDevAgenticBridge_Readiness_Scanner(); $result = $scanner->run_all(); TheDotDevAgenticBridge_Readiness_Store::save_result($result); foreach (($result['findings'] ?? []) as $finding) { if (is_array($finding) && ($finding['id'] ?? '') === $finding_id) { $status = (string) ($finding['status'] ?? ''); return ['result' => $status === 'pass' ? 'passed' : ($status === 'fail' ? 'failed' : 'unknown'), 'checkedAt' => (string) ($finding['lastChecked'] ?? $result['scannedAt'] ?? '')]; } } return ['result' => 'unknown', 'checkedAt' => (string) ($result['scannedAt'] ?? '')]; } /** @param array $snapshot */ private static function snapshot_fingerprint(array $snapshot): string { $encoded = wp_json_encode($snapshot, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); return hash('sha256', is_string($encoded) ? $encoded : ''); } /** @return array */ private static function homepage_schema_analysis(): array { $fetch = TheDotDevAgenticBridge_Same_Origin_Observer::fetch('/'); if (empty($fetch['ok'])) { return ['status' => 'absent', 'types' => []]; } return TheDotDevAgenticBridge_Readiness_Schema::analyze_html((string) ($fetch['body_sample'] ?? '')); } }