Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions php-transformer/docs/contracts/result-envelope.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
58 changes: 37 additions & 21 deletions php-transformer/src/ArtifactCompiler/ArtifactCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
));
Expand Down Expand Up @@ -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']) ) {
Expand Down Expand Up @@ -1024,6 +1032,14 @@ private function isSafeImageAsset(array $asset): bool
return ! empty($asset['content']) && $this->isSafeSvgContent((string) $asset['content']);
}

/**
* @param array<string, mixed> $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) ) {
Expand Down
23 changes: 15 additions & 8 deletions php-transformer/src/StaticSite/MaterializationPlanBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
17 changes: 17 additions & 0 deletions php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
13 changes: 13 additions & 0 deletions php-transformer/tests/fixtures/parity/compiled-site-contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<!-- wp:heading" },
{ "path": "source_reports.compiled_site.assets", "assert": "count", "count": 7 },
{ "path": "source_reports.compiled_site.assets.2.target_path", "assert": "equals", "value": "public/assets/site.css" },
{ "path": "source_reports.compiled_site.assets.2.content", "assert": "equals", "value": "main{max-width:60rem;margin:auto}" },
{ "path": "source_reports.compiled_site.assets.4.content", "assert": "contains", "value": "compiledSite" },
{ "path": "source_reports.compiled_site.theme.stylesheets.0", "assert": "equals", "value": "public/assets/site.css" },
{ "path": "source_reports.compiled_site.theme.scripts.0", "assert": "equals", "value": "public/assets/site.js" },
{ "path": "source_reports.compiled_site.theme.images.0", "assert": "equals", "value": "public/assets/hero.svg" },
Expand All @@ -81,6 +84,16 @@
{ "path": "source_reports.compiled_site.template_parts.0.block_markup", "assert": "contains", "value": "<header>" },
{ "path": "source_reports.compiled_site.visual_repair.stylesheets.0.path", "assert": "equals", "value": "public/assets/visual-repair.css" },
{ "path": "source_reports.compiled_site.visual_repair.css", "assert": "contains", "value": "min-height:100vh" },
{ "path": "source_reports.materialization_plan.assets.2.path", "assert": "equals", "value": "public/assets/site.css" },
{ "path": "source_reports.materialization_plan.assets.2.target_path", "assert": "equals", "value": "public/assets/site.css" },
{ "path": "source_reports.materialization_plan.assets.2.content_encoding", "assert": "equals", "value": "text" },
{ "path": "source_reports.materialization_plan.assets.2.media_type", "assert": "equals", "value": "text/css" },
{ "path": "source_reports.materialization_plan.assets.2.content", "assert": "equals", "value": "main{max-width:60rem;margin:auto}" },
{ "path": "source_reports.materialization_plan.assets.4.path", "assert": "equals", "value": "public/assets/site.js" },
{ "path": "source_reports.materialization_plan.assets.4.content", "assert": "contains", "value": "compiledSite" },
{ "path": "source_reports.materialization_plan.assets.5.path", "assert": "equals", "value": "public/assets/hero.svg" },
{ "path": "source_reports.materialization_plan.assets.5.media_type", "assert": "equals", "value": "image/svg+xml" },
{ "path": "source_reports.materialization_plan.assets.5.content", "assert": "contains", "value": "<svg" },
{ "path": "source_reports.conversion_report.presentation_gaps.0.type", "assert": "equals", "value": "presentation_stylesheet" },
{ "path": "source_reports.conversion_report.presentation_gaps.0.path", "assert": "equals", "value": "public/assets/visual-repair.css" },
{ "path": "source_reports.conversion_report.metrics.fallback_count", "assert": "equals", "value": 0 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
{ "path": "assets", "assert": "count", "count": 2 },
{ "path": "assets.0.path", "assert": "equals", "value": "style.css" },
{ "path": "assets.1.path", "assert": "equals", "value": "site.js" },
{ "path": "source_reports.materialization_plan.assets.0.target_path", "assert": "equals", "value": "style.css" },
{ "path": "source_reports.materialization_plan.assets.0.content", "assert": "contains", "value": ".hero" },
{ "path": "source_reports.materialization_plan.assets.0.content_encoding", "assert": "equals", "value": "text" },
{ "path": "source_reports.materialization_plan.assets.0.media_type", "assert": "equals", "value": "text/css" },
{ "path": "source_reports.materialization_plan.assets.0.hash", "assert": "equals", "value": "a93d51e57e2b77526b408acac657fcee165ee03caeb1de5cc194575d8940860f" },
{ "path": "source_reports.materialization_plan.assets.1.target_path", "assert": "equals", "value": "site.js" },
{ "path": "source_reports.materialization_plan.assets.1.content", "assert": "contains", "value": "website-artifact" },
{ "path": "source_reports.materialization_plan.assets.1.content_encoding", "assert": "equals", "value": "text" },
{ "path": "components.0.name", "assert": "equals", "value": "hero" },
{ "path": "serialized_blocks", "assert": "contains", "value": "Website Artifact Fixture" }
]
Expand Down
Loading