Skip to content

custom-block generation first slice: core/html → PHP-only dynamic block via classifier, fed to companion_plugin_payload#269

Merged
chubes4 merged 1 commit into
trunkfrom
feat/custom-block-generation-v2
Jun 28, 2026
Merged

custom-block generation first slice: core/html → PHP-only dynamic block via classifier, fed to companion_plugin_payload#269
chubes4 merged 1 commit into
trunkfrom
feat/custom-block-generation-v2

Conversation

@chubes4

@chubes4 chubes4 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

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/html fallback point and attaches a verdict; ArtifactCompiler already emits a companion_plugin_payload that SSI scaffolds into PHP-only dynamic blocks (#240 / #492 / #498 / #500). This PR adds the missing producer link: when the classifier says a core/html-fallback subtree is a custom_block, GENERATE a block definition, emit a block REFERENCE instead of raw core/html, and add the definition to companion_plugin_payload.blocks[].

Generation flow

At the core/html fallback decision (the unsupported_element path — a subtree that mapped to nothing native/Automattic), FallbackEmitter::maybeGenerateCustomBlock() consults the already-wired SubtreeClassifier. When it returns bucket=custom_block with confidence ≥ 0.7 and the sanitized subtree is non-empty:

  1. Generate a dynamic (PHP-only) block via the new CustomBlockGenerator: a generic, structurally-derived local name (e.g. collection-<sig8>), a single editable content attribute, supports.html=false, and a server-rendered render.php (wp_kses_post($attributes['content']) inside get_block_wrapper_attributes()). Dynamic/server-rendered → no save() mismatch.
  2. Emit a self-closing dynamic block reference in output (blockName = <namespace>/<local>, attrs only, no innerHTML) instead of core/html.
  3. Add the definition to companion_plugin_payload.blocks[]HtmlTransformer surfaces source_reports.generated_blocks; ArtifactCompiler threads them into CompanionPluginPayload (verified against the SSI consumer's name / block_json / render contract) and namespaces references to the per-site companion plugin (ssi-<site_slug>) so emitted refs match the registered blocks.

Gate + dedup

  • Conservative gate: generate ONLY when bucket=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.
  • Dedup by structural signature: a tag-only DOM skeleton (ignoring text/attributes) keys a per-transform registry. Same-shape subtrees → ONE generated block TYPE, reused; per-instance content rides each reference's attrs. No near-duplicate block types ("no zoo").

Before / after (qualifying subtree)

Input <my-pricing><div class="tier"><h3>Basic</h3><p>$9</p></div> …×3… </my-pricing>:

  • Before: dropped to an html_unsupported_element fallback diagnostic (raw-HTML-equivalent), no block, no payload.
  • After: one generated type collection-623e0f92 (title "Custom Collection", dynamic render.php) in source_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_element fallback for a minimal, green end-to-end slice; the div/section paths 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). Baseline composer 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_blocks entry.
  • html-custom-block-generation-dedup — two identical shapes → one type, two refs.
  • artifact-custom-block-companion-payload — end-to-end → matching companion_plugin_payload.blocks[] entry (SSI-shaped, namespaced block_json.name).
  • html-custom-block-low-confidence-unchanged — negative control: weak signals stay unknown, fallback unchanged.
  • tests/unit/custom-block-generator.php — generator shape + gate + dedup.

Full composer test green (135 parity fixtures), php -l clean.

DO NOT MERGE.

🤖 Generated with Claude Code

…#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>
@chubes4 chubes4 merged commit 4e69d0a into trunk Jun 28, 2026
1 check passed
@chubes4 chubes4 deleted the feat/custom-block-generation-v2 branch June 28, 2026 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant