Files
hyperframes/docs/packages
Miguel Ángelandmiga-heygen e8659cb653 fix(cli): recognize textual motion in sweep checks (#3707)
* fix(cli): fingerprint textual sweep state

* fix(cli): share one motion classifier between sweep guard and liveness

The frozen-sweep guard (sweep_static) and keepsMoving liveness (motion_frozen)
each owned their own definition of "what counts as motion": the sweep
fingerprint in layout-audit.browser.js read nine channels while
motion-sample.browser.js compared box + opacity only. They meet in one decision
in checkPipeline.ts, so a fixed-width attr() countdown was accepted by the
sweep guard and still reported frozen by liveness.

Move the classifier into motion-signature.browser.js, injected before both
samplers, and make it the single owner:

- Per-element channels (box, opacity, font-variation axes, clip-path, direct
  text, form-control state, ::before/::after content, media pixels) as a
  reader list so further channels (e.g. SVG dash) plug in.
- Liveness consumes the same signature with 2px / 0.08 bucketing; the sweep
  guard consumes it exact. motion-sample throws at install if the shared
  script is missing.
- Counter state is a composition-level channel over box-generating elements
  and root ancestors. display:none subtrees generate no boxes and are
  excluded, and only declarations naming a counter that some painted
  generated content (::before/::after/::marker on a non-ignored, paintable
  host) consumes are folded in, so a decoy hidden counter cannot make a
  frozen composition read as live.
- clip-path is a channel, replacing the fingerprint's former reliance on the
  clipped-away visibility probe, so a wipe over a static box still counts.
- data-layout-ignore layers are excluded from the signature but not from the
  per-selector `visible` bit reported for explicit motion assertions.
- Root resolution for the "*" liveness scope now matches the sweep guard and
  the root element is part of its own signature.

Tests: motion-signature.browser.test.ts (happy-dom, per-element fake computed
style so ancestor handling comes from the classifier) and
motion-signature.browser.chromium.test.ts (real Chromium via puppeteer-core;
skips without a system browser) pin every branch: attr() substitution,
display:none decoys, named-counter gating, ::marker, zero-box hosts, ancestor
counters, textarea/indeterminate, root text, clip-path, ignore layers, and
sweep/liveness agreement on the countdown fixture.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* fix(cli): prune unrendered content from the motion signature and measure asserted scopes

The shared motion classifier pruned only display:none subtrees, so text,
pseudo content, media pixels, and counter decoys inside skipped
content-visibility contents changed the frozen-sweep and liveness
signatures although nothing painted. Pruning is now platform-owned where
checkVisibility exists (display:none, skipped hidden/auto contents; a
display:contents host keeps its painting pseudo-elements), with the
computed-style fallback for happy-dom. A content-visibility:hidden host
signs its own box, opacity, clip-path, font axes, and widget state, but
not its text, pseudo content, control value, or media pixels; hosts where
size containment does not apply (non-atomic inline, display:contents,
table and internal table boxes, ruby) skip nothing. isVisibleElement
regains the checkVisibility floor layout-audit uses on its opacity-floor
path, so an asserted selector inside skipped contents reports hidden.

The layout-audit opt-out no longer applies to the element passed as the
signature root: a keepsMoving scope that is itself data-layout-ignore is
measured (an assertion naming an element outranks the opt-out) instead of
producing an empty signature that motionAudit reports as a missing
selector. Opt-outs inside the scope still apply.

motion-sample defers the missing-classifier error to the first call so
the driver's evaluate surfaces it instead of an opaque "is not a
function" page error; round() is shared.

The real-Chromium suite is opt-in on Windows (HYPERFRAMES_BROWSER_TESTS=1):
in the shared Windows package lane the runner's Chrome did not launch
within the package hookTimeout, and the suite already runs on Linux CI
where the system Chrome is found. The launch hook has its own 120s ceiling.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* fix(cli): prune display:contents in skipped contents; table-cell skips, caption paints

A display:contents child of a content-visibility:hidden host has no box
of its own, so the platform check could not tell it apart from an
ordinary display:contents owner and it stayed a counter consumer: a
change of the owner's counter-reset altered the sweep signature although
nothing paints. The parent's skipsContents verdict now prunes it; boxed
descendants keep being decided by the platform.

NOT_CONTAINABLE_DISPLAY had table-cell and table-caption inverted
versus Chromium 152, where a table-cell host skips its contents and a
table-caption host paints them.

Four Chromium pins: the display:contents-in-skipped-contents counter, a
table-cell host's own text and pseudo content (frozen), a table-caption
host's own text and pseudo content (motion), and a counter reachable
only through a skipped host's hidden ::marker / ::after (frozen; also
asserts the skipped child is not visible for motion-sample).

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* chore(cli): trim sweep motion comments to the comment ratchet

* chore(cli): shorten sweep motion comments to the merge-hook limit

---------

Co-authored-by: miga-heygen <miguel.sierra_miga@heygen.com>
2026-10-02 06:25:18 -07:00
..