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
13 changes: 0 additions & 13 deletions php-transformer/src/ArtifactCompiler/ArtifactCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,6 @@ public function compile(array $artifact): TransformerResult
components: $components,
blockTypes: $blockTypes,
sourceReports: $sourceReports,
legacyMapping: array(
'block-artifact-compiler/result/v1' => array(
'status' => 'status',
'input' => 'source_reports.artifact',
'wordpress_artifacts.block_markup' => 'serialized_blocks',
'wordpress_artifacts.blocks' => 'blocks',
'wordpress_artifacts.block_types' => 'block_types',
'wordpress_artifacts.components' => 'components',
'wordpress_artifacts.files' => 'assets',
'diagnostics' => 'diagnostics',
'provenance' => 'provenance',
),
),
blocks: $entryBlocks['blocks'],
serializedBlocks: $serializedBlocks,
documents: $documents['documents'],
Expand Down
55 changes: 51 additions & 4 deletions php-transformer/src/Contract/TransformerResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

namespace Automattic\BlocksEngine\PhpTransformer\Contract;

use InvalidArgumentException;

final class TransformerResult
{
public const SCHEMA = 'blocks-engine/php-transformer/result/v1';
Expand All @@ -11,7 +13,6 @@ final class TransformerResult
* @param array<int, array<string, mixed>> $components
* @param array<int, array<string, mixed>> $blockTypes
* @param array<string, mixed> $sourceReports
* @param array<string, mixed> $legacyMapping
* @param array<int, array<string, mixed>> $blocks
* @param array<int, array<string, mixed>> $documents
* @param array<int, array<string, mixed>> $assets
Expand All @@ -27,7 +28,6 @@ public function __construct(
public readonly array $components = array(),
public readonly array $blockTypes = array(),
public readonly array $sourceReports = array(),
public readonly array $legacyMapping = array(),
public readonly array $blocks = array(),
public readonly string $serializedBlocks = '',
public readonly array $documents = array(),
Expand All @@ -46,13 +46,12 @@ public function __construct(
*/
public function toArray(): array
{
return array(
$result = array(
'schema' => self::SCHEMA,
'status' => $this->status,
'components' => $this->components,
'block_types' => $this->blockTypes,
'source_reports' => $this->sourceReports,
'legacy_mapping' => $this->legacyMapping,
'blocks' => $this->blocks,
'serialized_blocks' => $this->serializedBlocks,
'documents' => $this->documents,
Expand All @@ -64,5 +63,53 @@ public function toArray(): array
'context' => $this->context,
'metrics' => $this->metrics,
);

self::assertCanonicalEnvelope($result);

return $result;
}

/**
* Validate the public result shape downstream wrappers should depend on.
*
* @param array<string, mixed> $result
*/
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 ) {
if ( ! array_key_exists($key, $result) ) {
throw new InvalidArgumentException(sprintf('Canonical transformer result is missing "%s".', $key));
}
}

if ( self::SCHEMA !== $result['schema'] ) {
throw new InvalidArgumentException('Canonical transformer result has an unsupported schema.');
}

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

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

$sourceReports = $result['source_reports'];
$conversionReport = $sourceReports['conversion_report'] ?? null;
if ( ! is_array($conversionReport) ) {
throw new InvalidArgumentException('Canonical transformer result is missing source_reports.conversion_report.');
}

if ( ConversionReportProjection::SCHEMA !== ($conversionReport['schema'] ?? null) ) {
throw new InvalidArgumentException('Canonical transformer result has an unsupported conversion report schema.');
}

$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.');
}
}
}
}
42 changes: 40 additions & 2 deletions php-transformer/src/FormatBridge/FormatBridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Automattic\BlocksEngine\PhpTransformer\FormatBridge;

use Automattic\BlocksEngine\PhpTransformer\Contract\ConversionReportProjection;
use Automattic\BlocksEngine\PhpTransformer\Contract\TransformationOptions;
use Automattic\BlocksEngine\PhpTransformer\Contract\TransformerResult;
use InvalidArgumentException;
Expand Down Expand Up @@ -130,8 +131,25 @@ public function convertResult(string $content, string $from, string $to, array $
$normalizedContent = $this->normalize($content, $from, $options);
$blocks = array_values($sourceAdapter->toBlocks($normalizedContent, $options));
$output = $from === $to ? $normalizedContent : $targetAdapter->fromBlocks($blocks, $options);
$metrics = array(
'input_bytes' => strlen($content),
'output_bytes' => strlen($output),
'block_count' => count($blocks),
'fallback_count' => 0,
'diagnostic_count' => 1,
);
$sourceReports = array(
'format_bridge' => array(
'source_format' => $from,
'target_format' => $to,
'input_bytes' => strlen($content),
'output_bytes' => strlen($output),
),
);
$sourceReports['conversion_report'] = ConversionReportProjection::fromResultParts($from, $blocks, array(), $sourceReports, array(), $provenance, $metrics);

return new TransformerResult(
sourceReports: $sourceReports,
blocks: $blocks,
serializedBlocks: 'blocks' === $to ? $output : '',
documents: array(
Expand All @@ -148,7 +166,8 @@ public function convertResult(string $content, string $from, string $to, array $
),
),
provenance: $provenance,
context: $context
context: $context,
metrics: $metrics
);
} catch ( InvalidArgumentException $exception ) {
return $this->failedResult('format_bridge_validation_failed', $exception->getMessage(), $provenance, $context);
Expand All @@ -163,8 +182,26 @@ public function convertResult(string $content, string $from, string $to, array $
*/
private function failedResult(string $code, string $message, array $provenance, array $context): TransformerResult
{
$metrics = array(
'input_bytes' => (int) ($provenance[0]['input_bytes'] ?? 0),
'block_count' => 0,
'fallback_count' => 0,
'diagnostic_count' => 1,
'output_bytes' => 0,
);
$sourceFormat = (string) ($provenance[0]['source_format'] ?? 'unknown');
$sourceReports = array(
'format_bridge' => array(
'source_format' => $sourceFormat,
'target_format' => (string) ($provenance[0]['target_format'] ?? ''),
'error_code' => $code,
),
);
$sourceReports['conversion_report'] = ConversionReportProjection::fromResultParts($sourceFormat, array(), array(), $sourceReports, array(), $provenance, $metrics);

return new TransformerResult(
status: 'failed',
sourceReports: $sourceReports,
diagnostics: array(
array(
'code' => $code,
Expand All @@ -173,7 +210,8 @@ private function failedResult(string $code, string $message, array $provenance,
),
),
provenance: $provenance,
context: $context
context: $context,
metrics: $metrics
);
}
}
33 changes: 31 additions & 2 deletions php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,34 @@ function serialize_blocks(array $blocks): string
exit(1);
};

$assertInvalidCanonicalEnvelope = static function (array $result, string $expectedMessage, string $message, bool $requireMaterializationPlan = false) use ($assert): void {
try {
TransformerResult::assertCanonicalEnvelope($result, $requireMaterializationPlan);
} catch ( \InvalidArgumentException $exception ) {
$assert(str_contains($exception->getMessage(), $expectedMessage), $message, $exception->getMessage());
return;
}

$assert(false, $message, 'Canonical envelope validation unexpectedly passed.');
};

$fixture = file_get_contents(dirname(__DIR__) . '/fixtures/simple-html.html');
$result = ( new HtmlTransformer() )->transform($fixture . "\n<ul><li>One</li><li><strong>Two</strong></li></ul><aside>Fallback</aside>")->toArray();

$assert(TransformerResult::SCHEMA === $result['schema'], 'result exposes schema');
TransformerResult::assertCanonicalEnvelope($result);

foreach ( array( 'status', 'components', 'block_types', 'source_reports', 'legacy_mapping', 'blocks', 'serialized_blocks', 'documents', 'assets', 'diagnostics', 'fallbacks', 'provenance', 'coverage', 'context', 'metrics' ) as $key ) {
foreach ( array( 'status', 'components', 'block_types', 'source_reports', 'blocks', 'serialized_blocks', 'documents', 'assets', 'diagnostics', 'fallbacks', 'provenance', 'coverage', 'context', 'metrics' ) as $key ) {
$assert(array_key_exists($key, $result), "Missing result key: {$key}");
}
$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');

$missingConversionReport = $result;
unset($missingConversionReport['source_reports']['conversion_report']);
$assertInvalidCanonicalEnvelope($missingConversionReport, 'source_reports.conversion_report', 'canonical validation rejects results without conversion reports');

$assertInvalidCanonicalEnvelope($result, 'source_reports.materialization_plan', 'canonical validation can require materialization plans for downstream artifact consumers', true);

$contextual = ( new HtmlTransformer() )->transform(
'<main><h1>Context</h1><aside>Fallback</aside></main>',
Expand Down Expand Up @@ -118,11 +138,12 @@ function serialize_blocks(array $blocks): string
'generated_html' => '<main><article data-component="Hero"><h1>Hello artifact</h1></article></main>',
)
)->toArray();
TransformerResult::assertCanonicalEnvelope($simple);
$assert('success' === $simple['status'], 'simple artifact compiles successfully', (string) $simple['status']);
$assert('index.html' === ($simple['source_reports']['artifact']['entry_path'] ?? ''), 'generated HTML becomes an index entry');
$assert(str_contains((string) $simple['serialized_blocks'], '<!-- wp:html -->'), 'HTML is preserved as serialized block markup');
$assert('hero' === ($simple['components'][0]['name'] ?? ''), 'component candidates are exposed');
$assert(is_array($simple['legacy_mapping'] ?? null), 'migration mapping metadata is exposed');
$assert(! array_key_exists('legacy_mapping', $simple), 'artifact result omits compatibility-only legacy mapping');
$assert(strlen('<main><article data-component="Hero"><h1>Hello artifact</h1></article></main>') === ($simple['metrics']['input_bytes'] ?? null), 'artifact metrics expose input bytes');
$assert(strlen((string) $simple['serialized_blocks']) === ($simple['metrics']['output_bytes'] ?? null), 'artifact metrics expose output bytes');
$assert(0 === ($simple['metrics']['block_count'] ?? null), 'artifact metrics expose block count');
Expand All @@ -134,6 +155,14 @@ function serialize_blocks(array $blocks): string
$assert(1 === ($simple['source_reports']['materialization_plan']['totals']['pages'] ?? null), 'materialization plan counts pages');
$assert('index' === ($simple['source_reports']['materialization_plan']['pages'][0]['slug'] ?? ''), 'materialization plan exposes page slug');

$missingMaterializationPlan = $simple;
unset($missingMaterializationPlan['source_reports']['materialization_plan']);
$assertInvalidCanonicalEnvelope($missingMaterializationPlan, 'source_reports.materialization_plan', 'canonical validation rejects artifact results without materialization plans');

$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');

$staticSite = $compiler->compile(
array(
'entrypoint' => 'index.html',
Expand Down
Loading