diff --git a/php-transformer/src/Contract/ConversionReportProjection.php b/php-transformer/src/Contract/ConversionReportProjection.php index 75b8e9e..1f1f00e 100644 --- a/php-transformer/src/Contract/ConversionReportProjection.php +++ b/php-transformer/src/Contract/ConversionReportProjection.php @@ -118,6 +118,12 @@ private static function fallbackDiagnostics(array $fallbacks): array 'type' => $fallback['type'] ?? '', 'reason' => $fallback['reason'] ?? '', 'diagnostic_code' => $fallback['diagnostic_code'] ?? '', + 'severity' => $fallback['severity'] ?? '', + 'runtime_requirement' => $fallback['runtime_requirement'] ?? '', + 'recoverability' => $fallback['recoverability'] ?? '', + 'actionability' => $fallback['actionability'] ?? '', + 'suggested_primitive' => $fallback['suggested_primitive'] ?? '', + 'materialization_hint' => $fallback['materialization_hint'] ?? '', 'source_format' => $fallback['source_format'] ?? '', 'source' => $fallback['source'] ?? '', 'scope' => $fallback['scope'] ?? '', diff --git a/php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php b/php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php new file mode 100644 index 0000000..45036b1 --- /dev/null +++ b/php-transformer/src/HtmlToBlocks/FallbackDiagnostic.php @@ -0,0 +1,85 @@ + $fields + * @param array $provenance + * @return array + */ + public static function build(array $fields, array $provenance = array()): array + { + return array_merge(self::defaults($fields), $fields, $provenance); + } + + /** + * @param array $fields + * @return array + */ + private static function defaults(array $fields): array + { + $code = (string) ($fields['diagnostic_code'] ?? ''); + + return match ( $code ) { + 'html_form_fallback' => array( + 'severity' => 'warning', + 'runtime_requirement' => 'server_or_client_form_handler', + 'recoverability' => 'recoverable_with_runtime_mapping', + 'actionability' => 'map_form_action_controls_and_submission_handler', + 'suggested_primitive' => 'form', + 'materialization_hint' => 'preserve_form_markup_or_replace_with_form_block_integration', + ), + 'html_script_fallback' => array( + 'severity' => 'warning', + 'runtime_requirement' => 'client_script_execution', + 'recoverability' => 'recoverable_with_script_enqueue_or_component_runtime', + 'actionability' => 'review_script_source_and_enqueue_or_rebuild_behavior', + 'suggested_primitive' => 'script_asset', + 'materialization_hint' => 'enqueue_script_or_rebuild_as_interactive_block', + ), + 'html_inline_svg_fallback' => array( + 'severity' => 'info', + 'runtime_requirement' => 'none', + 'recoverability' => 'recoverable_as_static_markup_or_image_asset', + 'actionability' => 'review_sanitized_svg_and_materialize_as_image_or_html', + 'suggested_primitive' => 'image_or_html', + 'materialization_hint' => 'materialize_safe_svg_as_image_asset_or_core_html', + ), + 'html_unsafe_inline_svg' => array( + 'severity' => 'warning', + 'runtime_requirement' => 'sanitization_review', + 'recoverability' => 'recoverable_after_security_review', + 'actionability' => 'remove_scriptable_svg_content_or_replace_with_safe_asset', + 'suggested_primitive' => 'image_asset', + 'materialization_hint' => 'sanitize_svg_before_materializing_asset', + ), + 'html_iframe_embed_fallback' => array( + 'severity' => 'warning', + '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', + 'suggested_primitive' => 'embed', + 'materialization_hint' => 'convert_supported_src_to_core_embed_or_preserve_sanitized_iframe_html', + ), + 'html_unsupported_element' => array( + 'severity' => 'info', + 'runtime_requirement' => 'unknown', + 'recoverability' => 'recoverable_with_manual_mapping', + 'actionability' => 'map_element_to_supported_block_or_preserve_html', + 'suggested_primitive' => 'core/html', + 'materialization_hint' => 'preserve_sanitized_markup_until_a_specific_block_mapping_exists', + ), + default => array( + 'severity' => 'warning', + 'runtime_requirement' => 'unknown', + 'recoverability' => 'unknown', + 'actionability' => 'review_fallback_metadata', + 'suggested_primitive' => 'core/html', + 'materialization_hint' => 'preserve_fallback_metadata_for_manual_review', + ), + }; + } +} diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index 5883b01..76ec83e 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -84,7 +84,7 @@ public function transform(string $html, array $options = array()): TransformerRe ), ); $fallbacks = array( - array_merge(array( + FallbackDiagnostic::build(array( 'type' => 'html', 'reason' => 'parse_failed', 'diagnostic_code' => 'html_parse_failed', @@ -440,7 +440,7 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca if ( 'form' === $tagName ) { $controls = $this->formControls($element); $boundedHtml = $this->boundedFallbackHtml($this->safeFallbackHtml($element)); - $fallbacks[] = array_merge(array( + $fallbacks[] = FallbackDiagnostic::build(array( 'type' => 'html', 'reason' => 'form_requires_runtime', 'diagnostic_code' => 'html_form_fallback', @@ -533,7 +533,7 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca $fallback['control'] = $control; } - $fallbacks[] = array_merge($fallback, $this->fallbackProvenance); + $fallbacks[] = FallbackDiagnostic::build($fallback, $this->fallbackProvenance); } return null; @@ -1194,7 +1194,7 @@ private function captureInlineSvgFallback(DOMElement $element, array &$fallbacks $safe = $this->isSafeSvgContent($rawHtml); $boundedHtml = $this->boundedFallbackHtml($this->safeFallbackHtml($element)); - $fallbacks[] = array_merge(array( + $fallbacks[] = FallbackDiagnostic::build(array( 'type' => 'inline_svg', 'reason' => $safe ? 'inline_svg_fallback' : 'unsafe_inline_svg', 'diagnostic_code' => $safe ? 'html_inline_svg_fallback' : 'html_unsafe_inline_svg', @@ -1220,7 +1220,7 @@ private function captureScriptFallback(DOMElement $element, array &$fallbacks): { $boundedHtml = $this->boundedFallbackHtml($this->safeFallbackHtml($element)); $boundedBody = $this->boundedFallbackText(trim($element->textContent ?? '')); - $fallbacks[] = array_merge(array( + $fallbacks[] = FallbackDiagnostic::build(array( 'type' => 'html', 'reason' => 'script_requires_runtime', 'diagnostic_code' => 'html_script_fallback', @@ -1850,7 +1850,7 @@ private function convertIframeElement(DOMElement $iframe, array &$fallbacks): ?a } $boundedHtml = $this->boundedFallbackHtml($this->safeFallbackHtml($iframe)); - $fallbacks[] = array_merge(array( + $fallbacks[] = FallbackDiagnostic::build(array( 'type' => 'html', 'reason' => 'iframe_embed_fallback', 'diagnostic_code' => 'html_iframe_embed_fallback', diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php index ba9e98c..4946030 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -48,6 +48,16 @@ function serialize_blocks(array $blocks): string exit(1); }; +$assertNormalizedFallbackDiagnostic = static function (array $diagnostic, string $code, string $severity, string $runtimeRequirement, string $suggestedPrimitive) use ($assert): void { + $assert($code === ($diagnostic['diagnostic_code'] ?? ''), "conversion report exposes {$code} diagnostic code"); + $assert($severity === ($diagnostic['severity'] ?? ''), "conversion report exposes {$code} severity"); + $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"); + $assert($suggestedPrimitive === ($diagnostic['suggested_primitive'] ?? ''), "conversion report exposes {$code} suggested primitive"); + $assert(isset($diagnostic['materialization_hint']) && '' !== $diagnostic['materialization_hint'], "conversion report exposes {$code} materialization hint"); +}; + $assertInvalidCanonicalEnvelope = static function (array $result, string $expectedMessage, string $message, bool $requireMaterializationPlan = false) use ($assert): void { try { TransformerResult::assertCanonicalEnvelope($result, $requireMaterializationPlan); @@ -107,7 +117,7 @@ function serialize_blocks(array $blocks): string )->toArray(); $formDiagnostic = $formFallback['source_reports']['conversion_report']['fallback_diagnostics'][0] ?? array(); $assert(array() === ($formFallback['blocks'] ?? array()), 'form fallback does not synthesize canonical blocks'); -$assert('html_form_fallback' === ($formDiagnostic['diagnostic_code'] ?? ''), 'conversion report exposes form fallback diagnostic code'); +$assertNormalizedFallbackDiagnostic($formDiagnostic, 'html_form_fallback', 'warning', 'server_or_client_form_handler', 'form'); $assert('/contact' === ($formDiagnostic['form']['action'] ?? ''), 'conversion report exposes form action metadata'); $assert('post' === ($formDiagnostic['form']['method'] ?? ''), 'conversion report exposes normalized form method metadata'); $assert(3 === ($formDiagnostic['control_count'] ?? null), 'conversion report exposes form control count'); @@ -117,6 +127,20 @@ function serialize_blocks(array $blocks): string $assert('support' === ($formDiagnostic['controls'][1]['options'][0]['value'] ?? ''), 'conversion report exposes select option values'); $assert(is_int($formDiagnostic['html_bytes'] ?? null), 'conversion report exposes bounded fallback HTML byte size'); +$normalizedFallbacks = ( new HtmlTransformer() )->transform( + '
' +)->toArray(); +$normalizedDiagnostics = $normalizedFallbacks['source_reports']['conversion_report']['fallback_diagnostics'] ?? array(); +$diagnosticsByCode = array(); +foreach ( $normalizedDiagnostics as $diagnostic ) { + $diagnosticsByCode[$diagnostic['diagnostic_code'] ?? ''] = $diagnostic; +} +$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_inline_svg_fallback'] ?? array(), 'html_inline_svg_fallback', 'info', 'none', 'image_or_html'); +$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_unsupported_element'] ?? array(), 'html_unsupported_element', 'info', 'unknown', 'core/html'); +$assertNormalizedFallbackDiagnostic($diagnosticsByCode['html_iframe_embed_fallback'] ?? array(), 'html_iframe_embed_fallback', 'warning', 'third_party_embed_runtime', 'embed'); + $assetMetadataOptions = array( 'context' => array( 'asset_metadata' => array(