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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# HTML Validation Outcome Consumer Inventory

`HtmlCompilation` evaluates block validity, semantic parity, and content round-trip once. `SemanticParityReporter::evaluate()` owns the semantic status, detailed enriched findings, landmark counts, and menu pairing/folding facts. Its `report()` facade projects the unchanged detailed report, while `HtmlCompilation` passes the evaluation's status and findings directly to `Contract\HtmlValidationOutcome`, which it carries in `BlockCompilationOutput`.
`HtmlCompilation` evaluates block validity, semantic parity, and content round-trip once. `BlockValidityValidator::evaluateBlocks()` produces structural `BlockValidityEvaluation` facts and `validateBlocks()` projects its public report; `Runtime::evaluateBlockSerialization()` parses string input once and returns the evaluation merged with canonical save-shape findings. Its `report()` facade projects the unchanged detailed report. `SemanticParityReporter::evaluate()` owns the semantic status, detailed enriched findings, landmark counts, and menu pairing/folding facts. `HtmlCompilation` passes both evaluations' explicit status and findings to the implementation-independent `Contract\HtmlValidationOutcome`, which it carries in `BlockCompilationOutput`.

| Consumer | Required facts | Source |
| --- | --- | --- |
Expand All @@ -9,4 +9,4 @@
| `ArtifactCompiler`, staged plans, and `WordPressSitePlan` | flattened diagnostics and their severities for artifact acceptance | existing `TransformerResult::diagnostics`, populated from `HtmlValidationOutcome` |
| `HtmlResultComposer` and `ConversionReportProjection` | full detailed validator evidence | unchanged `source_reports.wp_block_validity`, `semantic_parity`, and `content_round_trip`; `SemanticParityEvaluation::report()` supplies semantic parity and it remains projected into `conversion_report` |

The required outcome stores each validator status plus only the original finding keys used to emit diagnostics. Semantic facts come from the evaluation, not from a report map. Values remain mixed because the existing collector preserves any non-null severity and location value; it owns the existing defaults for missing or null `summary` and `severity`. Detailed report-only evidence remains outside the outcome and is serialized only through the existing report projections. Empty and parse-failure HTML results carry `BlockCompilationOutput::empty()` with explicit empty, `not_evaluated` validation outcomes.
The required outcome stores each validator status plus only the original finding keys used to emit diagnostics. Block-validity and semantic facts come from their evaluations, not from report maps. Values remain mixed because the existing collector preserves any non-null severity and location value; it owns the existing defaults for missing or null `summary` and `severity`. Detailed report-only evidence remains outside the outcome and is serialized only through the existing report projections. Empty and parse-failure HTML results carry `BlockCompilationOutput::empty()` with explicit empty, `not_evaluated` validation outcomes.
37 changes: 15 additions & 22 deletions php-transformer/src/Contract/HtmlValidationOutcome.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,25 @@ public function __construct(
) {
}

/** @param array<string, mixed> $blockValidityReport @param array<string, mixed> $semanticParityReport @param array<string, mixed> $contentRoundTripReport */
public static function fromReports(array $blockValidityReport, array $semanticParityReport, array $contentRoundTripReport): self
{
return new self(
blockValidityStatus: self::status($blockValidityReport),
blockValidityFindings: self::findings($blockValidityReport, array('block_name', 'path')),
semanticParityStatus: self::status($semanticParityReport),
semanticParityFindings: self::findings($semanticParityReport, array('selector')),
contentRoundTripStatus: self::status($contentRoundTripReport),
contentRoundTripFindings: self::findings($contentRoundTripReport, array('text'))
);
}

/**
* Semantic parity is evaluated by its producer; its detailed report remains
* a projection rather than the source of required diagnostic facts.
* Validators supply facts directly; detailed reports remain projections.
*
* @param array<string, mixed> $blockValidityReport
* @param array<int, array<string, mixed>> $blockValidityFindings
* @param array<int, array<string, mixed>> $semanticParityFindings
* @param array<string, mixed> $contentRoundTripReport
*/
public static function fromBlockValidityAndContentRoundTripReports(
array $blockValidityReport,
public static function fromValidationFactsAndContentRoundTripReport(
string $blockValidityStatus,
array $blockValidityFindings,
string $semanticParityStatus,
array $semanticParityFindings,
array $contentRoundTripReport
): self {
return new self(
blockValidityStatus: self::status($blockValidityReport),
blockValidityFindings: self::findings($blockValidityReport, array('block_name', 'path')),
blockValidityStatus: $blockValidityStatus,
blockValidityFindings: self::filteredFindings($blockValidityFindings, array('block_name', 'path')),
semanticParityStatus: $semanticParityStatus,
semanticParityFindings: self::findings(array('findings' => $semanticParityFindings), array('selector')),
semanticParityFindings: self::filteredFindings($semanticParityFindings, array('selector')),
contentRoundTripStatus: self::status($contentRoundTripReport),
contentRoundTripFindings: self::findings($contentRoundTripReport, array('text'))
);
Expand All @@ -72,9 +59,15 @@ private static function status(array $report): string

/** @param array<string, mixed> $report @param array<int, string> $fields @return array<int, array<string, mixed>> */
private static function findings(array $report, array $fields): array
{
return self::filteredFindings(is_array($report['findings'] ?? null) ? $report['findings'] : array(), $fields);
}

/** @param array<int, mixed> $sourceFindings @param array<int, string> $fields @return array<int, array<string, mixed>> */
private static function filteredFindings(array $sourceFindings, array $fields): array
{
$findings = array();
foreach ($report['findings'] ?? array() as $finding) {
foreach ($sourceFindings as $finding) {
if (!is_array($finding)) {
continue;
}
Expand Down
8 changes: 5 additions & 3 deletions php-transformer/src/HtmlToBlocks/HtmlCompilation.php
Original file line number Diff line number Diff line change
Expand Up @@ -1291,12 +1291,14 @@ public function transform(string $html, array $options = array()): TransformerRe
$authorStylesheetProjections
);
$this->navigationStyleProjector->materializeEditorStaticStateStylesheet();
$blockValidityReport = $this->runtime->validateBlockSerialization($blocks);
$blockValidityEvaluation = $this->runtime->evaluateBlockSerialization($blocks);
$blockValidityReport = $blockValidityEvaluation->report();
$semanticParityEvaluation = $this->semanticParityReporter->evaluate($body, $blocks, $sourceProvenance, $html, (string) ($options['static_css'] ?? ''));
$semanticParityReport = $semanticParityEvaluation->report();
$contentRoundTripReport = $this->contentRoundTripReporter->report($serializedBlocks, $html, $this->transformationEvidence()->formControlEchoTexts());
$validationOutcome = \Automattic\BlocksEngine\PhpTransformer\Contract\HtmlValidationOutcome::fromBlockValidityAndContentRoundTripReports(
$blockValidityReport,
$validationOutcome = \Automattic\BlocksEngine\PhpTransformer\Contract\HtmlValidationOutcome::fromValidationFactsAndContentRoundTripReport(
$blockValidityEvaluation->status,
$blockValidityEvaluation->findings,
$semanticParityEvaluation->status(),
$semanticParityEvaluation->findings,
$contentRoundTripReport
Expand Down
88 changes: 88 additions & 0 deletions php-transformer/src/WordPress/BlockValidityEvaluation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);

namespace Automattic\BlocksEngine\PhpTransformer\WordPress;

/**
* The single block-validity evaluation. Its report is a projection for public
* callers; compiler consumers use the authoritative status and findings here.
*/
final class BlockValidityEvaluation
{
/**
* @param array<string, mixed> $summary
* @param array<int, array<string, mixed>> $findings
*/
private function __construct(
public readonly string $status,
public readonly array $summary,
public readonly array $findings
) {
}

/** @param array<int, array<string, mixed>> $blocks */
public static function fromBlocks(array $blocks): self
{
return ( new BlockValidityValidator() )
->evaluateBlocks($blocks)
->withAdditionalFindings(( new CanonicalSaveShapeValidator() )->findings($blocks));
}

/**
* @param array<int, string> $checkedBlockTypes
* @param array<int, array<string, mixed>> $findings
*/
public static function fromStructuralFacts(int $blockCount, array $checkedBlockTypes, array $findings): self
{
return new self(
status: array() === $findings ? 'pass' : 'warning',
summary: array(
'block_count' => $blockCount,
'finding_count' => count($findings),
'checked_block_types' => $checkedBlockTypes,
),
findings: $findings
);
}

/** @param array<int, array<string, mixed>> $findings */
public function withAdditionalFindings(array $findings): self
{
if ( array() === $findings ) {
return $this;
}

$findings = array_merge($this->findings, $findings);
$summary = $this->summary;
$summary['finding_count'] = count($findings);

return new self('warning', $summary, $findings);
}

public function withParseFailure(): self
{
$findings = $this->findings;
$findings[] = array(
'code' => 'serialized_blocks_parse_failed',
'severity' => 'warning',
'category' => 'wp_block_validity',
'path' => 'serialized_blocks',
'summary' => 'Serialized block comments were present but could not be parsed into a balanced block tree.',
);
$summary = $this->summary;
$summary['finding_count'] = count($findings);

return new self('warning', $summary, $findings);
}

/** @return array<string, mixed> */
public function report(): array
{
return array(
'schema' => BlockValidityValidator::SCHEMA,
'status' => $this->status,
'summary' => $this->summary,
'findings' => $this->findings,
);
}
}
23 changes: 11 additions & 12 deletions php-transformer/src/WordPress/BlockValidityValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ final class BlockValidityValidator

/**
* @param array<int, array<string, mixed>> $blocks
* @return array<string, mixed>
*/
public function validateBlocks(array $blocks): array
public function evaluateBlocks(array $blocks): BlockValidityEvaluation
{
$findings = array();
$checkedBlockTypes = array();
Expand All @@ -41,16 +40,16 @@ public function validateBlocks(array $blocks): array

sort($checkedBlockTypes);

return array(
'schema' => self::SCHEMA,
'status' => array() === $findings ? 'pass' : 'warning',
'summary' => array(
'block_count' => $this->countBlocks($blocks),
'finding_count' => count($findings),
'checked_block_types' => $checkedBlockTypes,
),
'findings' => $findings,
);
return BlockValidityEvaluation::fromStructuralFacts($this->countBlocks($blocks), $checkedBlockTypes, $findings);
}

/**
* @param array<int, array<string, mixed>> $blocks
* @return array<string, mixed>
*/
public function validateBlocks(array $blocks): array
{
return $this->evaluateBlocks($blocks)->report();
}

/**
Expand Down
52 changes: 15 additions & 37 deletions php-transformer/src/WordPress/Runtime.php
Original file line number Diff line number Diff line change
Expand Up @@ -488,52 +488,30 @@ private function canonicalRuntimeBlocks(array $blocks): array
*/
public function validateBlockSerialization(string|array $serializedBlocksOrBlocks): array
{
if ( is_string($serializedBlocksOrBlocks) ) {
$blocks = $this->parseBlocks($serializedBlocksOrBlocks);
$report = $this->buildBlockValidityReport($blocks);

if ( array() === $blocks && str_contains($serializedBlocksOrBlocks, '<!-- wp:') ) {
$report['status'] = 'warning';
$report['summary']['finding_count'] = ((int) ($report['summary']['finding_count'] ?? 0)) + 1;
$report['findings'][] = array(
'code' => 'serialized_blocks_parse_failed',
'severity' => 'warning',
'category' => 'wp_block_validity',
'path' => 'serialized_blocks',
'summary' => 'Serialized block comments were present but could not be parsed into a balanced block tree.',
);
}

return $report;
}

return $this->buildBlockValidityReport($serializedBlocksOrBlocks);
return $this->evaluateBlockSerialization($serializedBlocksOrBlocks)->report();
}

/**
* Run the serialization-structure validator and the canonical save()-shape
* validator over the same parsed block tree and merge their findings into a
* single wp_block_validity report. Both are pure-PHP and need no WordPress
* runtime, so the report stays usable in the standalone transformer loop.
* Parse string input once, then evaluate both validity validators over the
* resulting tree. The public report facade delegates here so consumers can
* use facts without reverse-engineering report maps.
*
* @param array<int, array<string, mixed>> $blocks
* @return array<string, mixed>
* @param string|array<int, array<string, mixed>> $serializedBlocksOrBlocks
*/
private function buildBlockValidityReport(array $blocks): array
public function evaluateBlockSerialization(string|array $serializedBlocksOrBlocks): BlockValidityEvaluation
{
$report = ( new BlockValidityValidator() )->validateBlocks($blocks);
if ( is_string($serializedBlocksOrBlocks) ) {
$blocks = $this->parseBlocks($serializedBlocksOrBlocks);
$evaluation = BlockValidityEvaluation::fromBlocks($blocks);

$saveShapeFindings = ( new CanonicalSaveShapeValidator() )->findings($blocks);
if ( array() !== $saveShapeFindings ) {
$report['findings'] = array_merge(
is_array($report['findings'] ?? null) ? $report['findings'] : array(),
$saveShapeFindings
);
$report['summary']['finding_count'] = count($report['findings']);
$report['status'] = 'warning';
if ( array() === $blocks && str_contains($serializedBlocksOrBlocks, '<!-- wp:') ) {
return $evaluation->withParseFailure();
}

return $evaluation;
}

return $report;
return BlockValidityEvaluation::fromBlocks($serializedBlocksOrBlocks);
}

public function stripAllTags(string $text, bool $removeBreaks = false): string
Expand Down
14 changes: 8 additions & 6 deletions php-transformer/tests/contract/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,13 @@ function serialize_blocks(array $blocks): string
&& array_map(static fn (array $finding): string => $finding['code'], $validationOutcome->contentRoundTripFindings) === array_map(static fn (array $finding): string => (string) ($finding['code'] ?? ''), $validationOutcomeReports['content_round_trip']['findings'] ?? array()),
'semantic evaluation directly supplies required diagnostic facts while its full report remains an identical projection'
);
$validationFailureOutcome = HtmlValidationOutcome::fromBlockValidityAndContentRoundTripReports(
array('status' => 'fail', 'findings' => array(array('code' => 'invalid_save', 'summary' => null, 'severity' => 0, 'block_name' => false, 'path' => 12, 'verbose_evidence' => array('not-needed')))),
'fail',
array(array('code' => 'missing_landmark', 'severity' => null, 'selector' => 0, 'verbose_evidence' => array('not-needed'))),
array('status' => 'fail', 'findings' => array(array('code' => 'invented_text', 'summary' => null, 'severity' => false, 'text' => array('unexpected'), 'verbose_evidence' => array('not-needed'))))
$validationFailureOutcome = new HtmlValidationOutcome(
blockValidityStatus: 'fail',
blockValidityFindings: array(array('code' => 'invalid_save', 'summary' => null, 'severity' => 0, 'block_name' => false, 'path' => 12)),
semanticParityStatus: 'fail',
semanticParityFindings: array(array('code' => 'missing_landmark', 'severity' => null, 'selector' => 0)),
contentRoundTripStatus: 'fail',
contentRoundTripFindings: array(array('code' => 'invented_text', 'summary' => null, 'severity' => false, 'text' => array('unexpected')))
);
$validationFailureDiagnostics = (new DiagnosticsCollector())->collect('Example\\Transformer', array(), array(), array(), array(), array(), $validationFailureOutcome);
$validationDiagnosticsByCode = array_column($validationFailureDiagnostics, null, 'code');
Expand All @@ -165,7 +167,7 @@ function serialize_blocks(array $blocks): string
&& array('unexpected') === ($validationDiagnosticsByCode['html_content_round_trip_invented_text']['text'] ?? null),
'required validation outcomes preserve failure diagnostics, existing fallback messages, and mixed severity and location values without retaining verbose report evidence'
);
$notEvaluatedOutcome = HtmlValidationOutcome::fromReports(array(), array(), array());
$notEvaluatedOutcome = HtmlValidationOutcome::fromValidationFactsAndContentRoundTripReport('not_evaluated', array(), 'not_evaluated', array(), array());
$notEvaluatedDiagnostics = (new DiagnosticsCollector())->collect('Example\\Transformer', array(), array(), array(), array(), array(), $notEvaluatedOutcome);
$assert(
'not_evaluated' === $notEvaluatedOutcome->blockValidityStatus
Expand Down
Loading
Loading