Skip to content

Bound editor presentation delivery - #1478

Open
chubes4 wants to merge 15 commits into
trunkfrom
fix/1465-bounded-editor-presentation
Open

Bound editor presentation delivery#1478
chubes4 wants to merge 15 commits into
trunkfrom
fix/1465-bounded-editor-presentation

Conversation

@chubes4

@chubes4 chubes4 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • stop serializing compiled presentation CSS into Gutenberg settings
  • deliver the same route-scoped presentation through an external editor stylesheet
  • preserve media conditions and frontend/editor parity without increasing memory limits

Verification

  • php php-transformer/tests/contract/wordpress-site-plan.php
  • php php-transformer/tests/contract/staged-artifact-compilation.php
  • focused media and WordPress integration coverage added by the candidate

Closes #1465

AI assistance

OpenAI gpt-5.6-terra through OpenCode implemented the candidate and tests. OpenAI gpt-5.6-sol through OpenCode reviewed the branch, commit metadata, diff shape, and repository-native contract results before opening this PR.

@chubes4

chubes4 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Promotion gate blocker: upstream observer, not this PR

The solved-site-promotion gate fails here with failed_fixture_count lte 0 (actual 2), but the per-fixture evidence shows the conversion is clean on both solved fixtures:

  • raw_status: "passed", final status: "failed"
  • finding_count: 0, no visual_mismatch category
  • editor_valid_block_rate: 1, editor_invalid_count: 0, native_conversion_rate: 1
  • matrix_evidence_readiness: verified

The demotion comes entirely from editor_presentation:

expected_identity_count: 6     observed_identity_count: 0
expected_identities_complete: true     coverage_complete: false

Root cause

This PR moves editor presentation from inline delivery (wp_theme_json_data_theme + block_editor_settings_all, which emitted :root{--blocks-engine-presentation:<hash>;} inside a <style>) to wp_enqueue_style(), i.e. an external <link>.

The wp-codebox observer harvests identities only from inline <style> text:

inlineStyleContents: Array.from(document.querySelectorAll("style"), (style) => style.textContent ?? "")
// matched against /blocks-engine-presentation:([a-f0-9]{64})/gi

External stylesheet URLs were recorded but never contributed an identity. The stylesheets are genuinely delivered; the observer cannot see them. So this is an upstream capability gap rather than a defect in this PR, and re-inlining the CSS here would defeat the PR's purpose.

Upstream fix

Automattic/wp-codebox#2449 — fixed in Automattic/wp-codebox#2451, now merged to main. An observed external stylesheet URL whose version parameter exactly matches an expected identity now certifies that identity, with the expected-set comparison still fail-closed.

What this PR still needs

The gate resolves wp-codebox through a pinned chain, and both pins are behind the fix:

blocks-engine .github/workflows/solved-fixture-regression.yml
  └─ SSI workflow @5d6c5ed4, static-site-importer-sha: 2edd49e6
       └─ WP_CODEBOX_VERSION: v0.21.0

That reusable workflow at 5d6c5ed4 pins wp-codebox v0.21.0, which is what the run environment reports. SSI main has since moved to v0.26.4, so this gate is two steps behind regardless of #2451.

So, in order: release wp-codebox with #2451, bump WP_CODEBOX_VERSION in the SSI solved-site-promotion.yml, then bump the SSI workflow ref/sha here. I have not run any release.

Evidence: gate run https://github.com/Automattic/blocks-engine/actions/runs/33463678144 (identical signature on the two prior runs, so it is stable and not a trunk interaction).


AI assistance disclosure: diagnosed with Claude Sonnet 4.5 via opencode. The model pulled the failed run artifacts, read the per-fixture editor_presentation record, traced the pin chain to the wp-codebox capture function, and authored the upstream fix and its tests in Automattic/wp-codebox#2451. Reviewed by me before posting.

The gate resolved wp-codebox through an SSI workflow pinned at v0.21.0,
so the editor presentation capture could not observe an identity carried
by an external stylesheet URL version. Bounded external delivery of the
generated editor styles therefore reported 0 of 6 expected identities on
both solved fixtures while the stylesheets were in fact loaded.

Move the reusable workflow and the checked-out SSI code to 8a9edbce,
which pins wp-codebox v0.26.5 and carries the capture fix from
Automattic/wp-codebox#2451.
@chubes4

chubes4 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Update: pin chain resolved, and a second regression is now visible

The wp-codebox pin chain from my earlier comment is done. The gate on this branch now runs WP_CODEBOX_VERSION: v0.26.5:

The original expected 6 / observed 0 identity failure is gone. Two distinct problems remain, and only one of them is an observability gap.

1. Expected identity set is now empty (upstream)

With bounded external delivery there are no inline --blocks-engine-presentation: markers left in block_editor_settings_all, so the expectation query returns nothing:

expectedGeneratedPresentationIdentities: []
expectedGeneratedPresentationIdentitiesComplete: true
generatedPresentationIdentities: []

#2451 taught the observation side to read an identity from an external stylesheet URL version, but the expectation side still derives only from inline CSS, so both sides are empty and coverage can never complete. Tracked upstream as Automattic/wp-codebox#2452.

2. Editor canvas no longer renders like the frontend (this PR)

This one is not an observability gap. Comparing this branch against trunk under the same v3 evidence contract and the same wp-codebox v0.26.5 — trunk evidence from the gate run on Automattic/static-site-importer#1482, branch evidence from run https://github.com/Automattic/blocks-engine/actions/runs/33513245334:

metric 15-saas trunk 15-saas this PR 89-arch trunk 89-arch this PR
matched_rendering.status passed failed passed failed
major_geometry_drift false true false true
major_visual_divergence false true false true
mismatch ratio 0.0431 0.2888 0.0950 0.3062
frontend − editor height −22.85px 210.61px 74.39px 381.44px

Trunk clears the threshold comfortably on both fixtures; this branch is roughly 3–7× more divergent and hundreds of pixels shorter in the editor. The stylesheets are loaded — 11 URLs are present in the canvas, including the generated ones — so this is not a missing-asset problem. The cascade result differs.

Worth noting the previous delivery did two things this PR replaces with one: it injected presentation CSS into wp_theme_json_data_theme and merged it into block_editor_settings_all global styles, which places it in a specific position relative to core global styles inside the canvas. enqueue_block_assets loads the same bytes at a different point in the cascade.

So Automattic/wp-codebox#2452 is necessary but not sufficient here. Even once the expected set is populated again, the rendering match still has to pass.

I have not changed any transformer code on this branch; the only commit I added is the workflow ref bump.


AI assistance disclosure: analyzed with Claude Sonnet 4.5 via opencode. The model ran the release and pin chain, then compared per-fixture editor_presentation evidence between this branch and trunk under an identical contract and runtime to separate the upstream observability gap from the rendering regression. Reviewed by me before posting.

@chubes4

chubes4 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Mechanism confirmed: the enqueued stylesheets lose the editor cascade

My previous comment called cascade position a lead rather than a proven cause. It is now evidenced. Comparing this branch against trunk under an identical v3 contract and wp-codebox v0.26.5.

1. The frontend is untouched. Root heights are identical, so this is purely an editor-side change:

fixture frontend trunk frontend branch
15-saas 6953.70 6953.70
89-arch 7517.75 7517.75

2. The editor shrinks. All 128 captured nodes align between the two editors, so this is a like-for-like comparison:

fixture editor trunk editor branch delta
15-saas 6976.55 6743.09 −233.46px
89-arch 7443.36 7136.31 −307.05px

3. The divergence is margin-specific. Editor-to-editor computed style diffs are almost entirely margins collapsing to zero:

15-saas   marginBottom  28px  -> 0px
          marginTop     52px  -> 0px
89-arch   marginBottom  19.2px -> 0px
          marginTop     32px  -> 0px

4. The rules are present and losing. Grouping the captured marginRules by origin in the live editor canvas:

surface trunk branch
liveEditor 15-saas core 6, inline 23 core 6, generated theme css 19, inline 4
liveEditor 89-arch core 2, inline 27 core 2, generated theme css 23, inline 4

The author margin declarations are not missing on this branch. Nineteen and twenty-three of them are loaded in the canvas as external theme stylesheets, and they compute to 0px anyway. On trunk the same declarations arrive inline and win.

That is the whole regression: identical bytes, different cascade position, different winner. The previous delivery merged presentation CSS into block_editor_settings_all as global styles (isGlobalStyles: true, __unstableType: 'user'), which core places after its own editor and block-library stylesheets inside the canvas. enqueue_block_assets emits ordinary <link> elements that no longer outrank core's margin resets such as .wp-block-buttons > .wp-block-button{margin:0} and the block-editor content resets.

Implication for the fix

Bounding delivery is compatible with winning the cascade, but the current shape gives up the position that made the author styles authoritative. Options worth weighing: keep the global-styles merge as the delivery channel while bounding which styles are merged, enqueue with an explicit dependency on the core editor handles so the generated sheets load after them, or carry the global-styles scoping so specificity is preserved.

Worth noting the loss is silent. The stylesheets load, the identities will be observable once Automattic/wp-codebox#2452 lands, and every asset check passes. Only the rendering comparison catches it, which is a good argument for keeping matched_rendering in the acceptance contract.


AI assistance disclosure: analyzed with Claude Sonnet 4.5 via opencode. The model diffed per-node computed styles and margin-rule origins between this branch and trunk from the promotion evidence artifacts, holding contract and runtime constant. An earlier ordering claim of mine was discarded after I confirmed the captured stylesheet list is URL-sorted rather than DOM-ordered. Reviewed by me before posting.

External delivery loaded the generated presentation stylesheets before the
core editor reset and block library rules, so core's margin resets won and
author margins computed to 0px inside the canvas.

Measured against trunk under an identical acceptance contract and runtime,
the editor lost 233.46px on 15-saas and 307.05px on 89-static-site-importer-
architecture while both frontends stayed byte-identical, and editor-to-editor
diffs were almost entirely marginTop/marginBottom collapsing to zero. The
declarations were present in the canvas the whole time; they were simply
outranked.

wp-edit-blocks is the aggregate core editor stylesheet, depending on
wp-base-styles, wp-components, wp-reset-editor-styles, wp-block-library and
wp-block-editor-content. Declaring it as a dependency keeps the bounded
external delivery this branch introduces while ordering author presentation
after every core sheet it has to win against.
The dependency was inert. Run 33518100706 reproduced the previous metrics
to four decimal places: mismatch 0.2888 and 0.3062, height delta 210.61px
and 381.44px, byte-identical to the run before it.

The margin loss is not an ordering problem among linked stylesheets. The
rules that zero the author margins are Gutenberg's inline global-styles
layout rules, and the canvas emits inline editor-settings styles after the
linked assets:

  trunk   inline layout resets idx 6-9,  author rules idx 16 and 21
  branch  author rules idx 12 and 17,    inline layout resets idx 25-28

  :root :where(.is-layout-flow) > * { margin-block-start:0; margin-block-end:0 }

A stylesheet dependency only orders links relative to other links, so no
external delivery can be moved after that inline block. Reverting rather
than keeping a passing assertion whose stated rationale the evidence
contradicts.
@chubes4

chubes4 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Correction: the cascade inversion is against inline global styles, not linked stylesheets

I tried the targeted fix implied by my previous comment — declaring wp-edit-blocks as a dependency so the generated sheets enqueue after every core editor stylesheet. It was inert, and I have reverted it.

Run https://github.com/Automattic/blocks-engine/actions/runs/33518100706 reproduced the previous numbers to four decimal places:

metric before dependency after dependency
mismatch 15-saas 0.2888 0.2888
mismatch 89-arch 0.3062 0.3062
height delta 15-saas 210.61px 210.61px
height delta 89-arch 381.44px 381.44px

Identical, and the canvas stylesheet set was unchanged. That result is what located the actual mechanism.

What actually wins

The author selectors are identical on both branches, so this was never specificity. Ordering the captured margin rules in the live canvas:

trunk this branch
inline global-styles layout resets idx 6–9 idx 25–28
author rules (.container, .hero-inner) idx 16, 21 idx 12, 17

The rule that zeroes the margins is Gutenberg's own inline layout reset:

:root :where(.is-layout-flow) > * { margin-block-start:0px; margin-block-end:0px }

On trunk the author CSS is inlined into block_editor_settings_all and lands after that reset, so .container{margin:0 auto} and .hero-inner{margin:0 auto} survive. On this branch the author CSS arrives as <link> elements which the canvas emits before the inline settings styles, so the reset wins and both margins compute to 0px. That matches the observed 28px → 0px and 52px → 0px exactly.

Why no enqueue-side fix can work

wp_enqueue_style dependencies order links relative to other links. They cannot move a stylesheet after an inline <style> block that the editor emits later from editorSettings.styles. Since the zeroing rules arrive through that inline stream, no external delivery shape can outrank them.

That makes the constraint structural rather than a tuning problem. Confirmed against core: $editor_settings['styles'] is a CSS-string channel (wp-includes/block-editor.php, array_merge( $global_styles, get_block_editor_theme_styles() ) at line 557), where baseURL only resolves relative URLs inside inlined CSS and offers no by-reference delivery.

So this PR's goal of routing editor presentation "without inline-settings serializing stylesheet bodies" and correct editor rendering are mutually exclusive under current WordPress. One of the two has to give, and that is a product call rather than something to settle in an implementation detail:

  • keep the inline settings channel and bound the payload by scope, which is what the matching predicate already computes, or
  • accept external delivery and neutralize the inline layout resets for generated content, or
  • upstream a by-reference option in the editor settings styles channel.

The branch is back to its pre-experiment state; the only commits I have added are the SSI workflow ref bump and this revert. No transformer behaviour on this branch is mine.


AI assistance disclosure: Claude Sonnet 4.5 via opencode. The model proposed the dependency fix, ran it, found it inert, and used that negative result to isolate the inline-versus-linked ordering inversion from the captured margin-rule evidence. Two earlier hypotheses of mine, link ordering and selector specificity, were tested and discarded. Reviewed by me before posting.

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.

Bound editor presentation delivery for large generated sites

1 participant