*/ private const ARTIFACT_FINDING_IDS = [ 'disc-llms-route', 'disc-agent-map-route', 'disc-llms-valid', 'und-llms-emerging', 'und-agent-map-format', 'und-artifact-manifest', 'saf-artifact-stale', 'saf-physical-llms-conflict', 'act-agent-map-actions', ]; /** @var list */ private const CONTENT_FINDING_IDS = [ 'und-home-schema', 'act-intent-declarations', 'saf-content-policy', ]; /** @var list */ private const SAFETY_FINDING_IDS = [ 'saf-kill-switch', ]; /** @var list */ private const SETTINGS_FINDING_IDS = [ 'trn-search-visibility', 'trn-robots-rules', 'trn-https', 'trn-url-consistency', 'disc-robots-public', 'disc-sitemap', ]; /** * @param list> $findings * * @return array{action_needed: int, needs_review: int, all_clear: int} */ public static function triage_summary_counts(array $findings): array { $action_needed = 0; $needs_review = 0; $all_clear = 0; foreach ($findings as $finding) { if (!is_array($finding)) { continue; } $status = TheDotDevAgenticBridge_Readiness_Methodology::normalize_status( (string) ($finding['status'] ?? TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN) ); if ($status === TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL) { ++$action_needed; } elseif (in_array($status, [ TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, ], true)) { ++$needs_review; } else { ++$all_clear; } } return [ 'action_needed' => $action_needed, 'needs_review' => $needs_review, 'all_clear' => $all_clear, ]; } /** @param array $finding */ public static function owner_title(array $finding): string { $id = (string) ($finding['id'] ?? ''); $evidence = is_array($finding['evidence'] ?? null) ? $finding['evidence'] : []; $status = TheDotDevAgenticBridge_Readiness_Methodology::normalize_status( (string) ($finding['status'] ?? TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN) ); if (in_array($id, ['disc-llms-route', 'disc-llms-valid', 'und-llms-emerging'], true)) { if ($status !== TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS || self::evidence_http_status($evidence) === 404) { return __('AI guidance file is not published yet', 'agentic-bridge'); } } if (in_array($id, ['disc-agent-map-route', 'und-agent-map-format'], true)) { if ($status !== TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS || self::evidence_http_status($evidence) === 404) { return __('AI discovery map is not published yet', 'agentic-bridge'); } } if (self::uses_incomplete_check_copy($finding)) { return __('This check could not be completed safely', 'agentic-bridge'); } $catalog = self::catalog_copy($id); if ($catalog['title'] !== '') { return $catalog['title']; } $stored = trim((string) ($finding['title'] ?? '')); if ($stored !== '' && !self::looks_like_technical_evidence($stored)) { return $stored; } return __('Finding from your latest local scan', 'agentic-bridge'); } /** @param array $finding */ public static function owner_summary(array $finding): string { $id = (string) ($finding['id'] ?? ''); if (in_array($id, ['disc-llms-route', 'disc-llms-valid', 'und-llms-emerging'], true) && self::owner_title($finding) === __('AI guidance file is not published yet', 'agentic-bridge')) { return __('Agents look for /llms.txt to learn how they may use your public content.', 'agentic-bridge'); } if (in_array($id, ['disc-agent-map-route', 'und-agent-map-format'], true) && self::owner_title($finding) === __('AI discovery map is not published yet', 'agentic-bridge')) { return __('Agents use your discovery map to find eligible pages and safe actions on your site.', 'agentic-bridge'); } if (self::uses_incomplete_check_copy($finding)) { return __('We could not fully check this page. Review its summary and try the check again.', 'agentic-bridge'); } $why = trim((string) ($finding['whyItMatters'] ?? '')); if ($why !== '' && !self::looks_like_technical_evidence($why)) { return $why; } $catalog = self::catalog_copy($id); if ($catalog['summary'] !== '') { return $catalog['summary']; } return __('This item was recorded during your latest local readiness scan.', 'agentic-bridge'); } /** @param array $finding */ public static function owner_why(array $finding): string { $why = trim((string) ($finding['whyItMatters'] ?? '')); if ($why !== '' && !self::looks_like_technical_evidence($why)) { return $why; } $catalog = self::catalog_copy((string) ($finding['id'] ?? '')); if ($catalog['why'] !== '') { return $catalog['why']; } return self::owner_summary($finding); } /** @param array $finding */ public static function owner_next(array $finding): string { $next = trim((string) ($finding['nextStep'] ?? '')); if ($next !== '' && !self::looks_like_technical_evidence($next)) { return $next; } $id = (string) ($finding['id'] ?? ''); if (in_array($id, self::ARTIFACT_FINDING_IDS, true) && self::artifact_needs_publish_flow($finding)) { return __('Create a preview, review it, then publish only if you approve.', 'agentic-bridge'); } if (self::finding_unavailable_for_owner($finding)) { return __('Review the technical details, then run a fresh full scan when the site is reachable.', 'agentic-bridge'); } $catalog = self::catalog_copy($id); if ($catalog['next'] !== '') { return $catalog['next']; } return __('Review details and choose a safe next step from your WordPress controls.', 'agentic-bridge'); } /** * @param array $finding * * @return array{ * type: 'link', * label: string, * href: string * } */ public static function primary_row_action(array $finding): array { $id = (string) ($finding['id'] ?? ''); if (in_array($id, self::ARTIFACT_FINDING_IDS, true)) { return [ 'type' => 'link', 'label' => __('Review artifacts', 'agentic-bridge'), 'href' => TheDotDevAgenticBridge_Admin::ai_preview_url(), ]; } if (in_array($id, self::CONTENT_FINDING_IDS, true)) { return [ 'type' => 'link', 'label' => __('Open Content & Intent', 'agentic-bridge'), 'href' => self::workspace_url('content'), ]; } if (in_array($id, self::SAFETY_FINDING_IDS, true)) { return [ 'type' => 'link', 'label' => __('Open Safety', 'agentic-bridge'), 'href' => self::workspace_url('safety'), ]; } if (in_array($id, self::SETTINGS_FINDING_IDS, true)) { return [ 'type' => 'link', 'label' => __('Open Settings', 'agentic-bridge'), 'href' => self::workspace_url('settings'), ]; } return [ 'type' => 'link', 'label' => __('Open dashboard', 'agentic-bridge'), 'href' => TheDotDevAgenticBridge_Admin::admin_page_url(), ]; } /** @param array $finding */ public static function drawer_primary_action(array $finding, bool $allow_recheck): array { $id = (string) ($finding['id'] ?? ''); if ($allow_recheck && $id !== '' && TheDotDevAgenticBridge_Readiness_Methodology::is_valid_check_id($id)) { return [ 'type' => 'recheck', 'label' => __('Recheck this finding', 'agentic-bridge'), 'finding_id' => $id, ]; } return self::primary_row_action($finding); } /** @param array $finding */ public static function has_technical_evidence(array $finding): bool { $evidence = is_array($finding['evidence'] ?? null) ? $finding['evidence'] : []; return $evidence !== []; } /** * @param array $finding * * @return list */ public static function technical_evidence_lines(array $finding): array { $evidence = is_array($finding['evidence'] ?? null) ? $finding['evidence'] : []; if ($evidence !== []) { return TheDotDevAgenticBridge_Readiness_Methodology::evidence_lines($evidence); } if (self::finding_unavailable_for_owner($finding)) { return [__('Stored scan evidence is unavailable for this check.', 'agentic-bridge')]; } return []; } /** @param array $finding */ public static function search_blob(array $finding): string { return strtolower(implode(' ', [ (string) ($finding['id'] ?? ''), (string) ($finding['title'] ?? ''), (string) ($finding['whyItMatters'] ?? ''), (string) ($finding['nextStep'] ?? ''), (string) ($finding['category'] ?? ''), self::owner_title($finding), self::owner_summary($finding), self::owner_next($finding), ])); } /** @param array $finding */ private static function uses_incomplete_check_copy(array $finding): bool { $id = (string) ($finding['id'] ?? ''); if (in_array($id, self::ARTIFACT_FINDING_IDS, true)) { return false; } $evidence = is_array($finding['evidence'] ?? null) ? $finding['evidence'] : []; if (self::evidence_implies_incomplete_check($evidence)) { return true; } $status = TheDotDevAgenticBridge_Readiness_Methodology::normalize_status( (string) ($finding['status'] ?? TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN) ); return $status === TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL && $evidence === []; } /** @param array $finding */ private static function finding_unavailable_for_owner(array $finding): bool { $id = (string) ($finding['id'] ?? ''); if (in_array($id, self::ARTIFACT_FINDING_IDS, true)) { return false; } $evidence = is_array($finding['evidence'] ?? null) ? $finding['evidence'] : []; if ($evidence !== [] && self::evidence_implies_incomplete_check($evidence)) { return true; } if ($evidence === []) { $status = TheDotDevAgenticBridge_Readiness_Methodology::normalize_status( (string) ($finding['status'] ?? TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN) ); return $status !== TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS; } return false; } /** @param array $evidence */ public static function evidence_implies_incomplete_check(array $evidence): bool { if (self::evidence_http_status($evidence) === 404) { return true; } $error = strtolower((string) ($evidence['error'] ?? '')); if ($error !== '' && (str_contains($error, 'too large') || str_contains($error, 'response too large'))) { return true; } $byte_length = (int) ($evidence['byteLength'] ?? $evidence['bytelength'] ?? 0); if ($byte_length > 500000) { return true; } return false; } /** @param array $finding */ private static function artifact_needs_publish_flow(array $finding): bool { $status = TheDotDevAgenticBridge_Readiness_Methodology::normalize_status( (string) ($finding['status'] ?? TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN) ); return $status !== TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS || self::evidence_http_status(is_array($finding['evidence'] ?? null) ? $finding['evidence'] : []) === 404; } /** @param array $evidence */ private static function evidence_http_status(array $evidence): int { $raw = $evidence['httpStatus'] ?? $evidence['httpstatus'] ?? 0; return (int) $raw; } public static function looks_like_technical_evidence(string $text): bool { $normalized = strtolower($text); return str_contains($normalized, 'localcontrols') || str_contains($normalized, 'partial coverage') || str_contains($normalized, 'scan evidence') || str_contains($normalized, 'httpstatus') || str_contains($normalized, 'http status') || str_contains($normalized, 'http 404') || str_contains($normalized, 'response too large') || str_contains($normalized, 'bytelength') || str_contains($normalized, ' '', 'summary' => '', 'why' => '', 'next' => '']; return match ($id) { 'disc-llms-route', 'disc-llms-valid', 'und-llms-emerging' => [ 'title' => __('AI guidance file is available', 'agentic-bridge'), 'summary' => __('Agents can read your published guidance file for how to use public content.', 'agentic-bridge'), 'why' => __('A clear guidance file helps agents discover what they may use on your site.', 'agentic-bridge'), 'next' => __('Review the published file after content or URL changes.', 'agentic-bridge'), ], 'disc-agent-map-route', 'und-agent-map-format' => [ 'title' => __('AI discovery map is available', 'agentic-bridge'), 'summary' => __('Agents can use your discovery map to find eligible pages and safe actions.', 'agentic-bridge'), 'why' => __('A structured map makes eligible public pages easier for agents to understand.', 'agentic-bridge'), 'next' => __('Keep the map aligned with eligible content after publishing.', 'agentic-bridge'), ], 'disc-sitemap' => [ 'title' => __('XML sitemap discovery', 'agentic-bridge'), 'summary' => __('Sitemaps help crawlers find public URLs. They do not guarantee ranking.', 'agentic-bridge'), 'why' => __('A reachable sitemap is a discovery aid only, not a ranking promise.', 'agentic-bridge'), 'next' => __('Keep sitemap URLs current in WordPress, then run a fresh local scan.', 'agentic-bridge'), ], 'disc-robots-public' => [ 'title' => __('Site crawl discovery signals', 'agentic-bridge'), 'summary' => __('robots.txt and public crawl rules affect whether agents can find public pages.', 'agentic-bridge'), 'why' => __('Conflicting crawl rules can hide pages you intended to keep public.', 'agentic-bridge'), 'next' => __('Review crawl and visibility settings, then scan again.', 'agentic-bridge'), ], 'und-home-schema' => [ 'title' => __('Homepage structured data', 'agentic-bridge'), 'summary' => __('Agents need a clear homepage summary they can read without guessing.', 'agentic-bridge'), 'why' => __('Structured homepage context helps agents describe your site accurately.', 'agentic-bridge'), 'next' => __('Add an accurate homepage summary in Content & Intent.', 'agentic-bridge'), ], 'und-artifact-manifest' => [ 'title' => __('Discovery files need a content review', 'agentic-bridge'), 'summary' => __('Published discovery files should match the pages you marked eligible.', 'agentic-bridge'), 'why' => __('A mismatch can send agents to the wrong pages or omit approved ones.', 'agentic-bridge'), 'next' => __('Create a preview, review it, then publish only if you approve.', 'agentic-bridge'), ], 'trn-https' => [ 'title' => __('HTTPS in use', 'agentic-bridge'), 'summary' => __('Encrypted transport is required before agents should trust this site.', 'agentic-bridge'), 'why' => __('HTTPS protects visitors and is a basic trust signal for agents.', 'agentic-bridge'), 'next' => __('Keep HTTPS enabled on the public site, then run a fresh local scan.', 'agentic-bridge'), ], 'trn-url-consistency' => [ 'title' => __('Home and site addresses match', 'agentic-bridge'), 'summary' => __('WordPress home and site URLs should point at the same public host.', 'agentic-bridge'), 'why' => __('Mismatched addresses confuse agents about which site they are reading.', 'agentic-bridge'), 'next' => __('Align the WordPress home and site URLs, then scan again.', 'agentic-bridge'), ], 'trn-robots-rules' => [ 'title' => __('robots.txt agent rules', 'agentic-bridge'), 'summary' => __('Agent crawl rules in robots.txt should match what you want discovered.', 'agentic-bridge'), 'why' => __('Unexpected blocks can hide public pages from agents.', 'agentic-bridge'), 'next' => __('Review robots rules in Settings, then scan again.', 'agentic-bridge'), ], 'trn-search-visibility' => [ 'title' => __('Search visibility settings', 'agentic-bridge'), 'summary' => __('WordPress search-visibility settings affect whether public pages can be found.', 'agentic-bridge'), 'why' => __('A discouraged site can look unpublished to agents even when pages are public.', 'agentic-bridge'), 'next' => __('Review visibility settings, then run a fresh local scan.', 'agentic-bridge'), ], 'act-intent-declarations' => [ 'title' => __('Pages are missing agent intent', 'agentic-bridge'), 'summary' => __('Eligible pages should declare what agents may do with them.', 'agentic-bridge'), 'why' => __('Without intent, agents cannot tell allowed actions from withheld ones.', 'agentic-bridge'), 'next' => __('Open Content & Intent and set a safe intent on each eligible page.', 'agentic-bridge'), ], 'act-agent-map-actions' => [ 'title' => __('Discovery map is missing safe actions', 'agentic-bridge'), 'summary' => __('The discovery map should list only actions you approved.', 'agentic-bridge'), 'why' => __('Missing action metadata leaves agents without a safe next step.', 'agentic-bridge'), 'next' => __('Create a preview, review actions, then publish only if you approve.', 'agentic-bridge'), ], 'saf-content-policy' => [ 'title' => __('Content exposure policy needs a review', 'agentic-bridge'), 'summary' => __('Private or ineligible content must stay out of discovery files.', 'agentic-bridge'), 'why' => __('A weak exposure policy can publish pages you meant to withhold.', 'agentic-bridge'), 'next' => __('Review eligibility and exclusions in Content & Intent.', 'agentic-bridge'), ], 'saf-kill-switch' => [ 'title' => __('Emergency stop is available', 'agentic-bridge'), 'summary' => __('You can suppress public discovery files immediately without deleting content.', 'agentic-bridge'), 'why' => __('An emergency stop lets you pause agent-facing files if something looks wrong.', 'agentic-bridge'), 'next' => __('Open Safety to review or change the emergency stop.', 'agentic-bridge'), ], 'saf-artifact-stale' => [ 'title' => __('Published discovery files may be stale', 'agentic-bridge'), 'summary' => __('Published files should stay aligned with your latest approved content.', 'agentic-bridge'), 'why' => __('Stale files can describe pages that no longer match the live site.', 'agentic-bridge'), 'next' => __('Create a fresh preview, review it, then publish only if you approve.', 'agentic-bridge'), ], 'saf-physical-llms-conflict' => [ 'title' => __('A file on disk conflicts with /llms.txt', 'agentic-bridge'), 'summary' => __('Another file may be serving /llms.txt instead of Agentic Bridge.', 'agentic-bridge'), 'why' => __('A conflicting file can override the guidance you approved in WordPress.', 'agentic-bridge'), 'next' => __('Review Discovery assets and remove or align the conflicting file.', 'agentic-bridge'), ], default => $empty, }; } private static function workspace_url(string $view): string { return add_query_arg( ['thedotdev_agentic_bridge_view' => $view], TheDotDevAgenticBridge_Admin::admin_page_url() ); } }