diff --git a/php-transformer/docs/contracts/result-envelope.md b/php-transformer/docs/contracts/result-envelope.md index 2a11763..ef19381 100644 --- a/php-transformer/docs/contracts/result-envelope.md +++ b/php-transformer/docs/contracts/result-envelope.md @@ -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": [], @@ -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. diff --git a/php-transformer/src/Contract/TransformerResult.php b/php-transformer/src/Contract/TransformerResult.php index 7ee6cee..12dd087 100644 --- a/php-transformer/src/Contract/TransformerResult.php +++ b/php-transformer/src/Contract/TransformerResult.php @@ -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)); } @@ -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.'); @@ -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)); + } + } } } } diff --git a/php-transformer/src/StaticSite/MaterializationPlanBuilder.php b/php-transformer/src/StaticSite/MaterializationPlanBuilder.php index ab24c3b..702bc35 100644 --- a/php-transformer/src/StaticSite/MaterializationPlanBuilder.php +++ b/php-transformer/src/StaticSite/MaterializationPlanBuilder.php @@ -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(); } @@ -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), @@ -61,10 +67,6 @@ public function fromCompiledSite(array $compiledSite): array ), ); - if ( array() === $plan['products'] ) { - unset($plan['products']); - } - return $plan; } diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php index 1f5cf17..3371a38 100644 --- a/php-transformer/tests/contract/run.php +++ b/php-transformer/tests/contract/run.php @@ -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']); @@ -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'); @@ -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('

Fragment

Copy

', 'fixture:fragment')->toArray(); $assert('success' === $fragment['status'], 'fragment compiles successfully', (string) $fragment['status']);