*/ private const ALLOWED_TOP_LEVEL = ['schemaVersion', 'siteUrl', 'siteName', 'generatedAt', 'state', 'policy', 'entries']; /** @var list */ private const ALLOWED_POLICY_KEYS = ['metadataOnly', 'executable']; /** @var list */ private const ALLOWED_ENTRY_KEYS = [ 'url', 'title', 'type', 'contentId', 'intent', 'summary', 'allowedActions', 'blockedClaims', 'relationship', ]; /** @var list */ private const ALLOWED_ACTION_KEYS = ['id', 'mode', 'executable']; /** @var list */ private const ALLOWED_RELATIONSHIP_KEYS = ['label', 'type']; /** @var list */ public const RELATIONSHIP_TYPES = ['service', 'product']; /** * @param array $last_known * @return array{llms_txt: string, agent_map: array, content_ids: list}|null */ public static function sanitize_published_lkg(array $last_known): ?array { $agent_map = is_array($last_known['agent_map'] ?? null) ? $last_known['agent_map'] : []; $validation = self::validate_agent_map_strict($agent_map, true); if (!$validation['valid']) { return null; } $stored_llms_raw = $last_known['llms_txt'] ?? null; if (!is_string($stored_llms_raw)) { return null; } $stored_llms = $stored_llms_raw; $manifest = self::normalize_content_ids($last_known['content_ids'] ?? []); $entries = is_array($agent_map['entries'] ?? null) ? $agent_map['entries'] : []; $filtered = self::filter_entries_for_current_eligibility($entries, $manifest); if ($filtered === null) { return self::empty_safe_payload($agent_map); } $original_ids = self::entry_ids_from_map($agent_map); $filtered_ids = $filtered['content_ids']; sort($original_ids); sort($filtered_ids); $agent_map['entries'] = $filtered['entries']; $content_ids = $filtered['content_ids']; if ($original_ids === $filtered_ids && $stored_llms !== '') { $llms_txt = $stored_llms; } else { $llms_txt = self::build_llms_txt_from_map($agent_map); } return [ 'llms_txt' => $llms_txt, 'agent_map' => $agent_map, 'content_ids' => $content_ids, ]; } /** * @param array $draft * @return array{ok: bool, error: string} */ public static function validate_preview_draft_for_publish(array $draft): array { $llms_raw = $draft['llms_txt'] ?? null; if (!is_string($llms_raw)) { return ['ok' => false, 'error' => 'draft_invalid']; } if (strlen($llms_raw) > self::MAX_LLMS_BYTES) { return ['ok' => false, 'error' => 'draft_oversize']; } $agent_map = is_array($draft['agent_map'] ?? null) ? $draft['agent_map'] : []; $encoded = wp_json_encode($agent_map) ?: ''; if (strlen($encoded) > self::MAX_AGENT_MAP_BYTES) { return ['ok' => false, 'error' => 'draft_oversize']; } $validation = self::validate_agent_map_strict($agent_map, true); if (!$validation['valid']) { return ['ok' => false, 'error' => 'draft_invalid']; } $expected_llms = self::build_llms_txt_from_map($agent_map); if ($llms_raw !== $expected_llms) { return ['ok' => false, 'error' => 'llms_mismatch']; } $manifest = self::normalize_content_ids($draft['content_ids'] ?? []); $entry_ids = self::entry_ids_from_map($agent_map); sort($manifest); sort($entry_ids); if ($manifest !== $entry_ids) { return ['ok' => false, 'error' => 'manifest_mismatch']; } $metadata_store = TheDotDevAgenticBridge_Content_Metadata::normalized_store(); foreach ($entry_ids as $content_id) { $post = get_post($content_id); if (!$post instanceof WP_Post || !TheDotDevAgenticBridge_Content_Metadata::is_artifact_eligible($post, $metadata_store)) { return ['ok' => false, 'error' => 'draft_ineligible']; } } return ['ok' => true, 'error' => '']; } /** * @return array{valid: bool, errors: list, entryCount: int} */ public static function validate_agent_map_strict(mixed $decoded, bool $require_11 = false): array { $errors = []; if (!is_array($decoded) || !self::is_json_object_shape($decoded)) { return ['valid' => false, 'errors' => ['not_object'], 'entryCount' => 0]; } foreach (array_keys($decoded) as $key) { if (!in_array((string) $key, self::ALLOWED_TOP_LEVEL, true)) { $errors[] = 'unknown_top_level'; } } $schema_raw = $decoded['schemaVersion'] ?? null; $schema = is_string($schema_raw) ? $schema_raw : ''; if ($require_11 && !in_array($schema, ['1.1', '1.2'], true)) { $errors[] = 'schemaVersion'; } elseif (!in_array($schema, ['1.0', '1.1', '1.2'], true)) { $errors[] = 'schemaVersion'; } $state_raw = $decoded['state'] ?? null; if (!is_string($state_raw) || $state_raw !== 'published') { $errors[] = 'state'; } $site_url_raw = $decoded['siteUrl'] ?? null; if (!is_string($site_url_raw)) { $errors[] = 'siteUrl'; } else { $site_url = rtrim($site_url_raw, '/'); $expected = rtrim(home_url('/'), '/'); if ($site_url === '' || $site_url !== $expected) { $errors[] = 'siteUrl'; } } if (in_array($schema, ['1.1', '1.2'], true)) { $site_name_raw = $decoded['siteName'] ?? null; if (!is_string($site_name_raw) || !self::validate_site_name($site_name_raw)) { $errors[] = 'siteName'; } $generated_at_raw = $decoded['generatedAt'] ?? null; if (!is_string($generated_at_raw) || !self::is_valid_iso_datetime($generated_at_raw)) { $errors[] = 'generatedAt'; } $policy = $decoded['policy'] ?? null; if (!is_array($policy) || !self::is_json_object_shape($policy)) { $errors[] = 'policy'; } else { foreach (array_keys($policy) as $key) { if (!in_array((string) $key, self::ALLOWED_POLICY_KEYS, true)) { $errors[] = 'unknown_policy_key'; } } if (!is_bool($policy['metadataOnly'] ?? null) || $policy['metadataOnly'] !== true || !is_bool($policy['executable'] ?? null) || $policy['executable'] !== false) { $errors[] = 'policy'; } } } $entries = $decoded['entries'] ?? null; if (!is_array($entries) || !TheDotDevAgenticBridge_Compat::array_is_list($entries)) { return ['valid' => false, 'errors' => array_values(array_unique(array_merge($errors, ['entries']))), 'entryCount' => 0]; } if (count($entries) > ($schema === '1.2' ? self::MAX_ENTRIES : 50)) { $errors[] = 'too_many_entries'; } $seen_ids = []; $entry_count = 0; foreach ($entries as $entry) { if (!is_array($entry)) { $errors[] = 'entry_shape'; continue; } ++$entry_count; foreach (array_keys($entry) as $key) { if (!in_array((string) $key, self::ALLOWED_ENTRY_KEYS, true)) { $errors[] = 'unknown_entry_key'; } } $content_id = $entry['contentId'] ?? null; if (!self::is_strict_positive_int($content_id)) { $errors[] = 'entry_content_id'; } elseif (isset($seen_ids[$content_id])) { $errors[] = 'duplicate_content_id'; } else { $seen_ids[$content_id] = true; } $title_raw = $entry['title'] ?? null; if (!is_string($title_raw) || trim($title_raw) === '' || self::utf8_length($title_raw) > self::MAX_TITLE_CHARS) { $errors[] = 'entry_title'; } $type_raw = $entry['type'] ?? null; if (!is_string($type_raw) || !in_array($type_raw, ['post', 'page'], true)) { $errors[] = 'entry_type'; } $url_raw = $entry['url'] ?? null; if (!is_string($url_raw) || !self::is_valid_entry_url($url_raw)) { $errors[] = 'entry_url'; } if (in_array($schema, ['1.1', '1.2'], true)) { $intent_raw = $entry['intent'] ?? null; if (!is_string($intent_raw) || !in_array($intent_raw, TheDotDevAgenticBridge_Content_Metadata::INTENTS, true)) { $errors[] = 'entry_intent'; } if (!self::validate_entry_actions($entry['allowedActions'] ?? null)) { $errors[] = 'entry_allowed_actions'; } if (!self::validate_blocked_claims_list($entry['blockedClaims'] ?? null)) { $errors[] = 'entry_blocked_claims'; } if (array_key_exists('summary', $entry)) { $summary_raw = $entry['summary']; if (!is_string($summary_raw) || !self::validate_summary_text($summary_raw)) { $errors[] = 'entry_summary'; } } if (array_key_exists('relationship', $entry) && !self::validate_relationship($entry['relationship'])) { $errors[] = 'entry_relationship'; } } } return ['valid' => $errors === [], 'errors' => array_values(array_unique($errors)), 'entryCount' => $entry_count]; } /** @return array{llms_txt: string, agent_map: array, content_ids: list} */ public static function empty_safe_payload(?array $template_map = null): array { $generated_at = gmdate('c'); $site_name = self::artifact_site_name(); if (is_array($template_map)) { $generated_raw = $template_map['generatedAt'] ?? null; if (is_string($generated_raw) && $generated_raw !== '') { $generated_at = $generated_raw; } $site_name_raw = $template_map['siteName'] ?? null; if (is_string($site_name_raw) && $site_name_raw !== '') { $site_name = $site_name_raw; } } $agent_map = [ 'schemaVersion' => '1.1', 'siteUrl' => home_url('/'), 'siteName' => $site_name, 'generatedAt' => $generated_at, 'state' => 'published', 'policy' => ['metadataOnly' => true, 'executable' => false], 'entries' => [], ]; return [ 'llms_txt' => self::build_llms_txt_from_map($agent_map), 'agent_map' => $agent_map, 'content_ids' => [], ]; } /** * @param array $agent_map */ public static function build_llms_txt_from_map(array $agent_map): string { $entries = is_array($agent_map['entries'] ?? null) ? $agent_map['entries'] : []; $site_name_raw = $agent_map['siteName'] ?? null; $site_url_raw = $agent_map['siteUrl'] ?? null; return self::build_llms_txt_from_entries( $entries, is_string($site_name_raw) ? $site_name_raw : get_bloginfo('name'), is_string($site_url_raw) ? $site_url_raw : home_url('/'), ); } /** * @param list> $entries */ public static function build_llms_txt_from_entries(array $entries, string $site_name, string $site_url): string { $lines = [ '# ' . $site_name, $site_url, '', '# Policy: capabilities are metadata-only (not executable) and blocked claims are prohibitions only.', '# This file is a product publisher aid, not a universal web standard or ranking guarantee.', '', ]; foreach ($entries as $entry) { if (!is_array($entry)) { continue; } $url_raw = $entry['url'] ?? null; $title_raw = $entry['title'] ?? null; if (!is_string($url_raw) || !is_string($title_raw)) { continue; } $lines[] = $url_raw; $lines[] = '# ' . $title_raw; $intent_raw = $entry['intent'] ?? 'general_information'; $lines[] = '# intent: ' . (is_string($intent_raw) ? $intent_raw : 'general_information'); $summary_raw = $entry['summary'] ?? ''; if (is_string($summary_raw) && $summary_raw !== '') { $lines[] = $summary_raw; } $action_labels = []; foreach (is_array($entry['allowedActions'] ?? null) ? $entry['allowedActions'] : [] as $action) { if (!is_array($action)) { continue; } $id_raw = $action['id'] ?? null; if (!is_string($id_raw) && !is_int($id_raw)) { continue; } $action_labels[] = (is_string($id_raw) ? $id_raw : (string) $id_raw) . ' (metadata_only; not executable)'; } if ($action_labels !== []) { $lines[] = '# capabilities (metadata-only, not execution or availability proof): ' . implode(', ', $action_labels); } $blocked = is_array($entry['blockedClaims'] ?? null) ? $entry['blockedClaims'] : []; if ($blocked !== []) { $blocked_strings = []; foreach ($blocked as $claim) { if (is_string($claim)) { $blocked_strings[] = $claim; } } if ($blocked_strings !== []) { $lines[] = '# prohibited claims: ' . implode(', ', $blocked_strings); } } $lines[] = ''; } return implode("\n", $lines); } /** * @param list> $entries * @param list $manifest * @return array{entries: list>, content_ids: list}|null */ public static function filter_entries_for_current_eligibility(array $entries, array $manifest): ?array { $metadata_store = TheDotDevAgenticBridge_Content_Metadata::normalized_store(); $manifest_lookup = array_fill_keys($manifest, true); $filtered = []; $content_ids = []; foreach ($entries as $entry) { if (!is_array($entry)) { continue; } $content_id = $entry['contentId'] ?? null; if (!self::is_strict_positive_int($content_id) || !isset($manifest_lookup[$content_id])) { continue; } $post = get_post($content_id); if (!$post instanceof WP_Post || !TheDotDevAgenticBridge_Content_Metadata::is_artifact_eligible($post, $metadata_store)) { continue; } $filtered[] = $entry; $content_ids[] = $content_id; } if ($filtered === []) { return null; } return ['entries' => $filtered, 'content_ids' => array_values(array_unique($content_ids))]; } /** @param array $map */ public static function semantic_map_fingerprint(array $map): string { $normalized = $map; unset($normalized['generatedAt']); return hash('sha256', wp_json_encode($normalized) ?: ''); } public static function validate_summary_text(string $summary): bool { if ($summary === '') { return true; } if (self::utf8_length($summary) > self::MAX_SUMMARY_CHARS) { return false; } return !TheDotDevAgenticBridge_Content_Metadata::contains_dangerous_text_public($summary); } public static function validate_site_name(string $site_name): bool { $site_name = trim($site_name); if ($site_name === '' || self::utf8_length($site_name) > self::MAX_SITE_NAME_CHARS) { return false; } return !TheDotDevAgenticBridge_Content_Metadata::contains_dangerous_text_public($site_name); } /** * Return a deterministic, public-safe label for artifact output. * * WordPress permits an empty Site Title. Artifact preview must not fail in * that valid installation state, so fall back to the site's public host * and finally to a neutral label. */ public static function artifact_site_name(): string { $site_name = trim((string) get_bloginfo('name')); if (self::validate_site_name($site_name)) { return $site_name; } $host = wp_parse_url(home_url('/'), PHP_URL_HOST); $host = is_string($host) ? trim($host) : ''; if (self::validate_site_name($host)) { return $host; } return 'Site'; } /** @param mixed $actions */ private static function validate_entry_actions(mixed $actions): bool { if (!is_array($actions) || !TheDotDevAgenticBridge_Compat::array_is_list($actions) || $actions === []) { return false; } $seen = []; foreach ($actions as $action) { if (!is_array($action)) { return false; } foreach (array_keys($action) as $key) { if (!in_array((string) $key, self::ALLOWED_ACTION_KEYS, true)) { return false; } } $id_raw = $action['id'] ?? null; if (is_string($id_raw)) { $key = sanitize_key($id_raw); } elseif (is_int($id_raw)) { $key = sanitize_key((string) $id_raw); } else { return false; } if (!in_array($key, TheDotDevAgenticBridge_Content_Metadata::ALLOWED_ACTION_IDS, true)) { return false; } if (isset($seen[$key])) { return false; } $seen[$key] = true; $mode_raw = $action['mode'] ?? null; $executable_raw = $action['executable'] ?? null; if (!is_string($mode_raw) || $mode_raw !== 'metadata_only' || $executable_raw !== false) { return false; } } return true; } /** @param mixed $claims */ private static function validate_blocked_claims_list(mixed $claims): bool { if (!is_array($claims) || !TheDotDevAgenticBridge_Compat::array_is_list($claims)) { return false; } $seen = []; $custom_count = 0; foreach ($claims as $claim) { if (!is_string($claim)) { return false; } if (isset($seen[$claim])) { return false; } $seen[$claim] = true; if (in_array($claim, TheDotDevAgenticBridge_Content_Metadata::BLOCKED_CLAIM_IDS, true)) { continue; } if (trim($claim) === '') { return false; } ++$custom_count; if ($custom_count > TheDotDevAgenticBridge_Content_Metadata::MAX_CUSTOM_BLOCKED) { return false; } if (self::utf8_length($claim) > self::MAX_CUSTOM_CLAIM_CHARS) { return false; } if (TheDotDevAgenticBridge_Content_Metadata::contains_dangerous_text_public($claim)) { return false; } } return true; } /** @param mixed $relationship */ private static function validate_relationship(mixed $relationship): bool { if (!is_array($relationship) || !self::is_json_object_shape($relationship)) { return false; } foreach (array_keys($relationship) as $key) { if (!in_array((string) $key, self::ALLOWED_RELATIONSHIP_KEYS, true)) { return false; } } $type_raw = $relationship['type'] ?? null; if (!is_string($type_raw) || !in_array($type_raw, self::RELATIONSHIP_TYPES, true)) { return false; } $label_raw = $relationship['label'] ?? null; if (!is_string($label_raw)) { return false; } $label = trim($label_raw); if ($label === '') { return false; } if (self::utf8_length($label) > self::MAX_RELATIONSHIP_LABEL_CHARS) { return false; } return !TheDotDevAgenticBridge_Content_Metadata::contains_dangerous_text_public($label); } private static function is_valid_iso_datetime(string $value): bool { if ($value === '') { return false; } if (preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?(Z|[+-]\d{2}:\d{2})$/', $value) !== 1) { return false; } $normalized = preg_replace('/\.\d+/', '', $value) ?? $value; $normalized = preg_replace('/(Z|[+-]\d{2}:\d{2})$/', '', $normalized) ?? $normalized; $parsed = DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s', $normalized); if ($parsed === false) { return false; } return $parsed->format('Y-m-d\TH:i:s') === $normalized; } private static function is_strict_positive_int(mixed $value): bool { return is_int($value) && $value > 0; } private static function is_valid_entry_url(string $url): bool { $url = trim($url); if ($url === '' || str_starts_with($url, '//')) { return false; } $parts = wp_parse_url($url); if (!is_array($parts) || empty($parts['host'])) { return false; } if (isset($parts['user']) || isset($parts['pass']) || isset($parts['fragment'])) { return false; } $scheme = strtolower((string) ($parts['scheme'] ?? '')); if (!in_array($scheme, ['http', 'https'], true)) { return false; } $home = wp_parse_url(home_url('/')); if (!is_array($home) || empty($home['host'])) { return false; } $home_scheme = strtolower((string) ($home['scheme'] ?? '')); $home_host = strtolower((string) $home['host']); $home_port = isset($home['port']) ? (int) $home['port'] : ($home_scheme === 'https' ? 443 : 80); $url_port = isset($parts['port']) ? (int) $parts['port'] : ($scheme === 'https' ? 443 : 80); return strtolower((string) $parts['host']) === $home_host && $scheme === $home_scheme && $url_port === $home_port; } /** @param array $decoded */ private static function is_json_object_shape(array $decoded): bool { return $decoded === [] || !TheDotDevAgenticBridge_Compat::array_is_list($decoded); } /** @param mixed $raw @return list */ public static function normalize_content_ids(mixed $raw): array { if (!is_array($raw)) { return []; } $ids = []; foreach ($raw as $id) { if (is_int($id) && $id > 0) { $ids[] = $id; } } return array_values(array_unique($ids)); } /** @param array $map @return list */ public static function entry_ids_from_map(array $map): array { $ids = []; foreach (is_array($map['entries'] ?? null) ? $map['entries'] : [] as $entry) { if (!is_array($entry)) { continue; } $id = $entry['contentId'] ?? null; if (self::is_strict_positive_int($id)) { $ids[] = $id; } } return array_values(array_unique($ids)); } public static function utf8_length(string $text): int { if (function_exists('mb_strlen')) { return (int) mb_strlen($text, 'UTF-8'); } return strlen($text); } }