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
10 changes: 7 additions & 3 deletions php-transformer/docs/contracts/result-envelope.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ Required top-level keys for successful transformations:
"status": "success",
"components": [],
"block_types": [],
"source_reports": {},
"legacy_mapping": {},
"source_reports": {
"conversion_report": {
"schema": "blocks-engine/php-transformer/conversion-report/v1",
"source_format": "html"
}
},
"blocks": [],
"serialized_blocks": "",
"documents": [],
Expand Down Expand Up @@ -42,7 +46,7 @@ The materialization plan also includes product-neutral `routes`, `navigation_lin

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. For artifact compiles, `source_summary` mirrors canonical materialization counts such as `page_count`, `asset_count`, `route_count`, `navigation_link_count`, and `menu_count` so wrappers can validate report consistency without re-deriving write plans from product-specific assumptions.

`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.
`components` contains inspectable component candidates discovered before materialization. `block_types` contains generated block type artifacts promoted from `block.json` roots. Canonical result envelopes must not expose `legacy_mapping`; consumer-owned migration mappers should derive compatibility data at their own boundary.

`blocks` is always a list-shaped array of parsed WordPress block arrays when exposed through `TransformerResult` or `FormatBridge::toBlocks()`. `fallbacks` entries should include stable generic metadata such as `type`, `reason`, `diagnostic_code`, `source_format`, and the preserved source fragment needed by callers to inspect or replay unsupported content.

Expand Down
42 changes: 41 additions & 1 deletion php-transformer/src/Contract/TransformerResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public function toArray(): array
*/
public static function assertCanonicalEnvelope(array $result, bool $requireMaterializationPlan = false): void
{
foreach ( array( 'schema', 'status', 'source_reports', 'blocks', 'serialized_blocks', 'documents', 'assets', 'diagnostics', 'fallbacks', 'provenance', 'coverage', 'context', 'metrics' ) as $key ) {
foreach ( array( 'schema', 'status', 'components', 'block_types', 'source_reports', 'blocks', 'serialized_blocks', 'documents', 'assets', 'diagnostics', 'fallbacks', 'provenance', 'coverage', 'context', 'metrics' ) as $key ) {
if ( ! array_key_exists($key, $result) ) {
throw new InvalidArgumentException(sprintf('Canonical transformer result is missing "%s".', $key));
}
Expand All @@ -90,11 +90,31 @@ public static function assertCanonicalEnvelope(array $result, bool $requireMater
throw new InvalidArgumentException('Canonical transformer result must not expose compatibility-only legacy_mapping.');
}

foreach ( array( 'conversion_report', 'materialization_plan' ) as $key ) {
if ( array_key_exists($key, $result) ) {
throw new InvalidArgumentException(sprintf('Canonical transformer result must expose %s only under source_reports.', $key));
}
}

if ( ! in_array($result['status'], array( 'success', 'success_with_warnings', 'failed' ), true) ) {
throw new InvalidArgumentException('Canonical transformer result has an unsupported status.');
}

foreach ( array( 'components', 'block_types', 'blocks', 'documents', 'assets', 'diagnostics', 'fallbacks', 'provenance', 'coverage', 'context', 'metrics' ) as $key ) {
if ( ! is_array($result[$key]) ) {
throw new InvalidArgumentException(sprintf('Canonical transformer result %s must be an array.', $key));
}
}

if ( ! is_array($result['source_reports']) ) {
throw new InvalidArgumentException('Canonical transformer result source_reports must be an array.');
}

$sourceReports = $result['source_reports'];
if ( array_key_exists('legacy_mapping', $sourceReports) ) {
throw new InvalidArgumentException('Canonical transformer result source_reports must not expose compatibility-only legacy_mapping.');
}

$conversionReport = $sourceReports['conversion_report'] ?? null;
if ( ! is_array($conversionReport) ) {
throw new InvalidArgumentException('Canonical transformer result is missing source_reports.conversion_report.');
Expand All @@ -104,12 +124,32 @@ public static function assertCanonicalEnvelope(array $result, bool $requireMater
throw new InvalidArgumentException('Canonical transformer result has an unsupported conversion report schema.');
}

if ( ! is_string($conversionReport['source_format'] ?? null) || '' === $conversionReport['source_format'] ) {
throw new InvalidArgumentException('Canonical transformer result conversion report is missing source_format.');
}

foreach ( array( 'source_summary', 'selector_summary', 'fallback_diagnostics', 'asset_refs', 'navigation_candidates', 'presentation_gaps', 'metrics' ) as $key ) {
if ( array_key_exists($key, $conversionReport) && ! is_array($conversionReport[$key]) ) {
throw new InvalidArgumentException(sprintf('Canonical transformer result conversion report %s must be an array.', $key));
}
}

$artifactLike = isset($sourceReports['artifact']) || isset($sourceReports['compiled_site']) || 'artifact' === ($conversionReport['source_format'] ?? null);
if ( $requireMaterializationPlan || $artifactLike ) {
$materializationPlan = $sourceReports['materialization_plan'] ?? null;
if ( ! is_array($materializationPlan) ) {
throw new InvalidArgumentException('Canonical artifact result is missing source_reports.materialization_plan.');
}

if ( 'blocks-engine/php-transformer/materialization-plan/v1' !== ($materializationPlan['schema'] ?? null) ) {
throw new InvalidArgumentException('Canonical artifact result has an unsupported materialization plan schema.');
}

foreach ( array( 'pages', 'routes', 'navigation_links', 'menus', 'template_parts', 'template_part_writes', 'assets', 'theme', 'asset_rewrite_candidates', 'rewrite_candidates', 'totals' ) as $key ) {
if ( ! array_key_exists($key, $materializationPlan) || ! is_array($materializationPlan[$key]) ) {
throw new InvalidArgumentException(sprintf('Canonical artifact result materialization plan %s must be an array.', $key));
}
}
}
}
}
12 changes: 7 additions & 5 deletions php-transformer/src/StaticSite/MaterializationPlanBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ final class MaterializationPlanBuilder
public function fromResult(TransformerResult|array $result): array
{
$data = $result instanceof TransformerResult ? $result->toArray() : $result;
TransformerResult::assertCanonicalEnvelope($data);

$materializationPlan = $data['source_reports']['materialization_plan'] ?? array();
if ( is_array($materializationPlan) && array() !== $materializationPlan ) {
return $materializationPlan;
}

$compiledSite = $data['source_reports']['compiled_site'] ?? array();
return is_array($compiledSite) ? $this->fromCompiledSite($compiledSite) : $this->emptyPlan();
}
Expand Down Expand Up @@ -50,7 +57,6 @@ public function fromCompiledSite(array $compiledSite): array
'visual_repair_css' => (string) ($visualRepair['css'] ?? ''),
'asset_rewrite_candidates' => $assetRewriteCandidates,
'rewrite_candidates' => $assetRewriteCandidates,
'products' => is_array($compiledSite['products'] ?? null) ? $compiledSite['products'] : array(),
'totals' => array(
'pages' => count($pages),
'routes' => count($routes),
Expand All @@ -61,10 +67,6 @@ public function fromCompiledSite(array $compiledSite): array
),
);

if ( array() === $plan['products'] ) {
unset($plan['products']);
}

return $plan;
}

Expand Down
25 changes: 23 additions & 2 deletions php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ function serialize_blocks(array $blocks): string
}
$assert(! array_key_exists('legacy_mapping', $result), 'canonical result omits compatibility-only legacy mapping');
$assertInvalidCanonicalEnvelope(array_merge($result, array('legacy_mapping' => array())), 'legacy_mapping', 'canonical validation rejects legacy mapping aliases');
$assertInvalidCanonicalEnvelope(array_merge($result, array('conversion_report' => $result['source_reports']['conversion_report'])), 'only under source_reports', 'canonical validation rejects top-level conversion report aliases');
$assertInvalidCanonicalEnvelope(array_merge($result, array('materialization_plan' => array())), 'only under source_reports', 'canonical validation rejects top-level materialization plan aliases');

$invalidStatus = $result;
$invalidStatus['status'] = 'ok';
$assertInvalidCanonicalEnvelope($invalidStatus, 'unsupported status', 'canonical validation rejects unsupported status values');

$invalidConversionReport = $result;
$invalidConversionReport['source_reports']['conversion_report']['source_format'] = '';
$assertInvalidCanonicalEnvelope($invalidConversionReport, 'source_format', 'canonical validation rejects conversion reports without a source format');

$missingConversionReport = $result;
unset($missingConversionReport['source_reports']['conversion_report']);
Expand Down Expand Up @@ -159,6 +169,17 @@ function serialize_blocks(array $blocks): string
unset($missingMaterializationPlan['source_reports']['materialization_plan']);
$assertInvalidCanonicalEnvelope($missingMaterializationPlan, 'source_reports.materialization_plan', 'canonical validation rejects artifact results without materialization plans');

$invalidMaterializationPlan = $simple;
$invalidMaterializationPlan['source_reports']['materialization_plan']['schema'] = 'legacy/materialization-plan/v1';
$assertInvalidCanonicalEnvelope($invalidMaterializationPlan, 'materialization plan schema', 'canonical validation rejects materialization plans with unsupported schemas');

$incompleteMaterializationPlan = $simple;
unset($incompleteMaterializationPlan['source_reports']['materialization_plan']['routes']);
$assertInvalidCanonicalEnvelope($incompleteMaterializationPlan, 'materialization plan routes', 'canonical validation rejects incomplete materialization plans');

$rebuiltPlan = ( new MaterializationPlanBuilder() )->fromResult($simple);
$assert($simple['source_reports']['materialization_plan'] === $rebuiltPlan, 'materialization plan builder preserves canonical plans from result envelopes');

$formatResult = ( new FormatBridge() )->convertResult('# Format report', 'markdown', 'blocks')->toArray();
TransformerResult::assertCanonicalEnvelope($formatResult);
$assert('blocks-engine/php-transformer/conversion-report/v1' === ($formatResult['source_reports']['conversion_report']['schema'] ?? ''), 'format bridge exposes canonical conversion report');
Expand Down Expand Up @@ -214,14 +235,14 @@ function serialize_blocks(array $blocks): string
$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(
$neutralPlan = ( new MaterializationPlanBuilder() )->fromCompiledSite(
array(
'products' => array(
array('sku' => 'shirt-001', 'name' => 'Shirt'),
),
)
);
$assert('shirt-001' === ($productsPlan['products'][0]['sku'] ?? ''), 'materialization plan preserves compiled site products manifest when present');
$assert(! array_key_exists('products', $neutralPlan), 'materialization plan omits product-specific manifest buckets');

$fragment = $compiler->compileFragment('<main><h2>Fragment</h2><p>Copy</p></main>', 'fixture:fragment')->toArray();
$assert('success' === $fragment['status'], 'fragment compiles successfully', (string) $fragment['status']);
Expand Down
Loading