post_content; if ($intent === self::VISUAL_ONLY && class_exists('AIWEST_Visual_Layout_Engine')) { $protected_content = AIWEST_Visual_Layout_Engine::unwrap($protected_content); if (class_exists('AIWEST_Semantic_Layout_Composer')) { $protected_content = AIWEST_Semantic_Layout_Composer::decompose($protected_content); } } $heading_report = []; if (class_exists('AIWEST_Page_Title_Heading_Normalizer')) { $normalized = AIWEST_Page_Title_Heading_Normalizer::normalize($post_id, $protected_content, [ 'post_title' => (string)$post->post_title, 'post_type' => (string)$post->post_type, 'mode' => 'standard', ]); $protected_content = $normalized['content']; $heading_report = $normalized['report']; } $snapshot = self::snapshot_from_content($protected_content, [ 'post_id' => intval($post_id), 'post_title' => (string)$post->post_title, 'post_excerpt' => (string)$post->post_excerpt, 'post_status' => (string)$post->post_status, 'post_type' => (string)$post->post_type, 'post_name' => isset($post->post_name) ? (string)$post->post_name : '', 'action' => sanitize_key($action), 'instruction' => sanitize_textarea_field($instruction), 'intent' => $intent, 'duplicate_title_heading' => $heading_report, 'captured_at' => current_time('mysql'), ]); update_post_meta($post_id, '_aiwest_content_preservation_last_snapshot', $snapshot); return $snapshot; } public static function snapshot_from_content($content, $context = []) { $content = (string)$content; return array_merge($context, [ 'content' => $content, 'visible_text' => self::visible_text($content), 'word_count' => self::word_count($content), 'headings' => self::headings($content), 'links' => self::links($content), 'section_count' => self::section_count($content), 'critical_phrases' => self::critical_phrases($content), 'content_hash' => hash('sha256', $content), 'block_structure_hash' => hash('sha256', self::block_structure($content)), ]); } public static function validate($post_id, $snapshot, $candidate_html, $intent) { $post = get_post(intval($post_id)); $report = self::validate_snapshot($snapshot, $candidate_html, $intent, $post ? [ 'post_id' => intval($post_id), 'post_title' => (string)$post->post_title, 'post_name' => isset($post->post_name) ? (string)$post->post_name : '', ] : []); update_post_meta($post_id, '_aiwest_content_preservation_last_report', $report); if (!$report['passed']) { update_post_meta($post_id, '_aiwest_content_preservation_review_required', current_time('mysql')); if (class_exists('AIWEST_Logger')) { AIWEST_Logger::warning('Content Preservation Gate blocked an unsafe AI update.', [ 'post_id' => intval($post_id), 'intent' => $intent, 'reasons' => $report['reasons'], ]); } $message = $intent === self::VISUAL_ONLY ? __('The visual layout could not be applied safely. No rewrite was attempted, and the original content was preserved.', 'ai-webmaster-studio') : __('The AI update was stopped by the Content Preservation Gate. The original content was preserved.', 'ai-webmaster-studio'); return new WP_Error( 'aiwest_content_preservation_blocked', $message, $report ); } delete_post_meta($post_id, '_aiwest_content_preservation_review_required'); return $report; } public static function validate_snapshot($snapshot, $candidate_html, $intent, $context = []) { $candidate_html = (string)$candidate_html; $candidate = self::snapshot_from_content($candidate_html, $context); $original_word_count = intval($snapshot['word_count'] ?? 0); $original_words = max(1, $original_word_count); $word_ratio = $original_word_count === 0 ? 1.0 : ($candidate['word_count'] / $original_words); $strategic = self::is_strategic($snapshot + $context); $minimum_ratio = $strategic ? 0.97 : 0.95; $reasons = []; $missing_headings = self::missing_values($candidate['headings'], $snapshot['headings'] ?? []); $missing_links = self::missing_values($candidate['links'], $snapshot['links'] ?? []); if ($candidate_html === '') $reasons[] = 'empty_content'; if (!self::valid_html_structure($candidate_html)) $reasons[] = 'invalid_html_structure'; if (!self::valid_block_comments($candidate_html)) $reasons[] = 'invalid_wordpress_blocks'; if ($intent !== self::CONTENT_REWRITE) { if ($word_ratio < $minimum_ratio) $reasons[] = 'word_count_reduced'; if ($missing_headings) $reasons[] = 'headings_removed_or_changed'; if ($missing_links) $reasons[] = 'links_removed_or_changed'; if (!self::phrases_present($candidate['visible_text'], $snapshot['critical_phrases'] ?? [])) $reasons[] = 'critical_text_removed'; $original_sections = intval($snapshot['section_count'] ?? 0); if ($original_sections > 0 && $candidate['section_count'] < max(1, intval(ceil($original_sections * $minimum_ratio)))) $reasons[] = 'sections_reduced'; } if ($intent === self::VISUAL_ONLY && $candidate['visible_text'] !== (string)($snapshot['visible_text'] ?? '')) { $reasons[] = 'visible_text_changed'; } $semantic_identity = null; if ($intent === self::VISUAL_ONLY && class_exists('AIWEST_Semantic_Layout_Composer')) { $original_inventory = AIWEST_Semantic_Layout_Composer::inventory((string)($snapshot['content'] ?? '')); $candidate_inventory = AIWEST_Semantic_Layout_Composer::inventory($candidate_html); $semantic_identity = ($original_inventory === $candidate_inventory); if (!$semantic_identity) $reasons[] = 'semantic_content_identity_changed'; } return [ 'passed' => empty($reasons), 'intent' => $intent, 'strategic' => $strategic, 'minimum_word_ratio' => $minimum_ratio, 'word_ratio' => round($word_ratio, 4), 'original_words' => intval($snapshot['word_count'] ?? 0), 'candidate_words' => $candidate['word_count'], 'original_sections' => intval($snapshot['section_count'] ?? 0), 'candidate_sections' => $candidate['section_count'], 'missing_headings' => $missing_headings, 'missing_links' => $missing_links, 'original_block_structure_hash' => (string)($snapshot['block_structure_hash'] ?? ''), 'candidate_block_structure_hash' => $candidate['block_structure_hash'], 'semantic_content_identity_preserved' => $semantic_identity, 'reasons' => array_values(array_unique($reasons)), 'checked_at' => function_exists('current_time') ? current_time('mysql') : gmdate('Y-m-d H:i:s'), ]; } protected static function visible_text($html) { $html = preg_replace('//s', ' ', (string)$html); $html = preg_replace('/<(script|style|noscript)\b[^>]*>.*?<\/\1>/is', ' ', $html); $html = preg_replace('/<(?:\/?(?:address|article|aside|blockquote|div|details|figcaption|figure|footer|form|h[1-6]|header|hr|li|main|nav|ol|p|pre|section|summary|table|tbody|td|tfoot|th|thead|tr|ul)|br)\b[^>]*>/i', ' $0 ', $html); $text = html_entity_decode(wp_strip_all_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = preg_replace('/\s+/u', ' ', trim($text)); return self::lower($text); } protected static function word_count($html) { $text = self::visible_text($html); if ($text === '') return 0; preg_match_all('/[\p{L}\p{N}][\p{L}\p{N}\p{M}\x{2019}\'\-]*/u', $text, $matches); return count($matches[0]); } protected static function headings($html) { preg_match_all('/]*>(.*?)<\/h\1>/is', (string)$html, $matches, PREG_SET_ORDER); $headings = []; foreach ($matches as $match) { $headings[] = 'h' . $match[1] . ':' . self::visible_text($match[2]); } return $headings; } protected static function links($html) { preg_match_all('/]*\bhref\s*=\s*(["\'])(.*?)\1/is', (string)$html, $matches); return array_values(array_unique(array_map('html_entity_decode', $matches[2] ?? []))); } protected static function section_count($html) { preg_match_all('/<(?:section|article|h[1-3])\b/i', (string)$html, $matches); return count($matches[0]); } protected static function critical_phrases($html) { $text = self::visible_text($html); preg_match_all('/[^.!?\n]{35,}[.!?]/u', $text, $matches); $phrases = array_slice($matches[0] ?? [], 0, 24); $protected_terms = [ 'ai webmaster is a free wordpress plugin', 'aiwos cloud is a separate future platform', 'aiwos cloud is not a paid unlock of ai webmaster', 'administrator-supervised workflows', 'api keys', 'external services', 'security & privacy', 'approval center', 'rollback', 'monitoring', 'scheduled reports', 'limitation of liability', 'user responsibility', ]; foreach ($protected_terms as $term) { if (strpos($text, $term) !== false) $phrases[] = $term; } if (!$phrases && $text !== '') $phrases[] = self::substr($text, 0, 180); return array_values(array_unique(array_filter(array_map('trim', $phrases)))); } protected static function missing_values($candidate, $required) { $counts = array_count_values($candidate); $missing = []; foreach ($required as $item) { if (empty($counts[$item])) { $missing[] = $item; } else { $counts[$item]--; } } return $missing; } protected static function phrases_present($candidate_text, $phrases) { foreach ($phrases as $phrase) { if ($phrase !== '' && strpos($candidate_text, self::lower($phrase)) === false) return false; } return true; } protected static function valid_html_structure($html) { $stack = []; $void = ['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']; preg_match_all('/<\/?([a-z][a-z0-9:-]*)\b[^>]*>/i', (string)$html, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $tag = strtolower($match[1]); $token = $match[0]; if (in_array($tag, $void, true) || preg_match('/\/\s*>$/', $token)) continue; if (strpos($token, ']*?)?\s*(\/?)-->/i', (string)$html, $matches, PREG_SET_ORDER); $stack = []; foreach ($matches as $match) { if ($match[1] === '/') { if (!$stack || array_pop($stack) !== strtolower($match[2])) return false; } elseif ($match[3] !== '/') { $stack[] = strtolower($match[2]); } } return empty($stack); } protected static function is_strategic($context) { $haystack = self::lower(($context['post_title'] ?? '') . ' ' . ($context['post_name'] ?? '')); $terms = [ 'home','homepage','start','o nas','about','kontakt','contact','polityka','policy','privacy','regulamin','terms','security','bezpieczeń', 'aiwos cloud','ai webmaster','api keys','openai api','external services','changelog','roadmap','faq','support','documentation','dokumentacja','operator','approval center','ai webmaster vs aiwos', ]; foreach ($terms as $term) { if (strpos($haystack, $term) !== false) return true; } if (!empty($context['post_id']) && function_exists('get_option') && intval(get_option('wp_page_for_privacy_policy', 0)) === intval($context['post_id'])) return true; return false; } protected static function block_structure($html) { preg_match_all('/|<\/?(?:section|article|div|p|h[1-6]|ul|ol|li|table|thead|tbody|tr|th|td|a)\b[^>]*>/i', (string)$html, $matches); return implode('|', array_map(function($token) { return preg_replace('/\s+/u', ' ', trim(self::lower($token))); }, $matches[0] ?? [])); } protected static function lower($text) { return function_exists('mb_strtolower') ? mb_strtolower((string)$text, 'UTF-8') : strtolower((string)$text); } protected static function substr($text, $start, $length) { return function_exists('mb_substr') ? mb_substr($text, $start, $length) : substr($text, $start, $length); } }