diff --git a/php-transformer/composer.json b/php-transformer/composer.json index 81b78aa16..b9117a266 100644 --- a/php-transformer/composer.json +++ b/php-transformer/composer.json @@ -159,6 +159,7 @@ "php tests/unit/corpus-detectors.php", "php tests/unit/live-wp-parity-runner.php", "php tests/unit/deterministic-row-deduplicator.php", + "php tests/unit/conversion-report-projection-input-reuse.php", "php tests/unit/html-transformer-shared-analysis-cache.php", "php tests/unit/html-transformer-session-state.php", "php tests/unit/reusable-component-recognition.php", diff --git a/php-transformer/src/Contract/ConversionReportProjection.php b/php-transformer/src/Contract/ConversionReportProjection.php index 10c93d821..d3c91546e 100644 --- a/php-transformer/src/Contract/ConversionReportProjection.php +++ b/php-transformer/src/Contract/ConversionReportProjection.php @@ -21,6 +21,10 @@ final class ConversionReportProjection */ public static function fromResultParts(string $sourceFormat, array $blocks, array $fallbacks, array $sourceReports, array $assets, array $provenance, array $metrics): array { + $fallbackDiagnostics = self::fallbackDiagnostics($fallbacks); + $runtimeIslands = self::runtimeIslands($sourceReports); + $runtimeIslandSummaryEntries = self::runtimeIslandSummaryEntries($runtimeIslands); + $report = array( 'schema' => self::SCHEMA, 'finding_schema' => ConversionFindingContract::SCHEMA, @@ -28,9 +32,9 @@ public static function fromResultParts(string $sourceFormat, array $blocks, arra 'source' => self::firstString($provenance, 'source'), '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), + 'selector_summary' => self::selectorSummary($sourceReports, $fallbackDiagnostics, $runtimeIslandSummaryEntries), + 'conversion_classification_summary' => self::conversionClassificationSummary($sourceReports, $fallbackDiagnostics, $runtimeIslandSummaryEntries), + 'fallback_diagnostics' => $fallbackDiagnostics, 'core_html_fallback_evidence' => self::coreHtmlFallbackEvidence($sourceReports), 'asset_refs' => self::assetReferences($blocks, $sourceReports), 'navigation_candidates' => self::navigationCandidates($blocks, $sourceReports), @@ -38,7 +42,7 @@ public static function fromResultParts(string $sourceFormat, array $blocks, arra 'editability_report' => is_array($sourceReports['editability_report'] ?? null) ? $sourceReports['editability_report'] : array(), 'editability_policy' => is_array($sourceReports['editability_policy'] ?? null) ? $sourceReports['editability_policy'] : array(), 'runtime_dependency_parity' => self::runtimeDependencyParity($sourceReports), - 'runtime_islands' => self::runtimeIslands($sourceReports), + 'runtime_islands' => $runtimeIslands, 'interaction_candidates' => self::interactionCandidates($sourceReports), 'presentation_gaps' => self::presentationGaps($sourceReports), 'native_target_blocks' => self::stringList($sourceReports, 'native_target_blocks'), @@ -89,10 +93,11 @@ private static function sourceSummary(string $sourceFormat, array $blocks, array /** * @param array $sourceReports - * @param array> $fallbacks + * @param array> $fallbackDiagnostics + * @param array> $runtimeIslandSummaryEntries * @return array */ - private static function selectorSummary(array $sourceReports, array $fallbacks): array + private static function selectorSummary(array $sourceReports, array $fallbackDiagnostics, array $runtimeIslandSummaryEntries): array { $selectors = array(); $sources = array(); @@ -102,12 +107,12 @@ private static function selectorSummary(array $sourceReports, array $fallbacks): self::appendSourcePath($sources, $entry); } - foreach ( self::fallbackDiagnostics($fallbacks) as $entry ) { + foreach ( $fallbackDiagnostics as $entry ) { self::appendSelector($selectors, $entry, 'fallback'); self::appendSourcePath($sources, $entry); } - foreach ( self::runtimeIslandSummaryEntries($sourceReports) as $entry ) { + foreach ( $runtimeIslandSummaryEntries as $entry ) { self::appendSelector($selectors, $entry, 'runtime_island'); self::appendSourcePath($sources, $entry); } @@ -196,15 +201,16 @@ private static function fallbackDiagnostics(array $fallbacks): array /** * @param array $sourceReports - * @param array> $fallbacks + * @param array> $fallbackDiagnostics + * @param array> $runtimeIslandSummaryEntries * @return array */ - private static function conversionClassificationSummary(array $sourceReports, array $fallbacks): array + private static function conversionClassificationSummary(array $sourceReports, array $fallbackDiagnostics, array $runtimeIslandSummaryEntries): array { $byClassification = array(); $byStrategy = array(); - foreach ( array_merge(self::sourceProvenance($sourceReports), self::fallbackDiagnostics($fallbacks), self::runtimeIslandSummaryEntries($sourceReports)) as $entry ) { + foreach ( array_merge(self::sourceProvenance($sourceReports), $fallbackDiagnostics, $runtimeIslandSummaryEntries) as $entry ) { if ( ! is_array($entry) ) { continue; } @@ -370,13 +376,13 @@ private static function runtimeIslands(array $sourceReports): array } /** - * @param array $sourceReports + * @param array> $runtimeIslands * @return array> */ - private static function runtimeIslandSummaryEntries(array $sourceReports): array + private static function runtimeIslandSummaryEntries(array $runtimeIslands): array { $entries = array(); - foreach ( self::runtimeIslands($sourceReports) as $island ) { + foreach ( $runtimeIslands as $island ) { $entries[] = array_filter( array( 'selector' => $island['selector'] ?? '', diff --git a/php-transformer/tests/unit/conversion-report-projection-input-reuse.php b/php-transformer/tests/unit/conversion-report-projection-input-reuse.php new file mode 100644 index 000000000..0058488b6 --- /dev/null +++ b/php-transformer/tests/unit/conversion-report-projection-input-reuse.php @@ -0,0 +1,59 @@ + 'html_script_fallback', 'selector' => '#widget', 'source_path' => 'index.html', 'conversion_classification' => 'runtime_gap', 'preservation_strategy' => 'core_html'), + array('diagnostic_code' => 'html_unsafe_inline_svg', 'selector' => '.logo svg', 'source_path' => 'about.html', 'conversion_classification' => 'asset_gap', 'preservation_strategy' => 'materialize_asset'), +); +$sharedIsland = array('selector' => '#widget', 'source_path' => 'index.html', 'tag' => 'div', 'preservation_strategy' => 'scoped_runtime_metadata'); +$secondIsland = array('selector' => '.map', 'source_path' => 'about.html', 'tag' => 'section', 'disposition' => 'preserved'); +$report = ConversionReportProjection::fromResultParts('html', array(), $fallbacks, array( + 'html' => array( + 'source_provenance' => array(array('selector' => 'main', 'source_path' => 'index.html', 'conversion_classification' => 'native', 'preservation_strategy' => 'native_block')), + 'runtime_islands' => array($sharedIsland, $secondIsland), + ), + 'runtime_islands' => array($sharedIsland), +), array(), array(), array()); + +$assert( + array($sharedIsland, $secondIsland) === ($report['runtime_islands'] ?? null), + 'runtime islands retain first-occurrence ordering while overlapping declarations are deduplicated' +); +$assert( + array('main', '#widget', '.logo svg', '#widget', '.map') === array_column($report['selector_summary']['selectors'] ?? array(), 'selector'), + 'selector summary retains source, fallback, and deduplicated runtime-island order' +); +$assert( + array('index.html', 'about.html') === ($report['selector_summary']['source_paths'] ?? null), + 'selector summary source paths retain their first occurrence order' +); +$assert( + array('native' => 1, 'runtime_gap' => 1, 'asset_gap' => 1, 'runtime_island_preserved' => 2) === ($report['conversion_classification_summary']['by_classification'] ?? null), + 'classification summary counts each normalized fallback and runtime island exactly once' +); +$assert( + array('native_block' => 1, 'core_html' => 1, 'materialize_asset' => 1, 'scoped_runtime_metadata' => 2) === ($report['conversion_classification_summary']['by_preservation_strategy'] ?? null), + 'preservation strategy summary preserves normalized row counts and omission behavior' +); +$assert( + array('html_script_fallback', 'html_unsafe_inline_svg') === array_column($report['fallback_diagnostics'] ?? array(), 'diagnostic_code'), + 'fallback diagnostics retain normalized final rows in input order' +); + +if ( 0 < $failures ) { + exit(1); +} + +echo "conversion-report-projection-input-reuse ok\n";