diff --git a/php-transformer/docs/html-validation-outcome-consumer-inventory.md b/php-transformer/docs/html-validation-outcome-consumer-inventory.md index 80e24bafd..74c2f1ef7 100644 --- a/php-transformer/docs/html-validation-outcome-consumer-inventory.md +++ b/php-transformer/docs/html-validation-outcome-consumer-inventory.md @@ -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 | | --- | --- | --- | @@ -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. diff --git a/php-transformer/src/Contract/HtmlValidationOutcome.php b/php-transformer/src/Contract/HtmlValidationOutcome.php index e07ee779d..28c372463 100644 --- a/php-transformer/src/Contract/HtmlValidationOutcome.php +++ b/php-transformer/src/Contract/HtmlValidationOutcome.php @@ -27,38 +27,25 @@ public function __construct( ) { } - /** @param array $blockValidityReport @param array $semanticParityReport @param array $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 $blockValidityReport + * @param array> $blockValidityFindings * @param array> $semanticParityFindings * @param array $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')) ); @@ -72,9 +59,15 @@ private static function status(array $report): string /** @param array $report @param array $fields @return array> */ private static function findings(array $report, array $fields): array + { + return self::filteredFindings(is_array($report['findings'] ?? null) ? $report['findings'] : array(), $fields); + } + + /** @param array $sourceFindings @param array $fields @return array> */ + 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; } diff --git a/php-transformer/src/HtmlToBlocks/HtmlCompilation.php b/php-transformer/src/HtmlToBlocks/HtmlCompilation.php index 5f63507f5..1508fd79f 100644 --- a/php-transformer/src/HtmlToBlocks/HtmlCompilation.php +++ b/php-transformer/src/HtmlToBlocks/HtmlCompilation.php @@ -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 diff --git a/php-transformer/src/WordPress/BlockValidityEvaluation.php b/php-transformer/src/WordPress/BlockValidityEvaluation.php new file mode 100644 index 000000000..5723fed3a --- /dev/null +++ b/php-transformer/src/WordPress/BlockValidityEvaluation.php @@ -0,0 +1,88 @@ + $summary + * @param array> $findings + */ + private function __construct( + public readonly string $status, + public readonly array $summary, + public readonly array $findings + ) { + } + + /** @param array> $blocks */ + public static function fromBlocks(array $blocks): self + { + return ( new BlockValidityValidator() ) + ->evaluateBlocks($blocks) + ->withAdditionalFindings(( new CanonicalSaveShapeValidator() )->findings($blocks)); + } + + /** + * @param array $checkedBlockTypes + * @param array> $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> $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 */ + public function report(): array + { + return array( + 'schema' => BlockValidityValidator::SCHEMA, + 'status' => $this->status, + 'summary' => $this->summary, + 'findings' => $this->findings, + ); + } +} diff --git a/php-transformer/src/WordPress/BlockValidityValidator.php b/php-transformer/src/WordPress/BlockValidityValidator.php index f952a8993..009deef58 100644 --- a/php-transformer/src/WordPress/BlockValidityValidator.php +++ b/php-transformer/src/WordPress/BlockValidityValidator.php @@ -26,9 +26,8 @@ final class BlockValidityValidator /** * @param array> $blocks - * @return array */ - public function validateBlocks(array $blocks): array + public function evaluateBlocks(array $blocks): BlockValidityEvaluation { $findings = array(); $checkedBlockTypes = array(); @@ -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> $blocks + * @return array + */ + public function validateBlocks(array $blocks): array + { + return $this->evaluateBlocks($blocks)->report(); } /** diff --git a/php-transformer/src/WordPress/Runtime.php b/php-transformer/src/WordPress/Runtime.php index 1b0c666da..135f5078d 100644 --- a/php-transformer/src/WordPress/Runtime.php +++ b/php-transformer/src/WordPress/Runtime.php @@ -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, '

Unbalanced'; +$unbalancedEvaluation = $runtime->evaluateBlockSerialization($unbalancedSerialization); +assertSame($runtime->validateBlockSerialization($unbalancedSerialization), $unbalancedEvaluation->report(), 'Block validity evaluation should preserve unbalanced serialized-comment report semantics.'); +assertSame(array('serialized_block_comment_in_inner_content', 'serialized_block_comment_in_inner_html'), array_column($unbalancedEvaluation->findings, 'code'), 'Unbalanced serialized comments should retain structural parser findings and ordering.'); + assertSame('Safe text', $runtime->stripAllTags('

Safe text

'), 'Fallback tag stripping should remove scripts and tags.'); assertSame('wordpress_strip_all_tags_unavailable', $runtime->diagnostics()[0]['code'] ?? null, 'Fallback tag stripping should expose a diagnostic.');