Skip to content

refactor: neutral capture vocabulary for Blocks Engine capture evidence - #2725

Merged
chubes4 merged 4 commits into
trunkfrom
refactor/neutral-capture-vocabulary
Oct 13, 2026
Merged

chubes4 merged 4 commits into
trunkfrom
refactor/neutral-capture-vocabulary

Conversation

@chubes4

@chubes4 chubes4 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Refs Automattic/data-liberation-agent#787 (part of Automattic/data-liberation-agent#761).

Important

LOCKSTEP: merge together with Automattic/data-liberation-agent#788 and Automattic/static-site-importer#2062 (data-liberation-agent#787 option A).
All three switch to the new names only. No repository has compatibility code. Blocks Engine reads only the names below.

What changes

Blocks Engine no longer knows any specific capture producer. It owns a producer-neutral capture-evidence input contract that any capture tool can emit:

  • php-transformer/src/Contract/CaptureEvidence.php: one constants class with all 63 names plus the 2 recognized runtime digests. Every reader and emitter in src/ uses it. Literals inside embedded JS modules are filled from the constants through the existing __PLACEHOLDER__ substitution, and regexes and XPath queries are built from them.
  • php-transformer/docs/contracts/capture-evidence.md: every name with its kind, value grammar and meaning.
  • Old names are not read any more. Blocks Engine has no compatibility path for them; only the producer writes both sets during rollout.
  • The pass-through aliases NativeControlState::ATTRIBUTE, CapturedGlyphColor::ATTRIBUTE and DocumentVariantIds::{MOBILE,DESKTOP}_DOCUMENT_CLASS are deleted. Callers use CaptureEvidence directly. No downstream code references the aliases.
  • packages/blocks-engine: the parity goldens, corpora, tests and golden-capture scripts get neutral names (*-dla-goldens.json → *-reference-goldens.json, DLA_* → REFERENCE_*). The scripts now require REFERENCE_ROOT and no longer fall back to a hardcoded path. The emitted scroll-state block dla/sticky becomes blocks-engine/sticky; no repository registers either name.

Mapping rule, and one deliberate deviation

Old New
data-dla-<name> data-blocks-engine-capture-<name>
data-liberation-<name> data-blocks-engine-capture-<name>
bare data-liberation (sidecar schemas) blocks-engine-capture (for example blocks-engine-capture/capture-receipt/v1)
class / token dla-<name>, including the --dla-<variant> id suffix data-blocks-engine-capture-<name>

Deviation from #787's class rule (dla-<name> → blocks-engine-capture-<name>). I ran the semantic comparison below against unmodified trunk, and it showed that the literal rule loses behavior. About 20 places in conversion treat any blocks-engine-* class as an engine-generated class: they strip it from source identity, skip it for naming, and don't project its styles. Classes such as blocks-engine-capture-dropdown were therefore no longer handled as source classes. In the comparison, dropdown/dialog/disclosure wrappers lost a block level and component names changed. With every capture class under the attribute prefix, conversion treats them as source classes and that loss goes away. This puts the whole vocabulary under one prefix, data-blocks-engine-capture-, with schemas under blocks-engine-capture/. The producer side of #787 should use this rule.

Values and semantics are unchanged. The two runtime-identity digests do change, because they hash script bodies that contain the names. CaptureEvidence::*_RUNTIME_SHA256 is the digest of the renamed body, and both bodies are published as tests/fixtures/capture-{disclosure,gallery}-runtime.js.

Published for downstream importers. Blocks Engine does not read these names, but it publishes them in CaptureEvidence so Static Site Importer and producers share one spelling. They are flagged in CaptureEvidence::PUBLISHED_FOR_DOWNSTREAM, and the doc gives them their own section. The vocabulary contract allows flagged names to go unused. It fails if a flagged name is ever read by src/, or if an unflagged name is unused.

Old New Kind
data-dla-device-selection data-blocks-engine-capture-device-selection attribute (flag on <script>)
data-dla-device-body data-blocks-engine-capture-device-body attribute (flag on <script>)
data-dla-selected-document data-blocks-engine-capture-selected-document attribute on <html>
data-dla-selected-viewport data-blocks-engine-capture-selected-viewport attribute (flag on viewport meta)
data-dla-document-unavailable data-blocks-engine-capture-document-unavailable attribute (flag on <html>)
data-dla-device-style data-blocks-engine-capture-device-style attribute on <style>/<link>
data-dla-member-login data-blocks-engine-capture-member-login attribute
class dla-member-login-<provider> class data-blocks-engine-capture-member-login-<provider> class prefix

Runtime scripts. The doc's "Runtime scripts" section says producers must emit the disclosure and gallery helpers exactly as published:

  • the disclosure helper is tests/fixtures/capture-disclosure-runtime.js, sha256 3d350f55…;
  • the gallery helper is tests/fixtures/capture-gallery-runtime.js, sha256 0a6bf62a….

It also explains how CapturedDialogProjector checks them: it compares sha256(trim(textContent)) for each marked script, and declines native replacement on any mismatch or any other executable script.

Full mapping

Old New Kind Blocks Engine role
data-dla- data-blocks-engine-capture- attribute prefix input
--dla- --data-blocks-engine-capture- id suffix prefix input + output (suffixed ids)
data-dla-document-scope data-blocks-engine-capture-document-scope attribute input
data-dla-device-document data-blocks-engine-capture-device-document attribute input
data-liberation-mobile-document data-blocks-engine-capture-mobile-document class input
data-liberation-desktop-document data-blocks-engine-capture-desktop-document class input
data-liberation-responsive-counterpart- data-blocks-engine-capture-responsive-counterpart- class prefix input
data-dla-responsive-source data-blocks-engine-capture-responsive-source attribute input
data-dla-source-media data-blocks-engine-capture-source-media attribute input
data-dla-dialog-trigger data-blocks-engine-capture-dialog-trigger attribute input
data-dla-dialog-panel data-blocks-engine-capture-dialog-panel attribute input
data-dla-dialog-close data-blocks-engine-capture-dialog-close attribute input
data-dla-dialog-panel-state data-blocks-engine-capture-dialog-panel-state attribute input
data-dla-dialog-ancestor-state data-blocks-engine-capture-dialog-ancestor-state attribute input
data-dla-dialog-ancestor-unverified data-blocks-engine-capture-dialog-ancestor-unverified attribute input + output (native dialog)
data-dla-observed-placement data-blocks-engine-capture-observed-placement attribute input
dla-dropdown data-blocks-engine-capture-dropdown class input
dla-dialog data-blocks-engine-capture-dialog class input
dla-disclosure data-blocks-engine-capture-disclosure class input
data-dla-disclosure-runtime data-blocks-engine-capture-disclosure-runtime attribute input
data-dla-disclosure-label data-blocks-engine-capture-disclosure-label attribute input
data-dla-disclosure-open- data-blocks-engine-capture-disclosure-open- attribute prefix input
data-dla-disclosure-closed- data-blocks-engine-capture-disclosure-closed- attribute prefix input
data-dla-local-disclosure data-blocks-engine-capture-local-disclosure attribute input
data-dla-local-disclosure-runtime data-blocks-engine-capture-local-disclosure-runtime attribute input
data-dla-exclusive-disclosures data-blocks-engine-capture-exclusive-disclosures attribute input
data-dla-listbox-trigger data-blocks-engine-capture-listbox-trigger attribute input + internal
data-dla-listbox-panel data-blocks-engine-capture-listbox-panel attribute input + internal
data-dla-gallery data-blocks-engine-capture-gallery attribute input
data-dla-gallery-sequence data-blocks-engine-capture-gallery-sequence attribute input
data-dla-gallery-slot data-blocks-engine-capture-gallery-slot attribute input
data-dla-gallery-stage data-blocks-engine-capture-gallery-stage attribute input
data-dla-gallery-initial data-blocks-engine-capture-gallery-initial attribute input + internal
data-dla-gallery-display data-blocks-engine-capture-gallery-display attribute input
data-dla-gallery-direction data-blocks-engine-capture-gallery-direction attribute input
data-dla-gallery-index data-blocks-engine-capture-gallery-index attribute output
data-dla-gallery-source data-blocks-engine-capture-gallery-source attribute input
data-dla-gallery-capture-width data-blocks-engine-capture-gallery-capture-width attribute input
data-dla-gallery-runtime data-blocks-engine-capture-gallery-runtime attribute input
data-dla-collection data-blocks-engine-capture-collection attribute input
data-dla-collection-field data-blocks-engine-capture-collection-field attribute input
data-dla-collection-category-control data-blocks-engine-capture-collection-category-control attribute input
data-dla-collection-index data-blocks-engine-capture-collection-index attribute input
data-dla-collection-empty data-blocks-engine-capture-collection-empty attribute input
data-dla-collection-item data-blocks-engine-capture-collection-item attribute input
data-dla-collection-members data-blocks-engine-capture-collection-members attribute input
data-dla-collection-status data-blocks-engine-capture-collection-status attribute input
data-dla-collection-runtime data-blocks-engine-capture-collection-runtime attribute input
data-dla-collection-visibility data-blocks-engine-capture-collection-visibility attribute input
data-dla-status-template data-blocks-engine-capture-status-template attribute input + output (filter runtime)
data-dla-status-hide-zero data-blocks-engine-capture-status-hide-zero attribute input
dla-no-match data-blocks-engine-capture-no-match probe token input
dla-finite-probe data-blocks-engine-capture-finite-probe probe token input
data-dla-choice-index data-blocks-engine-capture-choice-index attribute input
data-dla-native-control-state data-blocks-engine-capture-native-control-state attribute input
data-dla-native-control-runtime data-blocks-engine-capture-native-control-runtime attribute input
data-dla-glyph-color data-blocks-engine-capture-glyph-color attribute input
data-dla-anchor-unresolved data-blocks-engine-capture-anchor-unresolved attribute input
data-liberation/captured-interactions/v1 blocks-engine-capture/captured-interactions/v1 sidecar schema input
data-liberation/capture-receipt/v1 blocks-engine-capture/capture-receipt/v1 sidecar schema input
data-liberation/source-interactivity/v1 blocks-engine-capture/source-interactivity/v1 sidecar schema input
data-liberation/finite-bootstrap/v1 blocks-engine-capture/finite-bootstrap/v1 sidecar schema input
data-liberation/collection-status/v1 blocks-engine-capture/collection-status/v1 sidecar schema input
db750332…8624614 (disclosure interpreter sha256) 3d350f55…b70ce75dc runtime identity input
1321b380…5a649c5a7 (gallery helper sha256) 0a6bf62a…bf29f723aa runtime identity input
wp:dla/sticky, wp-block-dla-sticky wp:blocks-engine/sticky, wp-block-blocks-engine-sticky block name (packages/blocks-engine) output
*-dla-goldens.json, page-reconstruct-dla-baseline.*, theme-reconstruct-dla-baseline-parity.test.ts *-reference-goldens.json, page-reconstruct-reference-baseline.*, theme-reconstruct-reference-baseline-parity.test.ts fixtures/tests tooling
DLA_* constants, DLA_ROOT env, dlaSection, dlaSerializeBlockAttrs, "DLA parity" test names REFERENCE_*, REFERENCE_ROOT (required), referenceSection, referenceSerializeBlockAttrs, "reference parity" scripts/tests tooling
tests/fixtures/dla-disclosure-runtime.js tests/fixtures/capture-disclosure-runtime.js (+ new capture-gallery-runtime.js) fixture tooling

"input + internal" means a projector writes the name from sidecar evidence and a converter reads it later in the same compile. Every PHP test fixture's captured HTML, sidecar JSON and assertions were converted with the rule.

Equivalence gate: no loss of power

tests/contract/capture-evidence-equivalence.php runs in composer test.

  • Corpus: I wrapped the public entry points of a disposable origin/trunk (44a1eb5ed, after merging trunk) checkout and ran its full suite (composer test plus every other PHP test script). That recorded 349 distinct calls whose input carried the old names: ArtifactCompiler::compile 93, HtmlTransformer::transform 85, ArtifactNormalizer::normalize 29, CapturedDialogProjector 45, CapturedCollectionProjector 48, CapturedSelectableSetProjector 32, CapturedChoiceGroupProjector 17. Inputs are stored verbatim, with old names. Both harvests used the committed harvest-hook.php; the first, on 84d0853a6, was reproduced byte-identically. The expected outputs were stable across two baseline runs.
  • Comparison: each case is renamed with the published rule, including base64 file payloads, embedded JSON and the declared htmlBytes. It is then replayed through this branch. The expected output comes from a second disposable origin/trunk checkout with the same rule applied to its src/. The result must be byte-identical: compile result + WordPress site plan, projected files, or exception. Only the checkout path and *_duration_ms timings are neutralized.
  • Result: 349/349 byte-identical. I mutated one constant, one literal, and reintroduced one old name; the gate, the vocabulary contract and the guard each failed on the corresponding mutation. Flagging a consumed name as downstream-only, or unflagging a downstream name, also fails the vocabulary contract.

Cross-name semantic report (report only: tools/capture-evidence-equivalence/semantic-report.php). This compares unmodified trunk on the old names with this branch on the new names. It masks what a longer name changes by construction: byte counts, offsets, content digests, and excerpt cut points.

  • 320/349 identical.
  • 12 differ only in where diagnostic source_fragment/snippet excerpts are cut.
  • 16 differ only in the order of the hash-keyed reusable_components.components diagnostic list.
  • 1 is a diagnostic-only budget crossing. A reusable-component structural signature passes the 8 KiB cap because the names are longer, so candidate_count goes 21→20 in that report.
  • No difference in blocks, serialized markup, site plans, assets or finding codes.

Guards

  • tests/contract/capture-evidence-vocabulary.php:
    • no PHP string in src/ outside CaptureEvidence spells a capture name (checked by tokenizer);
    • every constant is used and none is undefined;
    • the doc lists exactly the published names;
    • the runtime digests match the published bodies.
  • tests/contract/producer-neutral-source.php (composer test) and packages/blocks-engine/src/__tests__/producer-neutral-source.test.ts (vitest): fail on dla / data-dla / data-liberation / liberation-agent, case-insensitive and word-ish (dlaSection and DLA_ROOT fail, childLayout passes). They scan php-transformer/src, packages/blocks-engine/src, and the scripts of composer.json and the package.json files.
  • No throw message changed; the throw-classification fixture is unchanged and its guard passes.

Verification (local, PHP 8.5.5 / Node 24, on the merged head)

  • CI on the earlier head 19fc3b9f3 was all green: PHP Transformer, JS Transformer, JS Fidelity Ratchet and Solved Site Promotion Gate.

  • php-transformer: composer install, composer validate --strict, composer lint (phpcs + phpstan), composer test:lint and composer test all pass. The repo-root tests/contract/production-acceptance-matrix.php passes.

  • Every PHP test script outside composer test exits the same as on trunk.

  • Visual-parity job: npm ci, npm run lint and npm test (with pre/post tests) pass. Both variants of layout-media-tracks.mjs pass. I ran them against a local WordPress 7.1 rather than downloading 7.0.4. The details, collection filter, runtime dialog trigger, dropdown panel, native button background, orphan list item and accordion browser and PHP steps all pass.

  • packages/blocks-engine:

    • pnpm typecheck and pnpm build pass;
    • pnpm test passes 106 files / 508 tests;
    • pnpm test:bundle passes (508);
    • pnpm test:svg-decode passes;
    • wp-runtime.cjs is 21 MB.
  • Left to CI: the WordPress PHPUnit site-plan integration, the stylesheet-activation Docker job, the PHP 8.2–8.4 matrix, and the solved-site promotion workflow.


AI disclosure: written by Claude in OpenCode with Chris Huber (direct implementation authorized by the operator); Claude implemented, tested and opened this PR.

…neutral vocabulary

Publish the capture-evidence input contract as Contract\CaptureEvidence and
docs/contracts/capture-evidence.md. Every attribute, class, id suffix, probe
token, sidecar schema and recognized runtime identity the transformer reads
or emits now comes from that vocabulary under data-blocks-engine-capture-* and
blocks-engine-capture/*; the producer-specific names are no longer read.

Add the equivalence gate (344 harvested pre-move entry-point calls replayed
byte-identically), the vocabulary completeness contract and the
producer-neutral source guard.
…y block neutrally

Rename the reference-implementation parity goldens, corpora, tests and golden
capture scripts away from producer-specific names, emit the scroll-state
block as blocks-engine/sticky, and guard the package source and scripts
against producer names.
…me script contract

Publish the device-selection, device-body, selected-document,
selected-viewport, document-unavailable, device-style and member-login names
that importers read, flagged as downstream-only so the vocabulary contract
keeps every consumed name in use. Document how producers must emit the
disclosure and gallery runtime scripts and how Blocks Engine verifies them.
chubes4 added a commit to Automattic/data-liberation-agent that referenced this pull request Oct 13, 2026
…-findings.json (#787)

Capture now emits the producer-neutral capture-evidence vocabulary
Blocks Engine owns, and nothing else: attributes, classes, ids and probe
tokens use data-blocks-engine-capture-<name>, id suffixes
--data-blocks-engine-capture-<variant>, and sidecar schemas
blocks-engine-capture/<name>/v<n>. Emitters, runtime scripts,
stylesheets, live-page dataset keys and interaction-state selectors are
renamed at the source; the dual-emission pass is removed.

src/lib/capture-vocabulary.ts is the one table of names, schemas and
recognized runtime digests, kept identical to the source by test. The
disclosure interpreter body hashes to Blocks Engine's published digest.

Capture writes website/source-findings.json
(static-site-importer/source-findings/v1) and no longer writes
website/capture-findings.json.

Lockstep with Automattic/blocks-engine#2725 and
Automattic/static-site-importer#2062 (#787 option A).
Move the trunk changes from #2719 and #2720 to the neutral capture vocabulary,
and regenerate the equivalence corpus and expectations from trunk 44a1eb5.
@chubes4
chubes4 merged commit 21717d6 into trunk Oct 13, 2026
13 checks passed
@chubes4
chubes4 deleted the refactor/neutral-capture-vocabulary branch October 13, 2026 05:58
chubes4 added a commit to Automattic/data-liberation-agent that referenced this pull request Oct 13, 2026
…-findings.json (#787) (#788)

* feat(export): write producer-neutral capture names and source-findings.json (#787 step 1)

Export mirrors every data-dla-*, data-liberation-* and dla-* capture name
that reaches website/ HTML or interaction-states.json under the Blocks
Engine capture namespace (data-blocks-engine-capture-*,
blocks-engine-capture-*) with identical values, in one source-located
pass that leaves every other byte unchanged.

Capture also writes website/source-findings.json in Static Site
Importer's neutral schema (static-site-importer/source-findings/v1)
beside capture-findings.json, from the same rows. DLA now owns its
code -> loss_class mapping (SOURCE_FINDING_LOSS_CLASSES).

Refs #787, part of #761.

* refactor!: write only the Blocks Engine capture vocabulary and source-findings.json (#787)

Capture now emits the producer-neutral capture-evidence vocabulary
Blocks Engine owns, and nothing else: attributes, classes, ids and probe
tokens use data-blocks-engine-capture-<name>, id suffixes
--data-blocks-engine-capture-<variant>, and sidecar schemas
blocks-engine-capture/<name>/v<n>. Emitters, runtime scripts,
stylesheets, live-page dataset keys and interaction-state selectors are
renamed at the source; the dual-emission pass is removed.

src/lib/capture-vocabulary.ts is the one table of names, schemas and
recognized runtime digests, kept identical to the source by test. The
disclosure interpreter body hashes to Blocks Engine's published digest.

Capture writes website/source-findings.json
(static-site-importer/source-findings/v1) and no longer writes
website/capture-findings.json.

Lockstep with Automattic/blocks-engine#2725 and
Automattic/static-site-importer#2062 (#787 option A).

* fix(capture): rename the dialog changes from main and re-pin the disclosure runtime digest
chubes4 added a commit that referenced this pull request Oct 15, 2026
…2855)

The gate proved the move to the neutral capture vocabulary (#2725) lost no
behavior. That move is merged; since then every intentional output change
has had to re-record its digests by hand.
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