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
82 changes: 82 additions & 0 deletions php-transformer/src/WordPressSitePlan/WordPressSitePlanView.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
namespace Automattic\BlocksEngine\PhpTransformer\WordPressSitePlan;

use Automattic\BlocksEngine\PhpTransformer\Contract\TransformerResult;
use InvalidArgumentException;

/** Bounded importer-facing projection of a canonical WordPress site plan result. */
final class WordPressSitePlanView
{
public const SCHEMA = 'blocks-engine/wordpress-site-plan-view/v1';
public const COMPACT_SCHEMA = 'blocks-engine/wordpress-site-plan-view/v2';
public const MAX_FAILURE_DIAGNOSTICS = 100;

/** @return array<string,mixed> */
Expand Down Expand Up @@ -52,6 +54,86 @@ public function fromResult(TransformerResult|array $result): array
);
}

/**
* Compact only the view-owned duplicate asset transports. The canonical plan
* remains unchanged at every external boundary; materialize() restores it.
*
* @param array<string,mixed> $view
* @return array<string,mixed>
*/
public function compact(array $view): array
{
if (self::SCHEMA !== ($view['schema'] ?? null) || !is_array($view['wordpress_site_plan'] ?? null)) {
throw new InvalidArgumentException('WordPress site plan view compaction requires the canonical v1 view.');
}
$plan = $view['wordpress_site_plan'];
if (!is_array($plan['assets'] ?? null) || !is_array($plan['writes'] ?? null)) {
throw new InvalidArgumentException('WordPress site plan view has an invalid canonical plan.');
}
$payloads = array();
foreach ($plan['assets'] as &$asset) {
$field = is_string($asset['content_base64'] ?? null) ? 'content_base64' : 'content';
$data = $asset[$field] ?? null;
if (!is_string($data)) continue;
$key = hash('sha256', $data);
$payloads[$key] = array('sha256' => $key, 'data' => $data);
$compactAsset = array();
foreach ($asset as $assetKey => $assetValue) $compactAsset[$field === $assetKey ? 'view_payload' : $assetKey] = $field === $assetKey ? $key : $assetValue;
$asset = $compactAsset;
$asset['view_payload_field'] = $field;
}
unset($asset);
foreach ($plan['writes'] as &$write) {
$payload = $write['payload'] ?? null;
if (!is_array($payload) || !is_string($payload['data'] ?? null) || !in_array($payload['encoding'] ?? null, array('utf8', 'base64'), true)) continue;
$key = hash('sha256', $payload['data']);
$payloads[$key] = array('sha256' => $key, 'data' => $payload['data']);
$compactPayload = array();
foreach ($payload as $payloadKey => $payloadValue) $compactPayload['data' === $payloadKey ? 'view_payload' : $payloadKey] = 'data' === $payloadKey ? $key : $payloadValue;
$write['payload'] = $compactPayload;
}
unset($write);
$view['schema'] = self::COMPACT_SCHEMA;
$view['wordpress_site_plan'] = $plan;
$view['view_payloads'] = $payloads;
return $view;
}

/** @param array<string,mixed> $view @return array<string,mixed> */
public static function materialize(array $view): array
{
if (self::COMPACT_SCHEMA !== ($view['schema'] ?? null) || !is_array($view['wordpress_site_plan'] ?? null) || !is_array($view['view_payloads'] ?? null)) throw new InvalidArgumentException('WordPress site plan view materialization requires the compact v2 view.');
$payloads = $view['view_payloads'];
$payload = static function (mixed $key) use ($payloads): string {
$row = is_string($key) ? ($payloads[$key] ?? null) : null;
if (!is_array($row) || !is_string($row['data'] ?? null) || !is_string($row['sha256'] ?? null) || $key !== $row['sha256'] || !hash_equals($key, hash('sha256', $row['data']))) throw new InvalidArgumentException('WordPress site plan view payload is invalid.');
return $row['data'];
};
$plan = $view['wordpress_site_plan'];
foreach ($plan['assets'] as &$asset) {
if (!isset($asset['view_payload'])) continue;
$data = $payload($asset['view_payload']);
$field = $asset['view_payload_field'] ?? null;
if (!in_array($field, array('content', 'content_base64'), true)) throw new InvalidArgumentException('WordPress site plan view payload transport is invalid.');
$restoredAsset = array();
foreach ($asset as $assetKey => $assetValue) if ('view_payload_field' !== $assetKey) $restoredAsset['view_payload' === $assetKey ? $field : $assetKey] = 'view_payload' === $assetKey ? $data : $assetValue;
$asset = $restoredAsset;
}
unset($asset);
foreach ($plan['writes'] as &$write) {
if (!isset($write['payload']['view_payload'])) continue;
$data = $payload($write['payload']['view_payload']);
$restoredPayload = array();
foreach ($write['payload'] as $payloadKey => $payloadValue) $restoredPayload['view_payload' === $payloadKey ? 'data' : $payloadKey] = 'view_payload' === $payloadKey ? $data : $payloadValue;
$write['payload'] = $restoredPayload;
}
unset($write);
unset($view['view_payloads']);
$view['schema'] = self::SCHEMA;
$view['wordpress_site_plan'] = $plan;
return $view;
}

/** @param array<int,array<string,mixed>> $diagnostics @return array<int,array<string,mixed>> */
private function boundedFailureDiagnostics(array $diagnostics): array
{
Expand Down
29 changes: 29 additions & 0 deletions php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Automattic\BlocksEngine\PhpTransformer\Path\ArtifactPath;
use Automattic\BlocksEngine\PhpTransformer\StaticSite\FontMaterialization\FontMaterializationPlanBuilder;
use Automattic\BlocksEngine\PhpTransformer\WordPressSitePlan\WordPressSitePlanView;
use Automattic\BlocksEngine\PhpTransformer\WordPressSitePlan\WordPressSitePlan;
use Automattic\BlocksEngine\PhpTransformer\StaticSite\MaterializationPlanBuilder;
use Automattic\BlocksEngine\PhpTransformer\VisualParity\TypographyVisualProbe;
use Automattic\BlocksEngine\PhpTransformer\VisualParity\TypographyVisualProbeComparator;
Expand Down Expand Up @@ -3695,6 +3696,34 @@ public function recognize(DOMElement $element, PatternContext $context): ?Patter
$assert(WordPressSitePlanView::SCHEMA === ($simpleObjectPlanView['schema'] ?? ''), 'Transformer result exposes the bounded WordPress site plan view directly');
$assert(($simple['source_reports']['wordpress_site_plan'] ?? array()) === ($simplePlanView['wordpress_site_plan'] ?? null), 'WordPress site plan view preserves the exact canonical plan');
$assert(($boundedHandoffResult->toArray()['source_reports']['wordpress_site_plan'] ?? array()) === ($simpleObjectPlanView['wordpress_site_plan'] ?? null), 'TransformerResult handoff preserves the exact canonical plan without a compatibility projection');
$payloadPlanView = $compiler->compile(array('entrypoint' => 'index.html', 'files' => array('index.html' => '<link rel="stylesheet" href="assets/site.css"><main>Payload view</main>', 'assets/site.css' => '.payload{color:#123}')))->toWordPressSitePlanView();
$payloadPlanView['wordpress_site_plan']['assets'] = array(
array('path' => 'assets/authored-base64.css', 'content_base64' => base64_encode('.base64-css{color:#1354}'), 'transport_sha256' => 'authored-base64-transport'),
array('path' => 'assets/authored-base64.txt', 'content_base64' => base64_encode("base64 text\n"), 'transport_sha256' => 'authored-text-transport'),
array('path' => 'assets/binary.bin', 'content_base64' => base64_encode("\x00\xffbinary\x00"), 'binary' => true, 'transport_sha256' => 'binary-transport'),
array('path' => 'assets/utf8.css', 'content' => '.utf8{content:"cafe"}', 'transport_sha256' => 'utf8-transport'),
);
$payloadPlanView['wordpress_site_plan']['writes'] = array(
array('payload' => array('encoding' => 'base64', 'data' => base64_encode('write bytes'), 'transport' => 'authored')),
array('payload' => array('encoding' => 'utf8', 'data' => 'write text', 'transport' => 'authored')),
);
$compactPlanView = (new WordPressSitePlanView())->compact($payloadPlanView);
$materializedCompactPlanView = WordPressSitePlanView::materialize(json_decode(json_encode($compactPlanView, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR));
$serializedPayloadPlanView = json_decode(json_encode($payloadPlanView, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR);
$assert(WordPressSitePlanView::COMPACT_SCHEMA === ($compactPlanView['schema'] ?? null) && !isset($compactPlanView['wordpress_site_plan']['assets'][0]['content_base64']), 'WordPress site plan compact view removes base64 asset transports');
$assert($payloadPlanView['wordpress_site_plan']['assets'] === $materializedCompactPlanView['wordpress_site_plan']['assets'], 'WordPress site plan compact view exactly restores asset payload transports');
$assert($payloadPlanView['wordpress_site_plan']['writes'] === $materializedCompactPlanView['wordpress_site_plan']['writes'], 'WordPress site plan compact view exactly restores write payload transports');
$assert($serializedPayloadPlanView === $materializedCompactPlanView, 'WordPress site plan compact view exactly restores the complete persisted v1 view');
$assert("\x00\xffbinary\x00" === base64_decode($materializedCompactPlanView['wordpress_site_plan']['assets'][2]['content_base64'], true) && '.base64-css{color:#1354}' === base64_decode($materializedCompactPlanView['wordpress_site_plan']['assets'][0]['content_base64'], true), 'WordPress site plan compact view preserves binary and base64 payload bytes');
$assert(WordPressSitePlan::canonicalHash($payloadPlanView['wordpress_site_plan']) === WordPressSitePlan::canonicalHash($materializedCompactPlanView['wordpress_site_plan']), 'WordPress site plan compact view preserves the canonical plan hash');
$tamperedCompactPlanView = $compactPlanView;
$tamperedCompactPlanView['wordpress_site_plan']['assets'][0]['view_payload_field'] = 'binary';
try { WordPressSitePlanView::materialize($tamperedCompactPlanView); $tamperedCompactRejected = false; } catch (InvalidArgumentException) { $tamperedCompactRejected = true; }
$assert($tamperedCompactRejected, 'WordPress site plan compact view rejects tampered payload transport metadata');
try { WordPressSitePlanView::materialize($payloadPlanView); $canonicalViewRejected = false; } catch (InvalidArgumentException) { $canonicalViewRejected = true; }
$assert($canonicalViewRejected, 'WordPress site plan materialization rejects canonical v1 views');
try { (new WordPressSitePlanView())->compact($compactPlanView); $compactViewRejected = false; } catch (InvalidArgumentException) { $compactViewRejected = true; }
$assert($compactViewRejected, 'WordPress site plan compaction rejects compact v2 views');
$assert(($simple['source_reports']['editability_report'] ?? null) === ($simplePlanView['editability_report'] ?? null), 'WordPress site plan view preserves the producer-owned editability report exactly');
$assert(array('schema', 'metrics', 'block_types', 'documents', 'signals', 'signal_totals') === array_keys($simplePlanView['editability_report'] ?? array()) && EditabilityReport::SCHEMA === ($simplePlanView['editability_report']['schema'] ?? null), 'WordPress site plan view exposes the current versioned editability report shape');
$boundedEditabilityReport = (new EditabilityReport())->fromDocuments(array('large.html' => array('blocks' => array_fill(0, 101, array('blockName' => 'core/group', 'attrs' => array(), 'innerBlocks' => array(), 'innerHTML' => '')))));
Expand Down
Loading