Repository navigation
fix(cli): sweep_static geometry fingerprint now covers SVG stroke/dash motion - #3914
Closed
miga-heygen wants to merge 6 commits into
Closed
miga-heygen wants to merge 6 commits into
miga-heygen wants to merge 6 commits into
Conversation
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>
Contributor
Author
|
Addressed in b8abfc7. |
miga-heygen
force-pushed
the
fix/sweep-static-svg-dash
branch
from
September 14, 2026 18:26
f35dfe3 to
b8abfc7
Compare
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
miga-heygen
changed the base branch from
main
to
magi/fix-sweep-static-text-fingerprint
September 14, 2026 18:26
…ure 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>
Contributor
Author
miga-heygen
force-pushed
the
fix/sweep-static-svg-dash
branch
from
September 15, 2026 01:19
b8abfc7 to
582018d
Compare
…s, 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>
Contributor
Author
miga-heygen
force-pushed
the
fix/sweep-static-svg-dash
branch
from
September 15, 2026 02:24
582018d to
5f4196b
Compare
A "draw the line in" SVG entrance animates stroke-dasharray / stroke-dashoffset on a shape whose geometry never changes, so the box+opacity channels read it as a frozen timeline and `hyperframes check` false-positives sweep_static on a visibly animating composition. - Append strokeDashChannel to ELEMENT_CHANNELS: hashes the computed dash pattern and offset for SVG geometry elements with a painted stroke; `none` / all-zero patterns, unpainted (none, transparent, zero-width, stroke-opacity 0) strokes and non-SVG elements contribute "". - Widen isVisibleElement for stroked geometry with one degenerate bbox axis: Chromium's getBoundingClientRect for SVG shapes excludes the stroke, so a straight horizontal or vertical connector reports 0 height or 0 width and was dropped by the box gate — the most common shape for this animation. motion-sample's `visible` bit for such connectors is corrected as a consequence. - Treat <defs>, <clipPath> and <symbol> as never-painted subtrees in the hidden-subtree walk (display is not inherited, so descendants are hidden only through the ancestor walk) rather than relying on one engine's empty-box behaviour for their descendants. Tests: happy-dom pins for the degenerate-bbox connector (moves / frozen), a display:none ancestor, <defs>/<clipPath> content and a stroke-opacity:0 decoy; real-Chromium pins for the connector draw-in and the ignored cases. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
miga-heygen
force-pushed
the
fix/sweep-static-svg-dash
branch
from
September 15, 2026 02:34
5f4196b to
95624bc
Compare
Contributor
Author
|
Updated as 95624bc: SVG container set and guard. |
Follow-up coverage for the SVG stroke-dash motion channel:
- stroke-dasharray changes at a fixed stroke-dashoffset now change both
the exact sweep signature and the quantized liveness signature
(happy-dom and Chromium)
- one table row per guard: stroke-opacity 0, a transparent stroke,
stroke-width 0, and all-zero dash lists ("0", "0 0") each leave the
signature unchanged under a moving offset
- a dashed stroke inherited by <g> / <text> (not SVGGeometryElement) is
not signed
- a source-parity test asserts UNPAINTED_SVG_CONTAINERS and layout-audit's
CONNECTOR_SKIP_CONTAINERS name the same containers
- the container comment no longer claims motion inside a referenced
<mask> / <pattern> is attributed to the referencing element; its own
channels are, the referenced content has none
Mutation-checked: hashing the offset only, dropping each stroke/dash
guard, widening the element filter to SVGElement, and removing a
container name each fail exactly their own rows in both suites.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
Contributor
Author
|
Addressed in 434d1e0. |
Base automatically changed from
magi/fix-sweep-static-text-fingerprint
to
main
October 2, 2026 13:25
Collaborator
|
Closing as stale after a few weeks without activity. Reopen after rebasing on main if still needed. |
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.
Summary
hyperframes check's seek-audit firessweep_static("Timeline did not advance under seek") when the per-sample motion signature is byte-identical across the whole run. An SVGstroke-dasharray/stroke-dashoffset"draw the line in" entrance — the standard way to animate a connector or diagram line — moves no bounding box and no opacity, so a genuinely animating composition using it was flagged as a frozen timeline.With #3707 the signature has a single owner (
packages/cli/src/commands/motion-signature.browser.js,ELEMENT_CHANNELS), shared by the frozen-sweep guard andkeepsMovingliveness. This PR:strokeDashChanneltoELEMENT_CHANNELS. Hashes the computed dash pattern and offset for SVG geometry elements (SVGGeometryElement: path/circle/ellipse/rect/line/polyline/polygon) with a painted stroke.noneand all-zero patterns (which render solid), unpainted strokes (none, transparent, zero width,stroke-opacity: 0) and non-SVG elements contribute"", so ordinary compositions gain no payload.isVisibleElementfor stroked geometry with one degenerate bbox axis. Chromium'sgetBoundingClientRectfor SVG shapes is the object bounding box without the stroke: a straight horizontal or vertical connector reports 0 height or 0 width regardless ofstroke-width, so the box gate dropped exactly the most common shape for this animation. A stroked shape with one non-degenerate axis now counts as visible. As a consequencemotion-sample'svisiblebit for such connectors is corrected too (it previously reported them invisible).<defs>,<clipPath>,<mask>,<pattern>,<marker>and<symbol>as never-painted subtrees in the existing hidden-subtree walk (the same container listlayout-audit.browser.jsskips for connectors).displayis not inherited — a child of adisplay:noneparent computesdisplay: block— so descendants are excluded only through the ancestor walk, which now also starts at these containers rather than relying on one engine's empty-box behaviour for their descendants. The match is namespaced (instanceof SVGElement), so an HTML element that merely shares a name is not pruned.Test plan
motion-signature.browser.test.ts(happy-dom, 43 passing): dash-offset moves on a 290×0 connector change the signature, and so does astroke-dasharraychange at a fixed offset (both the exact sweep and the quantized liveness signature); identical scene hashes identically; dash motion under adisplay:noneancestor and inside<defs>/<clipPath>/<mask>/<pattern>/<marker>/<symbol>is ignored; one table row per channel guard —stroke-opacity: 0,stroke: rgba(0, 0, 0, 0),stroke-width: 0,stroke-dasharray: 0and0 0(solid) — leaves the signature unchanged under a moving offset; a dashed stroke inherited by a<g>/<text>(notSVGGeometryElement) is not signed; a fade inside an HTML element named<defs>still changes the signature. A source-parity test extractsUNPAINTED_SVG_CONTAINERSand layout-audit'sCONNECTOR_SKIP_CONTAINERSfrom the two files and asserts the name sets are equal (case-insensitive), so the hand-kept copy cannot drift silently. Fixtures use the real Chromium bbox of a horizontal stroked path (height 0) so they pin the widened gate, not the pre-existing box path.motion-signature.browser.chromium.test.ts(real Chromium, 44 passing): astroke-dashoffsetdraw-in and a fixed-offsetstroke-dasharraychange on a straight connector each change both the sweep and the liveness signature; dash motion underdisplay:noneand inside all six unpainted containers leaves both identical; the same five guard rows as the happy-dom suite (against Blink's computed values —stroke: transparentcomputes torgba(0, 0, 0, 0)) and the<g>/<text>case leave both identical; a fade inside an HTML<defs>element changes both signatures.motion-sample.browser.test.ts(happy-dom, 8 passing) unchanged.<path>reports a 290×0 box;display:nonedescendants computedisplay: inline; descendants of all six containers (<defs>,<clipPath>,<mask>,<pattern>,<marker>,<symbol>) report 0×0 whilecheckVisibilityreturns true for them, so the container rule is what excludes them by intent rather than the box gate; computedstroke-dashoffsetis rounded (144.99999999px→145px), so the signature cannot vary on its own at a fixed seek.stroke: transparent/stroke-opacity: 0counting as painted;stroke-dasharray: 0(solid) counting as a pattern; tag-name matching hitting HTML unknown elements (replaced byinstanceof SVGGeometryElement).<mask>/<pattern>/<marker>missing from the never-painted set although they meet its definition (and the widened bbox gate would otherwise admit a straight connector inside them); the tag-name match lacking a namespace guard.stroke-width > 0guard, or the all-zero-is-solid guard, widening the element filter toSVGElement, and removing a name fromUNPAINTED_SVG_CONTAINERSeach fail exactly their own row(s) in both suites.oxlint,oxfmt --check, andtsc --noEmitclean forpackages/cli. The happy-dom suites cannot be collected directly in the local sandbox under bun (No such built-in module: node:from the@vitest-environment happy-domloader); they were run with--environment nodeplus a setup file that populates globals from a happy-domWindow, and pass. CI runs them as-is.Deliberately out of scope: dashing on
<text>/<use>(no channel today); other "moves nothing else" SVG reveals (fill-opacity,stroke-opacity, gradientstop-opacity); animation of the content of a<mask>/<pattern>/<clipPath>/<marker>referenced by an otherwise static element (it changes pixels through the referencing element, whose own channels do not move — and Blink gives that content 0×0 boxes, so it was never signed); andpathLengthattribute animation (rescales the dash pattern with no computed-style change) — this closes the stroke/dash gap only.