From fb0a126b21784c4528b82d32f4bfbfd802e4ba2c Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 21 Jun 2026 11:33:44 -0400 Subject: [PATCH] Add generic asset materialization rows --- .../docs/contracts/result-envelope.md | 2 + .../src/ArtifactCompiler/ArtifactCompiler.php | 58 ++++++++++++------- .../StaticSite/MaterializationPlanBuilder.php | 23 +++++--- php-transformer/tests/contract/run.php | 17 ++++++ .../parity/compiled-site-contract.json | 13 +++++ .../parity/website-artifact-bundle.json | 8 +++ 6 files changed, 92 insertions(+), 29 deletions(-) diff --git a/php-transformer/docs/contracts/result-envelope.md b/php-transformer/docs/contracts/result-envelope.md index f10222a..a8725b4 100644 --- a/php-transformer/docs/contracts/result-envelope.md +++ b/php-transformer/docs/contracts/result-envelope.md @@ -36,6 +36,8 @@ Required top-level keys for successful transformations: Artifact compiler callers that need a compiled site view should read `source_reports.compiled_site`. It exposes a generic `blocks-engine/php-transformer/compiled-site/v1` report with normalized `pages`, per-page `metadata`, full page `block_markup`, `assets`, `template_parts`, `visual_repair`, and `theme` buckets for stylesheets, scripts, fonts, images, template parts, and generated block types. Product adapters should map from this report plus the top-level `documents` and `assets` arrays instead of depending on product-specific artifact semantics. +Artifact compiler callers that write compiled output should read `source_reports.materialization_plan`. Its `assets` list is product-neutral write intent: each row may include `source`, `path`, `target_path`, `kind`, `role`, `intent`, `media_type`, `mime_type`, `bytes`, `binary`, `content_encoding`, `content`, `content_base64`, and `hash`. `target_path` defaults to the normalized artifact-relative `path`; downstream materializers own any product-specific routing, upload, or rewrite policy. Text rows expose `content` with `content_encoding: "text"`; binary rows expose `content_base64` with `content_encoding: "base64"` when the payload is available. Unsafe SVG text is withheld from write rows and reported through diagnostics. + Transformers may expose `source_reports.conversion_report` as a generic `blocks-engine/php-transformer/conversion-report/v1` projection over the same result envelope. It summarizes fallback diagnostics, source paths/selectors, artifact-local asset references, navigation candidates, presentation-gap signals, and metrics without applying product rewrite, import, routing, or visual-parity policy. `components` contains inspectable component candidates discovered before materialization. `block_types` contains generated block type artifacts promoted from `block.json` roots. `legacy_mapping` is transitional metadata for consumer-owned migration mappers and is not a long-term package feature commitment. diff --git a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php index 9be1269..16a9fc4 100644 --- a/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php +++ b/php-transformer/src/ArtifactCompiler/ArtifactCompiler.php @@ -735,15 +735,22 @@ private function compiledSiteAssets(array $assets): array return array_values(array_map( static fn (array $asset): array => array_filter( array( - 'path' => $asset['path'] ?? '', - 'kind' => $asset['kind'] ?? '', - 'role' => $asset['role'] ?? '', - 'intent' => $asset['intent'] ?? '', - 'mime_type' => $asset['mime_type'] ?? '', - 'bytes' => $asset['bytes'] ?? 0, - 'binary' => $asset['binary'] ?? false, + 'source' => $asset['source'] ?? '', + 'path' => $asset['path'] ?? '', + 'target_path' => $asset['target_path'] ?? $asset['path'] ?? '', + 'kind' => $asset['kind'] ?? '', + 'role' => $asset['role'] ?? '', + 'intent' => $asset['intent'] ?? '', + 'media_type' => $asset['media_type'] ?? $asset['mime_type'] ?? '', + 'mime_type' => $asset['mime_type'] ?? '', + 'bytes' => $asset['bytes'] ?? 0, + 'binary' => $asset['binary'] ?? false, + 'content_encoding' => $asset['content_encoding'] ?? $asset['encoding'] ?? '', + 'content' => $asset['content'] ?? null, + 'content_base64' => $asset['content_base64'] ?? null, + 'hash' => $asset['hash'] ?? $asset['provenance']['hash'] ?? '', ), - static fn (mixed $value): bool => '' !== $value + static fn (mixed $value): bool => null !== $value && '' !== $value ), $assets )); @@ -966,23 +973,24 @@ private function assetManifest(array $files, string $entryPath): array continue; } $asset = array( - 'path' => $file['path'], - 'kind' => $file['kind'], - 'bytes' => $file['bytes'], - 'source' => $file['source'] ?? 'artifact', - 'mime_type' => $file['mime_type'], - 'role' => $file['role'], - 'encoding' => $file['encoding'], - 'binary' => $file['binary'], - 'provenance' => $file['provenance'], + 'source' => $file['source'] ?? 'artifact', + 'path' => $file['path'], + 'target_path' => $file['path'], + 'kind' => $file['kind'], + 'bytes' => $file['bytes'], + 'media_type' => $file['mime_type'], + 'mime_type' => $file['mime_type'], + 'role' => $file['role'], + 'encoding' => $file['encoding'], + 'content_encoding' => $file['encoding'], + 'binary' => $file['binary'], + 'hash' => $file['provenance']['hash'] ?? '', + 'provenance' => $file['provenance'], ); if ( ! empty($file['content_base64']) ) { $asset['content_base64'] = $file['content_base64']; } - if ( 'css' === ($file['kind'] ?? '') && empty($file['binary']) ) { - $asset['content'] = $file['content']; - } - if ( 'image/svg+xml' === ($file['mime_type'] ?? '') && empty($file['binary']) && $this->isSafeSvgContent((string) ($file['content'] ?? '')) ) { + if ( empty($file['binary']) && ! $this->isUnsafeSvgAsset($file) ) { $asset['content'] = $file['content']; } if ( ! empty($file['intent']) ) { @@ -1024,6 +1032,14 @@ private function isSafeImageAsset(array $asset): bool return ! empty($asset['content']) && $this->isSafeSvgContent((string) $asset['content']); } + /** + * @param array $file + */ + private function isUnsafeSvgAsset(array $file): bool + { + return 'image/svg+xml' === ($file['mime_type'] ?? '') && ! $this->isSafeSvgContent((string) ($file['content'] ?? '')); + } + private function isSafeSvgContent(string $content): bool { if ( '' === trim($content) ) { diff --git a/php-transformer/src/StaticSite/MaterializationPlanBuilder.php b/php-transformer/src/StaticSite/MaterializationPlanBuilder.php index a060bd8..d8d33b0 100644 --- a/php-transformer/src/StaticSite/MaterializationPlanBuilder.php +++ b/php-transformer/src/StaticSite/MaterializationPlanBuilder.php @@ -158,14 +158,21 @@ private function assets(array $assets): array continue; } $planned[] = array_filter(array( - 'path' => (string) ($asset['path'] ?? ''), - 'kind' => (string) ($asset['kind'] ?? ''), - 'role' => (string) ($asset['role'] ?? ''), - 'intent' => (string) ($asset['intent'] ?? ''), - 'mime_type' => (string) ($asset['mime_type'] ?? ''), - 'bytes' => (int) ($asset['bytes'] ?? 0), - 'binary' => ! empty($asset['binary']), - ), static fn (mixed $value): bool => '' !== $value && 0 !== $value && false !== $value); + 'source' => (string) ($asset['source'] ?? ''), + 'path' => (string) ($asset['path'] ?? ''), + 'target_path' => (string) ($asset['target_path'] ?? $asset['path'] ?? ''), + 'kind' => (string) ($asset['kind'] ?? ''), + 'role' => (string) ($asset['role'] ?? ''), + 'intent' => (string) ($asset['intent'] ?? ''), + 'media_type' => (string) ($asset['media_type'] ?? $asset['mime_type'] ?? ''), + 'mime_type' => (string) ($asset['mime_type'] ?? ''), + 'bytes' => (int) ($asset['bytes'] ?? 0), + 'binary' => ! empty($asset['binary']), + 'content_encoding' => (string) ($asset['content_encoding'] ?? $asset['encoding'] ?? ''), + 'content' => $asset['content'] ?? null, + 'content_base64' => $asset['content_base64'] ?? null, + 'hash' => (string) ($asset['hash'] ?? $asset['provenance']['hash'] ?? ''), + ), static fn (mixed $value): bool => null !== $value && '' !== $value && 0 !== $value && false !== $value); } return $planned; } diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php index b632baa..aee7383 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -153,6 +153,23 @@ function serialize_blocks(array $blocks): string $assert('wp_template_part' === ($staticPlan['template_part_writes'][0]['type'] ?? ''), 'template part writes identify the WordPress write target'); $assert(str_contains((string) ($staticPlan['visual_repair_css'] ?? ''), 'min-height:100vh'), 'materialization plan exposes visual repair CSS'); $assert(! empty(array_filter($staticPlan['asset_rewrite_candidates'] ?? array(), static fn (array $candidate): bool => 'template_part' === ($candidate['scope'] ?? '') && 'assets/logo.png' === ($candidate['asset_path'] ?? ''))), 'materialization plan exposes template part asset rewrite candidates'); +$logoAssetPlanRow = null; +$cssAssetPlanRow = null; +foreach ( $staticPlan['assets'] ?? array() as $assetPlanRow ) { + if ( 'assets/logo.png' === ($assetPlanRow['path'] ?? '') ) { + $logoAssetPlanRow = $assetPlanRow; + } + if ( 'visual-repair.css' === ($assetPlanRow['path'] ?? '') ) { + $cssAssetPlanRow = $assetPlanRow; + } +} +$assert('assets/logo.png' === ($logoAssetPlanRow['target_path'] ?? ''), 'materialization plan asset rows expose generic target paths'); +$assert('base64' === ($logoAssetPlanRow['content_encoding'] ?? ''), 'materialization plan asset rows expose binary content encoding'); +$assert(base64_encode("\x89PNG\r\n\x1a\n") === ($logoAssetPlanRow['content_base64'] ?? ''), 'materialization plan asset rows expose base64 payloads for binary assets'); +$assert('image/png' === ($logoAssetPlanRow['media_type'] ?? ''), 'materialization plan asset rows expose generic media types'); +$assert(! empty($logoAssetPlanRow['hash'] ?? ''), 'materialization plan asset rows expose stable payload hashes'); +$assert('text' === ($cssAssetPlanRow['content_encoding'] ?? ''), 'materialization plan asset rows expose text content encoding'); +$assert('.wp-site-blocks{min-height:100vh}' === ($cssAssetPlanRow['content'] ?? ''), 'materialization plan asset rows expose text payloads for writable assets'); $productsPlan = ( new MaterializationPlanBuilder() )->fromCompiledSite( array( diff --git a/php-transformer/tests/fixtures/parity/compiled-site-contract.json b/php-transformer/tests/fixtures/parity/compiled-site-contract.json index 6d2d30d..801f4c8 100644 --- a/php-transformer/tests/fixtures/parity/compiled-site-contract.json +++ b/php-transformer/tests/fixtures/parity/compiled-site-contract.json @@ -73,6 +73,9 @@ { "path": "source_reports.compiled_site.pages.2.metadata.template", "assert": "equals", "value": "page-guide" }, { "path": "source_reports.compiled_site.pages.2.block_markup", "assert": "contains", "value": "