> */ private array $cache = []; /** @return array */ public function run_all(): array { $findings = []; foreach (TheDotDevAgenticBridge_Readiness_Methodology::checks() as $check) { $findings[] = $this->run_check((string) $check['id']); } return TheDotDevAgenticBridge_Readiness_Store::build_result( $findings, $this->new_run_id(), 'manual_full', TheDotDevAgenticBridge_Readiness_Store::latest(), ); } /** @return array */ public function run_check(string $check_id): array { if (!TheDotDevAgenticBridge_Readiness_Methodology::is_valid_check_id($check_id)) { throw new InvalidArgumentException('Unknown readiness check id: ' . sanitize_key($check_id)); } return match ($check_id) { 'disc-llms-route' => $this->check_disc_llms_route(), 'disc-agent-map-route' => $this->check_disc_agent_map_route(), 'disc-llms-valid' => $this->check_disc_llms_valid(), 'disc-sitemap' => $this->check_disc_sitemap(), 'disc-robots-public' => $this->check_disc_robots_public(), 'und-llms-emerging' => $this->check_und_llms_emerging(), 'und-agent-map-format' => $this->check_und_agent_map_format(), 'und-home-schema' => $this->check_und_home_schema(), 'und-artifact-manifest' => $this->check_und_artifact_manifest(), 'trn-https' => $this->check_trn_https(), 'trn-url-consistency' => $this->check_trn_url_consistency(), 'trn-robots-rules' => $this->check_trn_robots_rules(), 'trn-search-visibility' => $this->check_trn_search_visibility(), 'act-intent-declarations' => $this->check_act_intent_declarations(), 'act-agent-map-actions' => $this->check_act_agent_map_actions(), 'saf-content-policy' => $this->check_saf_content_policy(), 'saf-kill-switch' => $this->check_saf_kill_switch(), 'saf-artifact-stale' => $this->check_saf_artifact_stale(), 'saf-physical-llms-conflict' => $this->check_saf_physical_llms_conflict(), default => throw new InvalidArgumentException('Unhandled readiness check id: ' . sanitize_key($check_id)), }; } /** @return array */ private function check_disc_llms_route(): array { $fetch = $this->fetch('/llms.txt'); if (!$fetch['ok']) { return $this->finding('disc-llms-route', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, [ 'httpStatus' => $fetch['status'] ?? 'unknown', 'error' => $fetch['error'], ], 'Agents and publishers increasingly look for machine-readable discovery files.', 'Publish /llms.txt through Agentic Bridge or verify the public route manually, then recheck.'); } $status = (int) ($fetch['status'] ?? 0); if ($status === 200 && !TheDotDevAgenticBridge_Storage::artifacts_published()) { return $this->finding('disc-llms-route', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => $status, 'routeConflict' => 'another-source', ], 'A public /llms.txt route responds while plugin artifacts are unpublished, suggesting another source serves this path.', 'Remove or align the conflicting route, or publish plugin-owned artifacts intentionally.'); } if ($status === 200) { return $this->finding('disc-llms-route', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'httpStatus' => $status, 'contentType' => $fetch['content_type'], ], 'A reachable llms.txt route gives agents a single discovery entry point.', 'Keep the route available and review its contents after major site changes.'); } if ($status === 404) { return $this->finding('disc-llms-route', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => $status, ], 'llms.txt is an emerging publisher proposal, not a universal standard.', 'Publish /llms.txt explicitly if you want agent discovery guidance.'); } return $this->finding('disc-llms-route', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => $status, ], 'Unexpected HTTP status on /llms.txt can confuse automated discovery.', 'Return 200 when published or 404 when intentionally unpublished.'); } /** @return array */ private function check_disc_agent_map_route(): array { $fetch = $this->fetch('/agent-map.json'); if (!$fetch['ok']) { return $this->finding('disc-agent-map-route', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, [ 'httpStatus' => $fetch['status'] ?? 'unknown', 'error' => $fetch['error'], ], 'agent-map.json is this product\'s bounded discovery manifest format.', 'Publish /agent-map.json when you want structured discovery metadata available on-site.'); } $status = (int) ($fetch['status'] ?? 0); if ($status === 200 && !TheDotDevAgenticBridge_Storage::artifacts_published()) { return $this->finding('disc-agent-map-route', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => $status, 'routeConflict' => 'another-source', ], 'A public /agent-map.json route responds while plugin artifacts are unpublished.', 'Remove the conflicting route or publish plugin-owned artifacts intentionally.'); } if ($status === 200) { return $this->finding('disc-agent-map-route', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'httpStatus' => $status, ], 'A public agent-map.json route exposes structured discovery metadata for agents.', 'Keep the manifest aligned with eligible content after publishing.'); } if ($status === 404) { return $this->finding('disc-agent-map-route', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => $status, ], 'agent-map.json is optional until you publish artifacts.', 'Publish /agent-map.json when you want on-site structured discovery metadata.'); } return $this->finding('disc-agent-map-route', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => $status, ], 'Unexpected HTTP status on /agent-map.json reduces predictable discovery.', 'Serve 200 when published or 404 when intentionally withheld.'); } /** @return array */ private function check_disc_llms_valid(): array { if (!TheDotDevAgenticBridge_Storage::artifacts_published()) { $fetch = $this->fetch('/llms.txt'); if ($fetch['ok'] && (int) ($fetch['status'] ?? 0) === 200) { return $this->finding('disc-llms-valid', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => 200, 'routeConflict' => 'another-source', ], 'llms.txt is reachable but not plugin-published, so validity cannot be scored as plugin-owned output.', 'Align the route with plugin publishing or remove the conflicting source.'); } return $this->finding('disc-llms-valid', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_NOT_APPLICABLE, [ 'artifactsPublished' => 'no', ], 'Response validity is only scored when plugin-owned llms.txt is published.', 'Publish artifacts first if you want this check to evaluate content quality.'); } $fetch = $this->fetch('/llms.txt'); if (!$fetch['ok']) { return $this->finding('disc-llms-valid', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, [ 'error' => $fetch['error'], ], 'Could not verify llms.txt response quality without a same-origin response.', 'Retry the scan when the site front end is reachable from the server.'); } if ((int) ($fetch['status'] ?? 0) !== 200) { return $this->finding('disc-llms-valid', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'httpStatus' => $fetch['status'], ], 'Published llms.txt should return HTTP 200 for agents to consume it.', 'Republish artifacts or fix the public route.'); } $body = trim((string) $fetch['body_sample']); if ($body !== '' && $this->is_plain_text_content_type((string) $fetch['content_type'])) { return $this->finding('disc-llms-valid', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'contentType' => $fetch['content_type'], 'byteLength' => strlen($body), ], 'A plain-text llms.txt response is easier for agents and operators to consume.', 'Review llms.txt after content or URL changes.'); } return $this->finding('disc-llms-valid', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'contentType' => $fetch['content_type'], 'byteLength' => strlen($body), ], 'Invalid or empty llms.txt responses reduce usefulness for discovery.', 'Serve non-empty plain-text content at /llms.txt.'); } /** @return array */ private function check_disc_sitemap(): array { $candidates = self::sitemap_candidates(); foreach ($candidates as $candidate) { $fetch = $this->fetch($candidate); if (!$fetch['ok']) { continue; } if ((int) ($fetch['status'] ?? 0) !== 200) { continue; } $validation = TheDotDevAgenticBridge_Readiness_Sitemap::validate_body((string) $fetch['body_sample']); if ($validation['status'] === 'valid') { return $this->finding('disc-sitemap', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'path' => $candidate, 'root' => $validation['root'], ], 'XML sitemaps help crawlers discover URLs; they do not guarantee indexing or ranking.', 'Keep sitemap URLs current; sitemaps are a discovery aid only.'); } if ($validation['status'] === 'unknown') { return $this->finding('disc-sitemap', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, [ 'path' => $candidate, 'error' => $validation['error'], ], 'Sitemap XML could not be validated because the XML parser is unavailable.', 'Retry on a host with DOM/XML support or validate sitemaps manually.'); } } if ($candidates === []) { return $this->finding('disc-sitemap', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, [], 'Could not determine a sitemap endpoint from local configuration.', 'Enable WordPress sitemaps or provide /sitemap.xml, then recheck.'); } return $this->finding('disc-sitemap', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'candidatesChecked' => $candidates, ], 'No reachable valid XML sitemap was detected.', 'Publish a valid XML sitemap via WordPress or your SEO plugin.'); } /** @return array */ private function check_disc_robots_public(): array { $blog_public = (string) get_option('blog_public', '1'); if ($blog_public !== '1') { return $this->finding('disc-robots-public', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'blogPublic' => $blog_public, ], 'WordPress is discouraging search engines from indexing this site.', 'Set Settings → Reading → discourage search engines to off unless the site should stay private.'); } $fetch = $this->fetch('/robots.txt'); if (!$fetch['ok']) { return $this->finding('disc-robots-public', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'blogPublic' => $blog_public, 'robotsStatus' => 'unknown', 'error' => $fetch['error'], ], 'WordPress is allowing search engines to index this site, but robots.txt could not be observed for root crawl permission.', 'Ensure /robots.txt is reachable or document intentional absence.'); } $status = (int) ($fetch['status'] ?? 0); if ($status !== 200) { return $this->finding('disc-robots-public', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'blogPublic' => $blog_public, 'httpStatus' => $status, ], 'WordPress is allowing search engines to index this site, but robots.txt did not return HTTP 200 for parsing.', 'Serve a valid robots.txt at /robots.txt or document intentional absence.'); } if (!$this->is_plain_text_content_type((string) $fetch['content_type'])) { return $this->finding('disc-robots-public', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'blogPublic' => $blog_public, 'httpStatus' => $status, 'contentType' => $fetch['content_type'], 'robotsStatus' => 'unknown', ], 'robots.txt must be plain text (HTTP 200) before root crawl policy can be evaluated.', 'Serve robots.txt as text/plain; HTML responses are not parsed.'); } $agents = TheDotDevAgenticBridge_Readiness_Robots::evaluate_discovery_agents((string) $fetch['body_sample']); if ($agents['general'] === 'disallow' || $agents['oai'] === 'disallow') { return $this->finding('disc-robots-public', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'blogPublic' => $blog_public, 'generalRoot' => $agents['general'], 'oaiRoot' => $agents['oai'], ], 'WordPress allows indexing, but observed robots.txt blocks root access for crawlers.', 'Align robots.txt with intended public discovery or document the block.'); } if ($agents['general'] === 'unknown' && $agents['oai'] === 'unknown') { return $this->finding('disc-robots-public', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'blogPublic' => $blog_public, 'robotsStatus' => 'unknown', ], 'WordPress is allowing search engines to index this site, but robots.txt root policy could not be determined.', 'Review robots.txt groups and recheck.'); } return $this->finding('disc-robots-public', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'blogPublic' => $blog_public, 'generalRoot' => $agents['general'], 'oaiRoot' => $agents['oai'], ], 'WordPress and observed robots.txt both allow public crawl discovery at the root.', 'Review robots.txt after SEO or agent policy changes.'); } /** @return array */ private function check_und_llms_emerging(): array { if (!TheDotDevAgenticBridge_Storage::artifacts_published()) { return $this->finding('und-llms-emerging', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_NOT_APPLICABLE, [ 'artifactsPublished' => 'no', ], 'llms.txt emerging-format guidance is scored when plugin-owned llms.txt is published.', 'Publish artifacts when you want llms.txt evaluated as product output.'); } $fetch = $this->fetch('/llms.txt'); if (!$fetch['ok'] || (int) ($fetch['status'] ?? 0) !== 200) { return $this->finding('und-llms-emerging', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => $fetch['status'] ?? 'unknown', ], 'Published artifacts expect a reachable llms.txt route.', 'Republish artifacts and verify /llms.txt returns 200.'); } $body = trim((string) $fetch['body_sample']); if ($body === '') { return $this->finding('und-llms-emerging', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'byteLength' => 0, ], 'Empty llms.txt provides little publisher guidance for agents.', 'Republish with eligible content.'); } return $this->finding('und-llms-emerging', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'byteLength' => strlen($body), ], 'llms.txt is an emerging publisher convention; presence can help agents understand site intent.', 'Keep llms.txt aligned with what you want agents to discover.'); } /** @return array */ private function check_und_agent_map_format(): array { if (!TheDotDevAgenticBridge_Storage::artifacts_published()) { return $this->finding('und-agent-map-format', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_NOT_APPLICABLE, [], 'agent-map.json format is evaluated only when plugin artifacts are published.', 'Publish /agent-map.json to expose structured discovery metadata.'); } $fetch = $this->fetch('/agent-map.json'); if (!$fetch['ok']) { return $this->finding('und-agent-map-format', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, [ 'error' => $fetch['error'], ], 'Could not verify agent-map.json without a same-origin response.', 'Retry when the public route is reachable from the server.'); } if ((int) ($fetch['status'] ?? 0) !== 200) { return $this->finding('und-agent-map-format', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'httpStatus' => $fetch['status'], ], 'Published agent-map.json should return HTTP 200.', 'Republish artifacts or fix the route.'); } if (!$this->is_json_content_type((string) $fetch['content_type'])) { return $this->finding('und-agent-map-format', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'httpStatus' => 200, 'contentType' => $fetch['content_type'], ], 'Published agent-map.json must be served as application/json, not HTML or other types.', 'Configure the route to return application/json for plugin-owned agent-map.json.'); } $decoded = json_decode((string) $fetch['body_sample'], true); $validation = TheDotDevAgenticBridge_Readiness_Artifacts::validate_agent_map($decoded); if (!$validation['valid']) { return $this->finding('und-agent-map-format', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'validationErrors' => $validation['errors'], 'entryCount' => $validation['entryCount'], ], 'Invalid agent-map.json prevents agents from reading the discovery manifest.', 'Republish artifacts to regenerate valid agent-map.json.'); } $semantic = TheDotDevAgenticBridge_Readiness_Artifacts::compare_public_agent_map($decoded); if ($semantic['match'] === false) { return $this->finding('und-agent-map-format', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'semanticMatch' => 'no', ], 'Public agent-map.json bytes do not match the plugin-owned expected artifact.', 'Republish artifacts or remove conflicting route output.'); } return $this->finding('und-agent-map-format', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'entryCount' => $validation['entryCount'], ], 'Valid agent-map.json helps agents consume bounded discovery metadata.', 'Recheck after publishing or exclusion changes.'); } /** @return array */ private function check_und_home_schema(): array { $fetch = $this->fetch('/'); if (!$fetch['ok']) { return $this->finding('und-home-schema', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, [ 'error' => $fetch['error'], ], 'Homepage structured data could not be verified without a same-origin homepage response.', 'Retry when the homepage is reachable from the server.'); } $status = (int) ($fetch['status'] ?? 0); if ($status !== 200) { return $this->finding('und-home-schema', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => $status, ], 'Homepage structured data requires an HTTP 200 homepage response.', 'Ensure the homepage returns HTTP 200 before evaluating JSON-LD.'); } if (!$this->is_htmlish_content_type((string) $fetch['content_type'])) { return $this->finding('und-home-schema', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => $status, 'contentType' => $fetch['content_type'], ], 'Homepage structured data requires an HTML response content type.', 'Serve text/html (or equivalent) at the homepage before evaluating JSON-LD.'); } $analysis = TheDotDevAgenticBridge_Readiness_Schema::analyze_html((string) $fetch['body_sample']); if ($analysis['status'] === 'malformed') { return $this->finding('und-home-schema', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'blocks' => $analysis['blocks'], 'malformedBlocks' => $analysis['malformedBlocks'], ], 'Malformed JSON-LD on the homepage reduces machine-readable understanding.', 'Fix JSON-LD syntax; malformed blocks are not treated as absence.'); } if ($analysis['status'] === 'mixed') { return $this->finding('und-home-schema', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'validBlocks' => $analysis['validBlocks'], 'malformedBlocks' => $analysis['malformedBlocks'], ], 'Mixed valid and malformed JSON-LD weakens structured data signals.', 'Fix malformed JSON-LD blocks and keep markup aligned with visible content.'); } if ($analysis['status'] === 'absent') { return $this->finding('und-home-schema', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [], 'No JSON-LD blocks were detected on the homepage.', 'Add accurate JSON-LD for key entities if appropriate; type presence alone does not guarantee rich results.'); } if ($analysis['status'] === 'no_schema_org_context') { return $this->finding('und-home-schema', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'validBlocks' => $analysis['validBlocks'], 'schemaOrgContext' => 0, ], 'Valid JSON-LD was detected without a recognized Schema.org context.', 'Use https://schema.org context values; impostor domains are not counted.'); } $quality = $analysis['qualitySignals']; $has_quality = ($quality['nameCount'] + $quality['urlCount'] + $quality['idCount']) > 0; if ($analysis['schemaOrgContext'] < 1 || !$has_quality) { return $this->finding('und-home-schema', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'types' => $analysis['types'], 'schemaOrgContext' => $analysis['schemaOrgContext'], ], 'JSON-LD must include recognized Schema.org context and bounded quality signals (name/url/id). Type presence alone does not prove rich-result eligibility.', 'Add accurate Schema.org context plus name, url, or @id fields where appropriate.'); } return $this->finding('und-home-schema', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'types' => $analysis['types'], 'validBlocks' => $analysis['validBlocks'], 'schemaOrgContext' => $analysis['schemaOrgContext'], 'nameCount' => $quality['nameCount'], 'urlCount' => $quality['urlCount'], 'idCount' => $quality['idCount'], ], 'Homepage JSON-LD provides bounded entity signals. Google does not guarantee rich results from valid markup alone.', 'Keep structured data accurate and aligned with visible content.'); } /** @return array */ private function check_und_artifact_manifest(): array { if (!TheDotDevAgenticBridge_Storage::artifacts_published()) { return $this->finding('und-artifact-manifest', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_NOT_APPLICABLE, [], 'Manifest alignment is scored after artifacts are published.', 'Publish artifacts when ready, then recheck manifest alignment.'); } $payload = TheDotDevAgenticBridge_Artifacts::published_artifact_payload(); $ids = is_array($payload['content_ids'] ?? null) ? $payload['content_ids'] : []; $entry_ids = TheDotDevAgenticBridge_Readiness_Artifacts::entry_ids_from_map(is_array($payload['agent_map'] ?? null) ? $payload['agent_map'] : []); $divergent = TheDotDevAgenticBridge_Readiness_Artifacts::count_divergent_ids($ids, $entry_ids); if ($divergent > 0) { return $this->finding('und-artifact-manifest', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'manifestCount' => count($ids), 'entryCount' => count($entry_ids), 'divergentCount' => $divergent, ], 'Manifest content IDs and agent-map entry IDs must align.', 'Republish artifacts after eligibility or exclusion changes.'); } if ($ids === []) { return $this->finding('und-artifact-manifest', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'contentIds' => 0, ], 'Published artifacts currently expose no eligible content IDs.', 'Review exclusions and eligibility, then republish if content should be discoverable.'); } return $this->finding('und-artifact-manifest', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'contentIds' => count($ids), ], 'Published artifacts include eligible content identifiers aligned with the local policy.', 'Republish after eligibility or exclusion changes.'); } /** @return array */ private function check_trn_https(): array { $using_https = function_exists('wp_is_using_https') ? wp_is_using_https() : str_starts_with(home_url('/'), 'https://'); if ($using_https) { return $this->finding('trn-https', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'https' => 'yes', ], 'HTTPS protects transport integrity for visitors and agents.', 'Maintain valid TLS certificates and HTTPS redirects.'); } return $this->finding('trn-https', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'https' => 'no', ], 'Serving agent discovery artifacts over HTTP weakens transport trust.', 'Enable HTTPS for the public site and admin URLs.'); } /** @return array */ private function check_trn_url_consistency(): array { $home = wp_parse_url(home_url('/')); $site = wp_parse_url(site_url('/')); if (!is_array($home) || !is_array($site)) { return $this->finding('trn-url-consistency', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, [], 'Could not compare home_url and site_url.', 'Verify WordPress Address and Site Address settings.'); } $home_host = strtolower((string) ($home['host'] ?? '')); $site_host = strtolower((string) ($site['host'] ?? '')); $home_scheme = strtolower((string) ($home['scheme'] ?? '')); $site_scheme = strtolower((string) ($site['scheme'] ?? '')); if ($home_host === $site_host && $home_scheme === $site_scheme) { return $this->finding('trn-url-consistency', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'host' => $home_host, 'scheme' => $home_scheme, ], 'Consistent scheme and host reduce duplicate or conflicting discovery URLs.', 'Keep WordPress URL settings aligned.'); } return $this->finding('trn-url-consistency', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'homeHost' => $home_host, 'siteHost' => $site_host, 'homeScheme' => $home_scheme, 'siteScheme' => $site_scheme, ], 'Mismatched home/site URLs can produce conflicting canonical discovery endpoints.', 'Align WordPress Address and Site Address scheme/host.'); } /** @return array */ private function check_trn_robots_rules(): array { $fetch = $this->fetch('/robots.txt'); if (!$fetch['ok']) { return $this->finding('trn-robots-rules', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, [ 'error' => $fetch['error'], ], 'robots.txt agent-specific rules could not be verified remotely.', 'Ensure /robots.txt is reachable or rely on local configuration review.'); } $status = (int) ($fetch['status'] ?? 0); if ($status !== 200) { return $this->finding('trn-robots-rules', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => $status, ], 'robots.txt agent rules are only parsed from HTTP 200 responses.', 'Serve robots.txt with HTTP 200 or document intentional absence.'); } if (!$this->is_plain_text_content_type((string) $fetch['content_type'])) { return $this->finding('trn-robots-rules', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'httpStatus' => $status, 'contentType' => $fetch['content_type'], ], 'robots.txt agent rules are only parsed from plain-text HTTP 200 responses.', 'Serve robots.txt as text/plain; HTML responses are not parsed.'); } $agents = TheDotDevAgenticBridge_Readiness_Robots::evaluate_discovery_agents((string) $fetch['body_sample']); $signals = [ 'oaiSearchBot' => $agents['oai'], 'gptBot' => $agents['gptBot'], 'general' => $agents['general'], ]; if ($agents['oai'] === 'disallow') { return $this->finding('trn-robots-rules', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, $signals, 'OAI-SearchBot is disallowed at the root, which can block search-oriented discovery. GPTBot governs training crawls, not search discovery.', 'Allow OAI-SearchBot if you intend OpenAI search discovery, or document an intentional block.'); } if ($agents['oai'] === 'allow' || $agents['general'] === 'allow') { return $this->finding('trn-robots-rules', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, $signals, 'robots.txt allows relevant crawlers at the root. Crawl permission does not guarantee indexing; GPTBot rules govern training use separately.', 'Document intentional agent policy; separate search discovery from training crawlers.'); } if ($agents['oai'] === 'unknown' && $agents['general'] === 'unknown') { return $this->finding('trn-robots-rules', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, $signals, 'robots.txt root policy for key agents could not be determined.', 'Review robots.txt and recheck.'); } return $this->finding('trn-robots-rules', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, $signals, 'robots.txt does not clearly allow or deny key agent user-agents at the root.', 'Add explicit User-agent rules for agents you expect to discover the site.'); } /** @return array */ private function check_trn_search_visibility(): array { $blog_public = (string) get_option('blog_public', '1'); if ($blog_public === '1') { return $this->finding('trn-search-visibility', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'blogPublic' => '1', ], 'WordPress is not discouraging search engine visibility.', 'Keep Reading settings aligned with intended visibility.'); } return $this->finding('trn-search-visibility', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'blogPublic' => $blog_public, ], 'Discouraging search engines reduces discoverability for humans and agents alike.', 'Disable "discourage search engines" unless the site must remain private.'); } private function check_act_intent_declarations(): array { $eligible_count = count(TheDotDevAgenticBridge_Content_Metadata::artifact_eligible_content_items()); if ($eligible_count === 0) { return $this->finding('act-intent-declarations', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'localControls' => 'no-eligible-content', ], 'Intent and capability metadata can only be declared for artifact-eligible published content.', 'Publish eligible posts/pages or resolve policy exclusions before declaring metadata.'); } if (TheDotDevAgenticBridge_Content_Metadata::eligible_metadata_coverage_complete()) { return $this->finding('act-intent-declarations', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'localControls' => 'full-coverage', ], 'All artifact-eligible content is covered by reviewed defaults or valid explicit metadata configurations.', 'Keep metadata aligned with content changes and regenerate previews after updates.'); } return $this->finding('act-intent-declarations', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'localControls' => 'partial-coverage', ], 'Not all artifact-eligible content has reviewed defaults or valid explicit metadata.', 'Review defaults and configure explicit metadata for every eligible page/post.'); } /** @return array */ private function check_act_agent_map_actions(): array { if (!TheDotDevAgenticBridge_Storage::artifacts_published()) { return $this->finding('act-agent-map-actions', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_NOT_APPLICABLE, [], 'Action metadata is evaluated when plugin-owned agent-map.json is published.', 'Generate a preview and publish agent-map.json to expose metadata-only capabilities.'); } $fetch = $this->fetch('/agent-map.json'); if (!$fetch['ok'] || (int) ($fetch['status'] ?? 0) !== 200) { return $this->finding('act-agent-map-actions', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [], 'agent-map.json is published locally but the public route is not reachable.', 'Republish artifacts and verify /agent-map.json returns 200.'); } if (!$this->is_json_content_type((string) $fetch['content_type'])) { return $this->finding('act-agent-map-actions', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'contentType' => $fetch['content_type'], ], 'Published agent-map.json must be served as application/json.', 'Configure the route to return application/json.'); } $decoded = json_decode((string) $fetch['body_sample'], true); $validation = TheDotDevAgenticBridge_Artifact_Validator::validate_agent_map_strict($decoded, true); if (!$validation['valid']) { return $this->finding('act-agent-map-actions', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'validationErrors' => $validation['errors'], ], 'Published agent-map.json failed strict schema 1.1 validation.', 'Regenerate and publish a valid preview.'); } $entries = is_array($decoded['entries'] ?? null) ? $decoded['entries'] : []; if ($entries === []) { return $this->finding('act-agent-map-actions', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'entriesWithActions' => 0, ], 'No published entries were found to evaluate action metadata.', 'Publish eligible content with metadata-only capabilities.'); } return $this->finding('act-agent-map-actions', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'entriesWithActions' => count($entries), ], 'All published agent-map.json entries conform to metadata-only, non-executable schema 1.1.', 'Treat capabilities as descriptive metadata only; they do not enable execution.'); } /** @return array */ private function check_saf_content_policy(): array { if (!TheDotDevAgenticBridge_Storage::artifacts_published()) { return $this->finding('saf-content-policy', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_NOT_APPLICABLE, [], 'Content exposure policy is evaluated against published manifest entries only.', 'Publish artifacts to enable manifest exposure checks.'); } $settings = TheDotDevAgenticBridge_Storage::get(); $last_known = is_array($settings['last_known_good'] ?? null) ? $settings['last_known_good'] : []; $manifest_ids = is_array($last_known['content_ids'] ?? null) ? $last_known['content_ids'] : []; $entry_ids = TheDotDevAgenticBridge_Readiness_Artifacts::entry_ids_from_map(is_array($last_known['agent_map'] ?? null) ? $last_known['agent_map'] : []); $exposure = TheDotDevAgenticBridge_Readiness_Artifacts::evaluate_manifest_exposure($manifest_ids); $divergent = TheDotDevAgenticBridge_Readiness_Artifacts::count_divergent_ids($manifest_ids, $entry_ids); $exposure['divergentCount'] = $divergent; if ($divergent > 0 || $exposure['status'] === 'fail') { return $this->finding('saf-content-policy', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'checkedCount' => $exposure['checkedCount'], 'ineligibleCount' => $exposure['ineligibleCount'], 'missingCount' => $exposure['missingCount'], 'divergentCount' => $divergent, ], 'Published manifest references content that is missing, ineligible, or divergent from agent-map entries.', 'Republish after fixing exclusions, drafts, password protection, noindex, or internal blocks.'); } return $this->finding('saf-content-policy', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'checkedCount' => $exposure['checkedCount'], 'ineligibleCount' => 0, 'missingCount' => 0, 'divergentCount' => 0, ], 'Published manifest entries align with eligible local content policy.', 'Review exclusions regularly and keep sensitive content out of published artifacts.'); } /** @return array */ private function check_saf_kill_switch(): array { if (TheDotDevAgenticBridge_Storage::kill_switch_enabled()) { return $this->finding('saf-kill-switch', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'killSwitch' => 'on', ], 'Kill switch suppresses public artifacts — an intentional safety state that blocks discovery.', 'Disable kill switch when artifacts should be public again.'); } return $this->finding('saf-kill-switch', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'killSwitch' => 'off', ], 'Kill switch is off, so published artifacts can be served when published.', 'Use kill switch only for intentional suppression.'); } /** @return array */ private function check_saf_artifact_stale(): array { if (!TheDotDevAgenticBridge_Storage::artifacts_published()) { return $this->finding('saf-artifact-stale', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_NOT_APPLICABLE, [], 'Freshness is evaluated only after artifacts are published.', 'Publish artifacts to enable freshness checks.'); } $settings = TheDotDevAgenticBridge_Storage::get(); $last_known = is_array($settings['last_known_good'] ?? null) ? $settings['last_known_good'] : []; $current = TheDotDevAgenticBridge_Artifacts::generate_current(); $stored_fp = TheDotDevAgenticBridge_Readiness_Artifacts::semantic_fingerprint($last_known); $current_fp = TheDotDevAgenticBridge_Readiness_Artifacts::semantic_fingerprint($current); $semantic_changed = !hash_equals($stored_fp, $current_fp); $published_at = strtotime((string) ($last_known['published_at'] ?? '')); $age = $published_at === false ? null : TheDotDevAgenticBridge_Readiness_Clock::now() - $published_at; $age_stale = $age !== null && $age > TheDotDevAgenticBridge_Readiness_Methodology::STALE_ARTIFACT_THRESHOLD_SECONDS; if ($semantic_changed) { return $this->finding('saf-artifact-stale', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'semanticChanged' => 'yes', 'ageSeconds' => $age ?? 'unknown', ], 'Eligible content, exclusions, or claims changed since the last published artifact fingerprint.', 'Republish artifacts to refresh the public manifest.'); } if ($age_stale) { return $this->finding('saf-artifact-stale', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'semanticChanged' => 'no', 'ageSeconds' => $age, 'thresholdSeconds' => TheDotDevAgenticBridge_Readiness_Methodology::STALE_ARTIFACT_THRESHOLD_SECONDS, ], 'Published artifacts are older than the documented 7-day freshness threshold.', 'Review content changes and republish artifacts if needed.'); } return $this->finding('saf-artifact-stale', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'semanticChanged' => 'no', 'ageSeconds' => $age ?? 'unknown', ], 'Published artifacts match the current semantic fingerprint and are within the freshness threshold.', 'Republish after meaningful content or exclusion changes.'); } /** @return array */ private function check_saf_physical_llms_conflict(): array { if (TheDotDevAgenticBridge_Snapshot::has_physical_llms_txt()) { return $this->finding('saf-physical-llms-conflict', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, [ 'physicalPath' => '/llms.txt', ], 'A physical llms.txt file on disk can conflict with plugin rewrite routes.', 'Remove or relocate the physical file, or document which source is authoritative.'); } return $this->finding('saf-physical-llms-conflict', TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [], 'No physical llms.txt conflict detected at the web root.', 'Monitor for new static files that could override plugin routes.'); } /** @return array{ok: bool, status: int|null, content_type: string, body_sample: string, error: string, final_url: string, oversized: bool} */ private function fetch(string $path): array { if (!isset($this->cache[$path])) { $this->cache[$path] = TheDotDevAgenticBridge_Same_Origin_Observer::fetch($path); } return $this->cache[$path]; } /** @return list */ private static function sitemap_candidates(): array { $paths = []; if (function_exists('wp_sitemaps_get_server')) { $paths[] = '/wp-sitemap.xml'; } $paths[] = '/sitemap.xml'; $paths[] = '/sitemap_index.xml'; return array_values(array_unique($paths)); } /** * @param array $evidence * @return array */ private function finding(string $id, string $status, array $evidence, string $why, string $next): array { return [ 'id' => $id, 'status' => $status, 'evidence' => $evidence, 'whyItMatters' => $why, 'nextStep' => $next, 'lastChecked' => TheDotDevAgenticBridge_Readiness_Clock::iso(), ]; } private 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); } private function is_htmlish_content_type(string $content_type): bool { $primary = strtolower(trim(explode(';', $content_type)[0])); if ($primary === '') { return false; } return in_array($primary, ['text/html', 'application/xhtml+xml'], true); } private function is_plain_text_content_type(string $content_type): bool { $primary = strtolower(trim(explode(';', $content_type)[0])); return $primary === 'text/plain'; } private function is_json_content_type(string $content_type): bool { $primary = strtolower(trim(explode(';', $content_type)[0])); if ($primary === '') { return false; } return $primary === 'application/json' || str_ends_with($primary, '+json'); } }