*/ public static function checks(): array { return [ ['id' => self::CHECK_TITLE, 'category' => self::CATEGORY_UNDERSTANDING, 'weight' => 20, 'title' => 'Title present'], ['id' => self::CHECK_SUMMARY, 'category' => self::CATEGORY_UNDERSTANDING, 'weight' => 20, 'title' => 'Agent summary present'], ['id' => self::CHECK_INTENT, 'category' => self::CATEGORY_UNDERSTANDING, 'weight' => 20, 'title' => 'Intent selected'], ['id' => self::CHECK_INTERNAL_LINKS, 'category' => self::CATEGORY_DISCOVERABILITY, 'weight' => 20, 'title' => 'Internal links present'], [ 'id' => self::CHECK_ARTIFACT_INCLUSION, 'category' => self::CATEGORY_DISCOVERABILITY, 'weight' => 20, 'title' => 'llms.txt / agent-map inclusion', ], ]; } public static function is_valid_check_id(string $id): bool { if ($id === self::CHECK_ELIGIBILITY) { return true; } foreach (self::checks() as $check) { if ($check['id'] === $id) { return true; } } return false; } /** * Score one post/page. Fail-closed for ineligible content (including non-post/page types). * * @return array{ * version: string, * eligible: bool, * contentId: int, * contentType: string, * score: int, * findings: list>, * scoredAt: string * } */ public static function score(WP_Post $post): array { $scored_at = TheDotDevAgenticBridge_Readiness_Clock::iso(); $evaluation = TheDotDevAgenticBridge_Content_Policy::evaluate_post($post); if (!$evaluation['eligible']) { $finding = self::normalize_finding([ 'id' => self::CHECK_ELIGIBILITY, 'category' => self::CATEGORY_DISCOVERABILITY, 'title' => 'Content eligibility', 'status' => TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, 'weight' => self::OVERALL_SCORE_CAP, 'evidence' => [ 'contentId' => (string) $post->ID, 'contentType' => (string) $post->post_type, 'reason' => (string) ($evaluation['reason'] ?? TheDotDevAgenticBridge_Content_Policy::REASON_MISSING), ], 'recommendation' => 'Only public eligible posts and pages receive an Agent Ready score. Fix eligibility before scoring.', 'lastChecked' => $scored_at, ]); return [ 'version' => self::VERSION, 'eligible' => false, 'contentId' => (int) $post->ID, 'contentType' => (string) $post->post_type, 'score' => 0, 'findings' => [$finding], 'scoredAt' => $scored_at, ]; } $config = TheDotDevAgenticBridge_Content_Metadata::effective_config_for_post($post); $findings = [ self::check_title($post, $scored_at), self::check_summary($config, $scored_at), self::check_intent($config, $scored_at), self::check_internal_links($post, $scored_at), self::check_artifact_inclusion($post, $config, $scored_at), ]; $normalized = []; foreach ($findings as $finding) { $normalized[] = self::normalize_finding($finding); } return [ 'version' => self::VERSION, 'eligible' => true, 'contentId' => (int) $post->ID, 'contentType' => (string) $post->post_type, 'score' => self::score_findings($normalized), 'findings' => $normalized, 'scoredAt' => $scored_at, ]; } /** * @param list> $findings */ public static function score_findings(array $findings): int { $by_id = []; foreach ($findings as $finding) { if (!is_array($finding) || empty($finding['id'])) { continue; } $by_id[(string) $finding['id']] = $finding; } $total = 0; foreach (self::checks() as $check) { $id = (string) $check['id']; $weight = (int) $check['weight']; $status = is_array($by_id[$id] ?? null) ? (string) ($by_id[$id]['status'] ?? TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN) : TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN; $total += TheDotDevAgenticBridge_Readiness_Methodology::points_for_status($status, $weight); } return max(0, min(self::OVERALL_SCORE_CAP, $total)); } /** @param array $partial @return array */ public static function normalize_finding(array $partial): array { $id = (string) ($partial['id'] ?? ''); if (!self::is_valid_check_id($id)) { throw new InvalidArgumentException('Unknown content Agent Ready check id: ' . sanitize_key($id)); } $definition = null; foreach (self::checks() as $check) { if ($check['id'] === $id) { $definition = $check; break; } } $status = TheDotDevAgenticBridge_Readiness_Methodology::normalize_status( (string) ($partial['status'] ?? TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN) ); $weight = $definition !== null ? (int) $definition['weight'] : max(0, (int) ($partial['weight'] ?? 0)); $category = $definition !== null ? (string) $definition['category'] : (string) ($partial['category'] ?? self::CATEGORY_DISCOVERABILITY); $title = $definition !== null ? (string) $definition['title'] : (string) ($partial['title'] ?? $id); return [ 'id' => $id, 'category' => $category, 'title' => $title, 'severity' => TheDotDevAgenticBridge_Readiness_Methodology::severity_for_status($status), 'status' => $status, 'weight' => $weight, 'earnedPoints' => TheDotDevAgenticBridge_Readiness_Methodology::points_for_status($status, $weight), 'evidence' => self::bound_evidence($partial['evidence'] ?? []), 'recommendation' => self::escape_text((string) ($partial['recommendation'] ?? ''), self::MAX_EVIDENCE_CHARS), 'lastChecked' => (string) ($partial['lastChecked'] ?? TheDotDevAgenticBridge_Readiness_Clock::iso()), ]; } /** @param mixed $evidence @return array> */ public static function bound_evidence(mixed $evidence): array { if (!is_array($evidence)) { return []; } $out = []; $count = 0; foreach ($evidence as $key => $value) { if ($count >= self::MAX_EVIDENCE_ENTRIES) { break; } if (!self::is_allowed_evidence_key((string) $key)) { continue; } if (self::is_forbidden_evidence_value($value)) { continue; } if (is_scalar($value) || $value === null) { $bounded = self::escape_text((string) $value, self::MAX_EVIDENCE_CHARS); if (self::is_forbidden_evidence_value($bounded)) { continue; } $out[(string) $key] = $bounded; } elseif (is_array($value)) { $flat = []; foreach ($value as $item) { if (count($flat) >= 8) { break; } if (is_scalar($item) && !self::is_forbidden_evidence_value($item)) { $flat[] = self::escape_text((string) $item, 120); } } if ($flat !== []) { $out[(string) $key] = $flat; } } ++$count; } return $out; } public static function is_allowed_evidence_key(string $key): bool { $normalized = preg_replace('/[^a-z0-9]/', '', strtolower($key)) ?? ''; return $normalized !== '' && isset(self::allowed_evidence_keys()[$normalized]); } /** @return array */ public static function allowed_evidence_keys(): array { static $keys = null; if ($keys !== null) { return $keys; } $list = [ 'contentid', 'contenttype', 'reason', 'titlepresent', 'titlelength', 'titlepreview', 'summarypresent', 'summarylength', 'intent', 'internallinkcount', 'inclusionmode', 'artifacteligible', 'llmsincluded', 'agentmapincluded', ]; $keys = array_fill_keys($list, true); return $keys; } /** @return array */ private static function check_title(WP_Post $post, string $scored_at): array { $raw_title = trim(wp_strip_all_tags(get_the_title($post))); $present = $raw_title !== ''; if ($present) { return self::partial_finding(self::CHECK_TITLE, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'titlePresent' => 'yes', 'titleLength' => (string) strlen($raw_title), 'titlePreview' => substr($raw_title, 0, self::MAX_TITLE_PREVIEW_CHARS), ], 'Keep a clear public title so agents can name this page accurately.', $scored_at); } return self::partial_finding(self::CHECK_TITLE, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'titlePresent' => 'no', 'titleLength' => '0', ], 'Add a public title before publishing Agent Ready guidance for this content.', $scored_at); } /** * @param array $config * @return array */ private static function check_summary(array $config, string $scored_at): array { $summary = trim((string) ($config['agentSummary'] ?? '')); $present = $summary !== ''; if ($present) { return self::partial_finding(self::CHECK_SUMMARY, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'summaryPresent' => 'yes', 'summaryLength' => (string) strlen($summary), ], 'Review the agent summary when the page purpose changes.', $scored_at); } return self::partial_finding(self::CHECK_SUMMARY, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'summaryPresent' => 'no', 'summaryLength' => '0', ], 'Add a bounded agent summary in content metadata so agents receive a safe page gist.', $scored_at); } /** * @param array $config * @return array */ private static function check_intent(array $config, string $scored_at): array { $intent = (string) ($config['intent'] ?? ''); $selected = $intent !== '' && in_array($intent, TheDotDevAgenticBridge_Content_Metadata::INTENTS, true); if ($selected) { return self::partial_finding(self::CHECK_INTENT, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'intent' => $intent, ], 'Keep intent aligned with the public purpose of this page.', $scored_at); } return self::partial_finding(self::CHECK_INTENT, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'intent' => $intent === '' ? 'missing' : $intent, ], 'Select a supported intent in content metadata.', $scored_at); } /** @return array */ private static function check_internal_links(WP_Post $post, string $scored_at): array { $count = self::count_internal_links((string) $post->post_content); if ($count > 0) { return self::partial_finding(self::CHECK_INTERNAL_LINKS, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, [ 'internalLinkCount' => (string) $count, ], 'Maintain useful same-site links so agents can traverse related public pages.', $scored_at); } return self::partial_finding(self::CHECK_INTERNAL_LINKS, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, [ 'internalLinkCount' => '0', ], 'Add at least one same-site internal link to help agents discover related public content.', $scored_at); } /** * @param array $config * @return array */ private static function check_artifact_inclusion(WP_Post $post, array $config, string $scored_at): array { $mode = (string) ($config['inclusionMode'] ?? TheDotDevAgenticBridge_Content_Metadata::INCLUSION_INHERIT); $artifact_eligible = TheDotDevAgenticBridge_Content_Metadata::is_artifact_eligible($post); $included = $artifact_eligible ? 'yes' : 'no'; $evidence = [ 'inclusionMode' => $mode, 'artifactEligible' => $artifact_eligible ? 'yes' : 'no', 'llmsIncluded' => $included, 'agentMapIncluded' => $included, ]; if ($mode === TheDotDevAgenticBridge_Content_Metadata::INCLUSION_EXCLUDE || !$artifact_eligible) { return self::partial_finding( self::CHECK_ARTIFACT_INCLUSION, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, $evidence, 'Set inclusion to include (or inherit defaults) so this page can appear in llms.txt and agent-map.json.', $scored_at ); } if ($mode === TheDotDevAgenticBridge_Content_Metadata::INCLUSION_INCLUDE) { return self::partial_finding( self::CHECK_ARTIFACT_INCLUSION, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, $evidence, 'Keep inclusion aligned with publishing decisions after content changes.', $scored_at ); } return self::partial_finding( self::CHECK_ARTIFACT_INCLUSION, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, $evidence, 'Consider an explicit include so llms.txt / agent-map inclusion is intentional for this page.', $scored_at ); } /** * Count same-site href targets. Does not store content body or raw secrets. */ public static function count_internal_links(string $content): int { if ($content === '' || !preg_match_all('/\bhref\s*=\s*(["\'])(.*?)\1/i', $content, $matches)) { return 0; } $home_host = strtolower((string) (wp_parse_url(home_url('/'), PHP_URL_HOST) ?? '')); $count = 0; foreach ($matches[2] as $href) { $href = trim(html_entity_decode((string) $href, ENT_QUOTES | ENT_HTML5, 'UTF-8')); if ($href === '' || str_starts_with($href, '#') || str_starts_with(strtolower($href), 'mailto:') || str_starts_with(strtolower($href), 'tel:') || str_starts_with(strtolower($href), 'javascript:') || str_starts_with(strtolower($href), 'data:') ) { continue; } if (str_starts_with($href, '/') && !str_starts_with($href, '//')) { ++$count; continue; } $host = wp_parse_url($href, PHP_URL_HOST); if (is_string($host) && $home_host !== '' && strtolower($host) === $home_host) { ++$count; } } return $count; } /** * @param array $evidence * @return array */ private static function partial_finding( string $id, string $status, array $evidence, string $recommendation, string $scored_at ): array { return [ 'id' => $id, 'status' => $status, 'evidence' => $evidence, 'recommendation' => $recommendation, 'lastChecked' => $scored_at, ]; } private static function escape_text(string $value, int $max_chars): string { $trimmed = substr($value, 0, max(0, $max_chars)); return esc_html($trimmed); } private static function is_forbidden_evidence_value(mixed $value): bool { if (!is_scalar($value)) { return false; } $normalized = strtolower((string) $value); return str_contains($normalized, 'secret_sentinel') || str_contains($normalized, 'password_sentinel') || str_contains($normalized, 'token_sentinel') || str_contains($normalized, 'credential_sentinel') || str_contains($normalized, 'nonce_sentinel'); } }