diff --git a/php-transformer/src/Contract/ConversionReportProjection.php b/php-transformer/src/Contract/ConversionReportProjection.php index 2c9d70fe..8bda3260 100644 --- a/php-transformer/src/Contract/ConversionReportProjection.php +++ b/php-transformer/src/Contract/ConversionReportProjection.php @@ -25,6 +25,7 @@ public static function fromResultParts(string $sourceFormat, array $blocks, arra 'scope' => self::firstString($provenance, 'scope'), 'source_summary' => self::sourceSummary($sourceFormat, $blocks, $fallbacks, $sourceReports, $assets, $metrics), 'selector_summary' => self::selectorSummary($sourceReports, $fallbacks), + 'conversion_classification_summary' => self::conversionClassificationSummary($sourceReports, $fallbacks), 'fallback_diagnostics' => self::fallbackDiagnostics($fallbacks), 'asset_refs' => self::assetReferences($blocks, $sourceReports), 'navigation_candidates' => self::navigationCandidates($blocks, $sourceReports), @@ -122,6 +123,8 @@ private static function fallbackDiagnostics(array $fallbacks): array 'reason' => $fallback['reason'] ?? '', 'diagnostic_code' => $fallback['diagnostic_code'] ?? '', 'severity' => $fallback['severity'] ?? '', + 'conversion_classification' => $fallback['conversion_classification'] ?? '', + 'preservation_strategy' => $fallback['preservation_strategy'] ?? '', 'runtime_requirement' => $fallback['runtime_requirement'] ?? '', 'recoverability' => $fallback['recoverability'] ?? '', 'actionability' => $fallback['actionability'] ?? '', @@ -152,6 +155,41 @@ private static function fallbackDiagnostics(array $fallbacks): array return $diagnostics; } + /** + * @param array $sourceReports + * @param array> $fallbacks + * @return array + */ + private static function conversionClassificationSummary(array $sourceReports, array $fallbacks): array + { + $byClassification = array(); + $byStrategy = array(); + + foreach ( array_merge(self::sourceProvenance($sourceReports), self::fallbackDiagnostics($fallbacks)) as $entry ) { + if ( ! is_array($entry) ) { + continue; + } + + $classification = (string) ($entry['conversion_classification'] ?? ''); + if ( '' !== $classification ) { + $byClassification[$classification] = ($byClassification[$classification] ?? 0) + 1; + } + + $strategy = (string) ($entry['preservation_strategy'] ?? ''); + if ( '' !== $strategy ) { + $byStrategy[$strategy] = ($byStrategy[$strategy] ?? 0) + 1; + } + } + + return array_filter( + array( + 'by_classification' => $byClassification, + 'by_preservation_strategy' => $byStrategy, + ), + static fn (mixed $value): bool => array() !== $value + ); + } + /** * @param array> $blocks * @param array $sourceReports @@ -383,6 +421,8 @@ private static function appendSelector(array &$selectors, array $entry, string $ 'block_name' => $entry['block_name'] ?? '', 'tag' => $entry['tag'] ?? ($entry['element'] ?? ''), 'attribute' => $entry['attribute'] ?? '', + 'conversion_classification' => $entry['conversion_classification'] ?? '', + 'preservation_strategy' => $entry['preservation_strategy'] ?? '', ), static fn (mixed $value): bool => '' !== $value ); diff --git a/php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php b/php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php index da84290c..da642311 100644 --- a/php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php +++ b/php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php @@ -26,6 +26,8 @@ private static function defaults(array $fields): array return match ( $code ) { 'html_form_fallback' => array( 'severity' => 'warning', + 'conversion_classification' => 'runtime_island_preserved', + 'preservation_strategy' => 'fallback_metadata_with_readable_blocks', 'runtime_requirement' => 'server_or_client_form_handler', 'recoverability' => 'recoverable_with_runtime_mapping', 'actionability' => 'map_form_action_controls_and_submission_handler', @@ -34,6 +36,8 @@ private static function defaults(array $fields): array ), 'html_script_fallback' => array( 'severity' => 'warning', + 'conversion_classification' => 'runtime_island_preserved', + 'preservation_strategy' => 'scoped_runtime_metadata', 'runtime_requirement' => 'client_script_execution', 'recoverability' => 'recoverable_with_script_enqueue_or_component_runtime', 'actionability' => 'review_script_source_and_enqueue_or_rebuild_behavior', @@ -42,6 +46,8 @@ private static function defaults(array $fields): array ), 'html_inline_svg_fallback' => array( 'severity' => 'info', + 'conversion_classification' => 'editable_approximation', + 'preservation_strategy' => 'sanitized_static_markup_or_image', 'runtime_requirement' => 'none', 'recoverability' => 'recoverable_as_static_markup_or_image_asset', 'actionability' => 'review_sanitized_svg_and_materialize_as_image_or_html', @@ -50,6 +56,8 @@ private static function defaults(array $fields): array ), 'html_unsafe_inline_svg' => array( 'severity' => 'warning', + 'conversion_classification' => 'unsupported_loss', + 'preservation_strategy' => 'diagnostic_only_until_security_review', 'runtime_requirement' => 'sanitization_review', 'recoverability' => 'recoverable_after_security_review', 'actionability' => 'remove_scriptable_svg_content_or_replace_with_safe_asset', @@ -58,6 +66,8 @@ private static function defaults(array $fields): array ), 'html_iframe_embed_fallback' => array( 'severity' => 'warning', + 'conversion_classification' => 'runtime_island_preserved', + 'preservation_strategy' => 'sanitized_embed_markup', 'runtime_requirement' => 'third_party_embed_runtime', 'recoverability' => 'recoverable_with_embed_provider_or_html_preservation', 'actionability' => 'map_iframe_src_to_supported_embed_provider_or_preserve_html', @@ -66,6 +76,8 @@ private static function defaults(array $fields): array ), 'html_canvas_runtime_fallback' => array( 'severity' => 'warning', + 'conversion_classification' => 'runtime_island_preserved', + 'preservation_strategy' => 'bounded_raw_html_runtime_island', 'runtime_requirement' => 'canvas_element_and_client_script_execution', 'recoverability' => 'recoverable_with_canvas_markup_preservation_or_rebuilt_interactive_block', 'actionability' => 'preserve_canvas_markup_with_matching_script_runtime_or_rebuild_canvas_behavior', @@ -74,6 +86,8 @@ private static function defaults(array $fields): array ), 'html_unsupported_element' => array( 'severity' => 'info', + 'conversion_classification' => 'unsupported_loss', + 'preservation_strategy' => 'diagnostic_only', 'runtime_requirement' => 'unknown', 'recoverability' => 'recoverable_with_manual_mapping', 'actionability' => 'map_element_to_supported_block_or_preserve_html', @@ -82,6 +96,8 @@ private static function defaults(array $fields): array ), default => array( 'severity' => 'warning', + 'conversion_classification' => 'unsupported_loss', + 'preservation_strategy' => 'diagnostic_only', 'runtime_requirement' => 'unknown', 'recoverability' => 'unknown', 'actionability' => 'review_fallback_metadata', diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index cdaf7ddb..78cf1552 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -216,6 +216,8 @@ public function transform(string $html, array $options = array()): TransformerRe 'source' => self::class, 'reason' => $fallback['reason'] ?? null, 'severity' => $fallback['severity'] ?? null, + 'conversion_classification' => $fallback['conversion_classification'] ?? null, + 'preservation_strategy' => $fallback['preservation_strategy'] ?? null, 'runtime_requirement' => $fallback['runtime_requirement'] ?? null, 'tag' => $fallback['tag'] ?? null, 'selector' => $fallback['selector'] ?? null, @@ -1496,13 +1498,47 @@ private function resolveSourceProvenancePaths(array &$blocks, string $path, arra */ private function sourceProvenanceEntry(string $blockName, DOMElement $element): array { - return array( + return array_merge(array( 'block_name' => $blockName, 'tag' => strtolower($element->tagName), 'selector' => $this->elementSelector($element), 'source_attributes' => $this->safeSourceAttributes($element), 'source_fragment' => $this->safeSourceFragment($element), 'context' => $this->sourceContext($element), + ), $this->sourceConversionMetadata($blockName, $element)); + } + + /** + * @return array{conversion_classification: string, preservation_strategy: string} + */ + private function sourceConversionMetadata(string $blockName, DOMElement $element): array + { + $tagName = strtolower($element->tagName); + + if ( 'core/html' === $blockName ) { + return array( + 'conversion_classification' => 'runtime_island_preserved', + 'preservation_strategy' => 'bounded_raw_html_runtime_island', + ); + } + + if ( $this->isRuntimeDomTarget($element) ) { + return array( + 'conversion_classification' => 'runtime_island_preserved', + 'preservation_strategy' => 'core_block_shell_with_runtime_target', + ); + } + + if ( in_array($tagName, array('form', 'input', 'select', 'textarea'), true) && 'core/search' !== $blockName ) { + return array( + 'conversion_classification' => 'editable_approximation', + 'preservation_strategy' => 'readable_static_block_approximation', + ); + } + + return array( + 'conversion_classification' => 'native_block_conversion', + 'preservation_strategy' => 'core_block', ); } diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php index 75ffc3c8..22634bbe 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -108,9 +108,13 @@ function serialize_blocks(array $blocks): string $assert('image' === ($imageReferenceReports['image_references'][5]['element'] ?? ''), 'image reference analysis reports SVG image href elements'); $assert('assets/vector.png' === ($imageReferenceReports['image_references'][5]['asset_path'] ?? ''), 'image reference analysis resolves SVG image href paths relative to the HTML document'); -$assertNormalizedFallbackDiagnostic = static function (array $diagnostic, string $code, string $severity, string $runtimeRequirement, string $suggestedPrimitive) use ($assert): void { +$assertNormalizedFallbackDiagnostic = static function (array $diagnostic, string $code, string $severity, string $runtimeRequirement, string $suggestedPrimitive, string $conversionClassification = '') use ($assert): void { $assert($code === ($diagnostic['diagnostic_code'] ?? ''), "conversion report exposes {$code} diagnostic code"); $assert($severity === ($diagnostic['severity'] ?? ''), "conversion report exposes {$code} severity"); + if ( '' !== $conversionClassification ) { + $assert($conversionClassification === ($diagnostic['conversion_classification'] ?? ''), "conversion report exposes {$code} conversion classification"); + $assert(isset($diagnostic['preservation_strategy']) && '' !== $diagnostic['preservation_strategy'], "conversion report exposes {$code} preservation strategy"); + } $assert($runtimeRequirement === ($diagnostic['runtime_requirement'] ?? ''), "conversion report exposes {$code} runtime requirement"); $assert(isset($diagnostic['recoverability']) && '' !== $diagnostic['recoverability'], "conversion report exposes {$code} recoverability"); $assert(isset($diagnostic['actionability']) && '' !== $diagnostic['actionability'], "conversion report exposes {$code} actionability"); @@ -560,7 +564,7 @@ public function match(DOMElement $element, PatternContext $context): ?array } $assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_unsafe_inline_svg'] ?? array(), 'html_unsafe_inline_svg', 'warning', 'sanitization_review', 'image_asset'); $assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_script_fallback'] ?? array(), 'html_script_fallback', 'warning', 'client_script_execution', 'script_asset'); -$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_canvas_runtime_fallback'] ?? array(), 'html_canvas_runtime_fallback', 'warning', 'canvas_element_and_client_script_execution', 'runtime_canvas'); +$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_canvas_runtime_fallback'] ?? array(), 'html_canvas_runtime_fallback', 'warning', 'canvas_element_and_client_script_execution', 'runtime_canvas', 'runtime_island_preserved'); $assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_iframe_embed_fallback'] ?? array(), 'html_iframe_embed_fallback', 'warning', 'third_party_embed_runtime', 'embed'); $assert(! isset($diagnosticsByCode['html_inline_svg_fallback']), 'safe inline SVGs convert to image blocks instead of fallback diagnostics'); @@ -568,7 +572,7 @@ public function match(DOMElement $element, PatternContext $context): ?array '
Fallback
' )->toArray(); $canvasDiagnostic = $canvasFallback['source_reports']['conversion_report']['fallback_diagnostics'][0] ?? array(); -$assertNormalizedFallbackDiagnostic($canvasDiagnostic, 'html_canvas_runtime_fallback', 'warning', 'canvas_element_and_client_script_execution', 'runtime_canvas'); +$assertNormalizedFallbackDiagnostic($canvasDiagnostic, 'html_canvas_runtime_fallback', 'warning', 'canvas_element_and_client_script_execution', 'runtime_canvas', 'runtime_island_preserved'); $assert('canvas_requires_runtime' === ($canvasDiagnostic['reason'] ?? ''), 'canvas fallback exposes runtime-specific reason'); $assert('bonsai' === ($canvasFallback['fallbacks'][0]['attributes']['id'] ?? ''), 'canvas fallback preserves id for runtime mapping'); $assert(str_contains((string) ($canvasFallback['fallbacks'][0]['html'] ?? ''), 'transform( + '
', + array( + 'runtime_canvas_selectors' => array('#stage'), + 'runtime_dom_selectors' => array('#amount'), + ) +)->toArray(); +$runtimeSelectors = $runtimePreserved['source_reports']['conversion_report']['selector_summary']['selectors'] ?? array(); +$runtimeClassifications = array(); +foreach ( $runtimeSelectors as $selector ) { + if ( 'block' === ($selector['kind'] ?? '') && 'core/html' === ($selector['block_name'] ?? '') ) { + $runtimeClassifications[$selector['tag'] ?? ''] = $selector['conversion_classification'] ?? ''; + } +} +$assert('runtime_island_preserved' === ($runtimeClassifications['canvas'] ?? ''), 'runtime-preserved canvas core/html block is classified as runtime island preservation'); +$assert('runtime_island_preserved' === ($runtimeClassifications['input'] ?? ''), 'runtime-preserved control core/html block is classified as runtime island preservation'); +$runtimeSummary = $runtimePreserved['source_reports']['conversion_report']['conversion_classification_summary']['by_classification'] ?? array(); +$assert(2 <= ($runtimeSummary['runtime_island_preserved'] ?? 0), 'conversion report summarizes runtime island preservation counts'); + +$unsupportedLoss = ( new HtmlTransformer() )->transform('
')->toArray(); +$unsupportedDiagnostic = $unsupportedLoss['source_reports']['conversion_report']['fallback_diagnostics'][0] ?? array(); +$assert('html_unsupported_element' === ($unsupportedDiagnostic['diagnostic_code'] ?? ''), 'unsupported element emits fallback diagnostic'); +$assert('unsupported_loss' === ($unsupportedDiagnostic['conversion_classification'] ?? ''), 'true unsupported fallback is classified as unsupported loss'); + $decorativeCanvas = ( new HtmlTransformer() )->transform( '

Stars

', array(