From 7b4aa04a3510a753a9562195db2314f2ae50df28 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 24 Jun 2026 16:59:08 -0400 Subject: [PATCH 1/3] Add runtime parity repair diagnostics --- .../src/ArtifactCompiler/ArtifactCompiler.php | 2 +- .../RuntimeDependencyParityReport.php | 23 ++++++++++++++----- php-transformer/tests/contract/run.php | 7 ++++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php index 60d80b4..f1ae567 100644 --- a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php +++ b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php @@ -74,7 +74,7 @@ public function compile(array $artifact): TransformerResult ); $sourceReports['compiled_site'] = $this->compiledSiteReport($normalized, $entryPath, $documents['documents'], $assets, $blockTypes, $serializedBlocks); $sourceReports['materialization_plan'] = ( new MaterializationPlanBuilder() )->fromCompiledSite($sourceReports['compiled_site']); - $sourceReports['runtime_dependency_parity'] = ( new RuntimeDependencyParityReport() )->fromArtifact($normalized['files'], $html, $serializedBlocks); + $sourceReports['runtime_dependency_parity'] = ( new RuntimeDependencyParityReport() )->fromArtifact($normalized['files'], $html, $serializedBlocks, $entryPath); $provenance = array( array( 'source_format' => 'artifact', diff --git a/php-transformer/src/ArtifactCompiler/RuntimeDependencyParityReport.php b/php-transformer/src/ArtifactCompiler/RuntimeDependencyParityReport.php index cbfa521..bab304d 100644 --- a/php-transformer/src/ArtifactCompiler/RuntimeDependencyParityReport.php +++ b/php-transformer/src/ArtifactCompiler/RuntimeDependencyParityReport.php @@ -14,9 +14,9 @@ final class RuntimeDependencyParityReport * @param array> $files * @return array */ - public function fromArtifact(array $files, string $sourceHtml, string $generatedHtml): array + public function fromArtifact(array $files, string $sourceHtml, string $generatedHtml, string $sourcePath = ''): array { - $sourceTargets = $this->sourceTargets($sourceHtml); + $sourceTargets = $this->sourceTargets($sourceHtml, $sourcePath); $generatedTargets = $this->htmlTargets($generatedHtml); $dependencies = array(); $findings = array(); @@ -39,9 +39,12 @@ public function fromArtifact(array $files, string $sourceHtml, string $generated $exists = $this->targetExists($dependency, $generatedTargets); $canvasApi = true === $dependency['canvas_api'] && 'canvas' === ($target['tag'] ?? ''); $dependencyRow = array_filter(array( + 'source_path' => $target['source_path'] ?? $sourcePath, 'script_path' => $scriptPath, 'script_kind' => $scriptKind, 'selector' => $selector, + 'target_id' => $target['id'] ?? '', + 'target_class' => $target['class'] ?? '', 'target_kind' => $target['tag'] ?? '', 'dependency_kind' => $dependency['kind'], 'events' => $dependency['events'], @@ -56,16 +59,24 @@ public function fromArtifact(array $files, string $sourceHtml, string $generated } $severity = 'telemetry' === $scriptKind ? 'info' : 'warning'; + $repairBucket = $canvasApi ? 'runtime_canvas_target_preservation' : 'runtime_dom_target_preservation'; $findings[] = array_filter(array( 'code' => 'runtime_dependency_target_missing', 'severity' => $severity, + 'source_path' => $target['source_path'] ?? $sourcePath, 'script_path' => $scriptPath, 'script_kind' => $scriptKind, 'selector' => $selector, + 'target_id' => $target['id'] ?? '', + 'target_class' => $target['class'] ?? '', 'target_kind' => $target['tag'] ?? '', 'dependency_kind' => $dependency['kind'], 'events' => $dependency['events'], 'canvas_api' => $canvasApi, + 'repair_bucket' => $repairBucket, + 'suggested_primitive' => $canvasApi ? 'runtime_canvas' : 'runtime_dom_target', + 'actionability' => $canvasApi ? 'preserve_canvas_markup_with_matching_script_runtime_or_rebuild_canvas_behavior' : 'preserve_or_recreate_the_referenced_dom_target_for_script_runtime', + 'materialization_hint' => $canvasApi ? 'preserve_canvas_id_class_and_markup_for_runtime_mapping' : 'preserve_id_class_or_wrapper_markup_required_by_first_party_script', 'message' => sprintf('Script %s references %s, but the generated block markup does not expose that DOM target.', $scriptPath, $selector), ), static fn (mixed $value): bool => null !== $value && '' !== $value && array() !== $value); } @@ -90,9 +101,9 @@ private function isScriptFile(array $file): bool } /** - * @return array + * @return array */ - private function sourceTargets(string $html): array + private function sourceTargets(string $html, string $sourcePath): array { $targets = array(); $document = new DOMDocument(); @@ -111,11 +122,11 @@ private function sourceTargets(string $html): array $tag = strtolower($element->tagName); $id = trim($element->hasAttribute('id') ? $element->getAttribute('id') : ''); if ( '' !== $id ) { - $targets['#' . $id] = array('tag' => $tag); + $targets['#' . $id] = array('tag' => $tag, 'source_path' => $sourcePath, 'id' => $id); } foreach ( preg_split('/\s+/', trim($element->hasAttribute('class') ? $element->getAttribute('class') : '')) ?: array() as $class ) { if ( '' !== $class ) { - $targets['.' . $class] = array('tag' => $tag); + $targets['.' . $class] = array('tag' => $tag, 'source_path' => $sourcePath, 'class' => $class); } } } diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php index 76a25bd..e7185e2 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -612,10 +612,17 @@ function serialize_blocks(array $blocks): string $assert('blocks-engine/php-transformer/runtime-dependency-parity/v1' === ($runtimeDependencyReport['schema'] ?? ''), 'runtime dependency parity report exposes schema'); $assert($runtimeDependencyReport === $runtimeDependencyConversionReport, 'conversion report projects runtime dependency parity'); $assert('runtime_dependency_target_missing' === ($canvasFinding['code'] ?? ''), 'runtime dependency parity reports missing canvas DOM target'); +$assert('index.html' === ($canvasFinding['source_path'] ?? ''), 'runtime dependency parity reports source path for missing canvas DOM target'); +$assert('canvas' === ($canvasFinding['target_id'] ?? ''), 'runtime dependency parity reports missing canvas target id'); $assert('canvas' === ($canvasFinding['target_kind'] ?? ''), 'runtime dependency parity identifies canvas source target kind'); $assert(true === ($canvasFinding['canvas_api'] ?? null), 'runtime dependency parity flags canvas 2d API usage'); $assert('warning' === ($canvasFinding['severity'] ?? ''), 'first-party missing runtime dependency target is warning severity'); +$assert('runtime_canvas_target_preservation' === ($canvasFinding['repair_bucket'] ?? ''), 'runtime dependency parity reports repair bucket for missing canvas DOM target'); +$assert('runtime_canvas' === ($canvasFinding['suggested_primitive'] ?? ''), 'runtime dependency parity reports suggested primitive for missing canvas DOM target'); +$assert(isset($canvasFinding['actionability']) && '' !== $canvasFinding['actionability'], 'runtime dependency parity reports actionability for missing canvas DOM target'); +$assert(isset($canvasFinding['materialization_hint']) && '' !== $canvasFinding['materialization_hint'], 'runtime dependency parity reports materialization hint for missing canvas DOM target'); $assert(null !== $statusDependency, 'runtime dependency parity records preserved status container dependency'); +$assert('index.html' === ($statusDependency['source_path'] ?? ''), 'runtime dependency parity records source path for preserved DOM dependency'); $assert(true === ($statusDependency['generated_present'] ?? null), 'runtime dependency parity passes preserved div id target'); $assert(! empty($statusDependency['events'] ?? array()), 'runtime dependency parity records simple addEventListener usage'); $assert('info' === ($rumFinding['severity'] ?? ''), 'telemetry-like runtime dependency misses are info severity'); From f03bda053263388c86ec0e7620b1a28df14becdc Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 24 Jun 2026 19:38:00 -0400 Subject: [PATCH 2/3] Preserve empty runtime control buttons --- php-transformer/src/HtmlToBlocks/HtmlTransformer.php | 2 +- php-transformer/tests/contract/run.php | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php index ce7f455..611a7f0 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php +++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php @@ -1117,7 +1117,7 @@ private function convertElement(DOMElement $element, array &$fallbacks, bool $ca if ( 'button' === $tagName ) { if ( $this->isNonContentRuntimeControl($element) ) { - return null; + return $this->createBlock('core/html', array( 'content' => $this->outerHtml($element) ), array(), $element); } return $this->buttonsPattern->matchButton( diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php index e7185e2..170326a 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -458,6 +458,12 @@ function serialize_blocks(array $blocks): string $assert(in_array('carousel', $interactionKinds, true), 'HTML source report detects carousel-ish interactions'); $assert('#panel' === ($interactions['source_reports']['interaction_candidates'][0]['target'] ?? ''), 'control interaction candidate exposes aria-controls target'); +$emptyRuntimeControl = ( new HtmlTransformer() )->transform( + '
' +)->toArray(); +$assert(str_contains((string) ($emptyRuntimeControl['serialized_blocks'] ?? ''), 'nav-toggle'), 'empty runtime control button class is preserved for scripts'); +$assert(str_contains((string) ($emptyRuntimeControl['serialized_blocks'] ?? ''), 'aria-expanded="false"'), 'empty runtime control button ARIA state is preserved'); + $assetMetadataOptions = array( 'context' => array( 'asset_metadata' => array( From ac71cde140b4d903dba9342f10d96d76cb9b6056 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 24 Jun 2026 19:50:29 -0400 Subject: [PATCH 3/3] Update runtime control parity fixtures --- .../html-address-and-toggle-controls.json | 11 +++++++---- .../html-nav-list-signal-classification.json | 18 ++++++++++-------- .../parity/html-nav-menu-flat-list.json | 12 +++++++----- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/php-transformer/tests/fixtures/parity/html-address-and-toggle-controls.json b/php-transformer/tests/fixtures/parity/html-address-and-toggle-controls.json index 75c3f4f..88a6177 100644 --- a/php-transformer/tests/fixtures/parity/html-address-and-toggle-controls.json +++ b/php-transformer/tests/fixtures/parity/html-address-and-toggle-controls.json @@ -1,7 +1,7 @@ { "schema": "blocks-engine/php-transformer/parity-fixture/v1", "name": "html-address-and-toggle-controls", - "description": "Preserves semantic address content and omits empty runtime-only toggle controls from static block output.", + "description": "Preserves semantic address content and keeps empty runtime-only toggle controls available for scripts.", "source_reference": { "repo": "php-transformer", "path": "tests/fixtures/parity/html-address-and-toggle-controls.json", @@ -16,8 +16,10 @@ "content": "
" }, "expected_blocks": [ - { "path": "blocks.0", "name": "core/navigation" }, - { "path": "blocks.0.innerBlocks.0", "name": "core/navigation-link", "attrs": { "label": "About", "url": "/about" } }, + { "path": "blocks.0", "name": "core/group" }, + { "path": "blocks.0.innerBlocks.0", "name": "core/html" }, + { "path": "blocks.0.innerBlocks.1", "name": "core/navigation" }, + { "path": "blocks.0.innerBlocks.1.innerBlocks.0", "name": "core/navigation-link", "attrs": { "label": "About", "url": "/about" } }, { "path": "blocks.1", "name": "core/paragraph", "attrs": { "className": "footer-address", "content": "48 Elm Street
hello@example.com" } } ], "expected_fallbacks": [], @@ -28,7 +30,8 @@ { "path": "source_reports.interaction_candidates.0.target", "assert": "equals", "value": "#site-menu" }, { "path": "serialized_blocks", "assert": "contains", "value": "footer-address" }, { "path": "serialized_blocks", "assert": "contains", "value": "hello@example.com" }, - { "path": "serialized_blocks", "assert": "not_contains", "value": "Open navigation menu" }, + { "path": "serialized_blocks", "assert": "contains", "value": "Open navigation menu" }, + { "path": "serialized_blocks", "assert": "contains", "value": "aria-controls=\"site-menu\"" }, { "path": "serialized_blocks", "assert": "not_contains", "value": "wp:button" }, { "path": "coverage.0.fallback_count", "assert": "equals", "value": 0 } ] diff --git a/php-transformer/tests/fixtures/parity/html-nav-list-signal-classification.json b/php-transformer/tests/fixtures/parity/html-nav-list-signal-classification.json index 7a285a5..80702d3 100644 --- a/php-transformer/tests/fixtures/parity/html-nav-list-signal-classification.json +++ b/php-transformer/tests/fixtures/parity/html-nav-list-signal-classification.json @@ -19,23 +19,25 @@ { "path": "blocks.0", "name": "core/group", "attrs": { "className": "site-header" } }, { "path": "blocks.0.innerBlocks.0", "name": "core/group", "attrs": { "className": "nav-inner" } }, { "path": "blocks.0.innerBlocks.0.innerBlocks.0", "name": "core/paragraph", "attrs": { "className": "nav-logo" } }, - { "path": "blocks.0.innerBlocks.0.innerBlocks.1", "name": "core/navigation", "attrs": { "className": "nav-links" } }, - { "path": "blocks.0.innerBlocks.0.innerBlocks.1.innerBlocks.0", "name": "core/navigation-link", "attrs": { "label": "Curriculum", "url": "/curriculum", "kind": "custom" } }, - { "path": "blocks.0.innerBlocks.0.innerBlocks.1.innerBlocks.1", "name": "core/navigation-link", "attrs": { "label": "Pricing", "url": "/pricing", "kind": "custom" } }, - { "path": "blocks.0.innerBlocks.0.innerBlocks.1.innerBlocks.2", "name": "core/navigation-link", "attrs": { "label": "Enroll Now", "url": "/enroll", "kind": "custom" } } + { "path": "blocks.0.innerBlocks.0.innerBlocks.1", "name": "core/html" }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.2", "name": "core/navigation", "attrs": { "className": "nav-links" } }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.2.innerBlocks.0", "name": "core/navigation-link", "attrs": { "label": "Curriculum", "url": "/curriculum", "kind": "custom" } }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.2.innerBlocks.1", "name": "core/navigation-link", "attrs": { "label": "Pricing", "url": "/pricing", "kind": "custom" } }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.2.innerBlocks.2", "name": "core/navigation-link", "attrs": { "label": "Enroll Now", "url": "/enroll", "kind": "custom" } } ], "expected_fallbacks": [], "expect": [ { "path": "status", "assert": "equals", "value": "success" }, { "path": "blocks", "assert": "count", "count": 1 }, - { "path": "blocks.0.innerBlocks.0.innerBlocks", "assert": "count", "count": 2 }, - { "path": "blocks.0.innerBlocks.0.innerBlocks.1.innerBlocks", "assert": "count", "count": 3 }, + { "path": "blocks.0.innerBlocks.0.innerBlocks", "assert": "count", "count": 3 }, + { "path": "blocks.0.innerBlocks.0.innerBlocks.2.innerBlocks", "assert": "count", "count": 3 }, { "path": "fallbacks", "assert": "count", "count": 0 }, { "path": "serialized_blocks", "assert": "contains", "value": "