custom-block generation first slice: core/html → PHP-only dynamic block via classifier, fed to companion_plugin_payload#269
Merged
Conversation
…#497)
First slice of custom-block GENERATION — the producer link of the
classify -> route -> generate chain (epic #497, keystone #491).
At a core/html fallback decision (a subtree that mapped to nothing
native/Automattic), consult the already-wired SubtreeClassifier. When it
returns bucket=custom_block above a conservative confidence threshold,
generate a dynamic (PHP-only) block definition, emit a self-closing block
reference (attrs only, no innerHTML) instead of raw core/html, and add the
definition to companion_plugin_payload.blocks[] in the shape the SSI
companion-plugin scaffolder consumes (name, block_json, render).
- New CustomBlockGenerator: dynamic block.json (single editable `content`
attribute, supports.html=false) + server-rendered render.php (no
save()/render mismatch). Generic only: name/title derive from structure.
- FallbackEmitter.maybeGenerateCustomBlock: conservative gate
(custom_block + confidence >= 0.7) and structural-signature dedup so
same-shape subtrees collapse to ONE block type reused via per-instance
reference attrs ("no zoo").
- HtmlTransformer surfaces source_reports.generated_blocks; ArtifactCompiler
threads them into CompanionPluginPayload and namespaces references to the
per-site companion plugin (ssi-<site_slug>).
- Wired only at the unsupported_element fallback for a minimal, green
end-to-end slice; div/section paths already convert natively.
Tests: +4 parity fixtures (generation, dedup, artifact payload, negative
low-confidence) and a CustomBlockGenerator unit suite. Full composer test
green (135 parity fixtures); no existing fixtures changed.
Refs #497
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #497
What
First slice of custom-block generation — the producer link of the classify → route → generate chain (epic spine #497, keystone #491). The classifier (#258 wiring) already runs at the
core/htmlfallback point and attaches a verdict;ArtifactCompileralready emits acompanion_plugin_payloadthat SSI scaffolds into PHP-only dynamic blocks (#240 / #492 / #498 / #500). This PR adds the missing producer link: when the classifier says acore/html-fallback subtree is acustom_block, GENERATE a block definition, emit a block REFERENCE instead of rawcore/html, and add the definition tocompanion_plugin_payload.blocks[].Generation flow
At the
core/htmlfallback decision (theunsupported_elementpath — a subtree that mapped to nothing native/Automattic),FallbackEmitter::maybeGenerateCustomBlock()consults the already-wiredSubtreeClassifier. When it returns bucket=custom_blockwith confidence ≥ 0.7 and the sanitized subtree is non-empty:CustomBlockGenerator: a generic, structurally-derived local name (e.g.collection-<sig8>), a single editablecontentattribute,supports.html=false, and a server-renderedrender.php(wp_kses_post($attributes['content'])insideget_block_wrapper_attributes()). Dynamic/server-rendered → nosave()mismatch.<namespace>/<local>, attrs only, no innerHTML) instead ofcore/html.companion_plugin_payload.blocks[]—HtmlTransformersurfacessource_reports.generated_blocks;ArtifactCompilerthreads them intoCompanionPluginPayload(verified against the SSI consumer'sname/block_json/rendercontract) and namespaces references to the per-site companion plugin (ssi-<site_slug>) so emitted refs match the registered blocks.Gate + dedup
custom_block, confidence ≥ 0.7 (the classifier's own MIN_SCORE/MARGIN already caps UNKNOWN at 0.4), and nothing native/Automattic matched. Otherwise keep the existing fallback behavior. Generic only — name/title derive from structure, never fixture/site strings.Before / after (qualifying subtree)
Input
<my-pricing><div class="tier"><h3>Basic</h3><p>$9</p></div> …×3… </my-pricing>:html_unsupported_elementfallback diagnostic (raw-HTML-equivalent), no block, no payload.collection-623e0f92(title "Custom Collection", dynamicrender.php) insource_reports.generated_blocks/companion_plugin_payload.blocks[], and a self-closing reference<!-- wp:ssi-<slug>/collection-623e0f92 {"content":"…"} /-->in output. Two identical<my-pricing>blocks → one type, two references.Scope
Wired only at the
unsupported_elementfallback for a minimal, green end-to-end slice; thediv/sectionpaths already convert repeated content natively (e.g.html-class-grid-card-layout), so they are intentionally untouched. Classifier +Style//Classification/internals are read-only.Verification
Intentional behavior change for qualifying subtrees (
core/html→ generated dynamic block + payload entry). Baselinecomposer test:canonical+composer parity(131) were green first; no existing fixtures changed.Added tests:
html-custom-block-generation— high-confidence custom_block fallback → generated dynamic block reference +generated_blocksentry.html-custom-block-generation-dedup— two identical shapes → one type, two refs.artifact-custom-block-companion-payload— end-to-end → matchingcompanion_plugin_payload.blocks[]entry (SSI-shaped, namespacedblock_json.name).html-custom-block-low-confidence-unchanged— negative control: weak signals stayunknown, fallback unchanged.tests/unit/custom-block-generator.php— generator shape + gate + dedup.Full
composer testgreen (135 parity fixtures),php -lclean.DO NOT MERGE.
🤖 Generated with Claude Code