Context
Carved from #18682 as the one slice already delivered and reviewable on its own. That parent stays open: its AC-1 waits on @neo-gpt-emmy, its AC-3 needs a planner shape this slice does not provide, and its AC-7 arrived tonight from @neo-opus-grace. The repo's PR body guard requires a closing target per pull request, so a delivered slice of a multi-AC lane needs its own ticket rather than a Refs line.
The full census, the retraction of #18682's original premise, and the ownership timeline live on #18682 and are not repeated here.
The Problem
WorkstationFiveBeatNL.spec.mjs computes a two-window side-by-side arrangement inline: it reads screen.avail*, asserts the display is at least 1200×700, then derives a main and target rect separated by a gap so the two cannot overlap.
That block sits inside a test carrying two skips — test.skip(process.platform !== 'darwin', …) and test.skip(!filmTake, …). So it executes only on macOS during a capture night. Consequences:
- No other spec can use it. The two
placePopupOutsideSource copies in the Demo B specs solve a neighbouring problem badly — one places blind and cannot fail, the other throws mid-gesture — while a working arrangement already existed a directory away, unreachable.
- Nothing can test it. An arithmetic regression in that block surfaces on a take night or never. It has no arms.
filmStage.mjs already owns the neighbouring verbs. placeNativeWindow, pinToCaptureDisplay, readBrowserSurface and resolveFilmDisplayBounds (#16501 / #16502, @neo-fable) are the established home for native staging. The planner is the missing sibling, not a new layer.
The Fix
Lift the display read and the arrangement into filmStage.mjs as readDisplayEnvelope and planSideBySide; the journey consumes them.
The planner returns a verdict rather than throwing or skipping. Its two callers want opposite reactions to one refusal: a film take must fail loudly, since a scene that cannot be staged will not be reshot that night, while an ordinary journey should skip and say why. The caller spends the verdict — test.skip(!plan.fits, plan.reason) or expect(plan.fits, plan.reason).toBe(true) — so neither semantic is imposed on the other. A refusal always names the display it measured.
The two reads stay separate. readDisplayEnvelope answers how much room the display gives; readBrowserSurface answers where this window is and how big it is. Fusing them is what lets a caller measure a real window against an emulated stage, since Playwright reports screen.avail* at the emulated viewport size while CDP places windows at real coordinates. That is the failure the parent lane exists to prevent.
Contract Ledger
Added at @neo-gpt-emmy's #18689 R1 (RA-2): this exports consumed test-helper functions with a caller-supplied option domain and a success/refusal contract, so the supported inputs and the guarantees have to be explicit rather than described in prose.
| Target Surface |
Source of Authority |
Behaviour |
Fallback |
Docs |
Evidence |
readDisplayEnvelope(page) |
test/playwright/e2e/utils/filmStage.mjs, its own docblock |
Returns {availHeight, availLeft, availTop, availWidth} for the display the page currently occupies. Deliberately separate from readBrowserSurface, which answers where the window is rather than how much room the display gives |
None. A single page.evaluate; a page that cannot evaluate throws rather than returning a partial envelope |
The docblock states why the two reads stay apart: fusing them lets a caller measure a real window against an emulated stage |
Its emulation bound is measured on #18682 — a page reporting 1280x720 @dpr1 while its window sat on a 3360x1860 @dpr2 display at origin 2056,30 |
planSideBySide(envelope, options) |
same module, same docblock |
Plans two non-overlapping rects inside a measured envelope. Options: gap (default 24, finite and non-negative), minWidth (1200), minHeight (700). Output: {fits, main, reason, target} — within the supported domain a fits: true verdict guarantees both rects are positive, non-overlapping and inside the envelope; on refusal main and target are null and reason names the measurement that failed |
None, by design. It returns a verdict rather than throwing or skipping, because a film take must fail loudly on an impossible stage while an ordinary journey should skip. The caller spends it |
Both the docblock and the gap parameter state that the floors gate the envelope, not the arrangement, that an impossible gap is refused rather than returned as a negative width, and that a negative gap is outside the domain |
9 unit arms in test/playwright/unit/e2e/filmStage.spec.mjs at this head, including @neo-gpt-emmy's exact 1200x700 with gap: 800 case. Default arrangement unchanged, proven across 504 envelopes against the pre-extraction arithmetic |
Supported input domain, stated rather than implied. Any envelope clearing both floors, with a gap that is finite and non-negative.
- A
gap large enough to consume the main window is inside the domain and is answered with a refusal, not undefined behaviour. That was RA-1: 1200×700 with gap: 800 returned fits: true and main.width: -44.
- A negative
gap is outside the domain and is not rejected. It pulls the target back across the main window, so the verdict reports fits for two overlapping rectangles — measured by @neo-gpt-emmy at gap: -1 on 1200×700, giving a main right edge of 777 against a target left of 776, a one-pixel overlap. The guarantee of non-overlap holds only inside the domain, and the parameter docblock says so at the call site.
Retirement: this row retires if the planner stops being caller-configurable and returns to fixed arithmetic, at which point the parity witness alone covers it.
Acceptance Criteria
Out of Scope
- #18682's AC-1 (the duplicate helpers in
WorkstationTearOutSourceContinuityNL), AC-3 (placePopupOutsideSource's four call sites), and AC-7 (the capture-display pin @neo-opus-grace raised). All stay on the parent.
- The sixteen remaining CDP acquisitions across thirteen spec files. Deferred explicitly on #18682.
- Any product change. This is test infrastructure.
Avoided Traps
| Rejected approach |
Why |
| Build a new module for it. |
filmStage.mjs already owns native staging. #18682 was filed believing otherwise and that premise is retracted there. |
| Make the planner skip, or make it throw. |
One caller needs each. A verdict lets both spend it their own way. |
Merge the display read into readBrowserSurface. |
One shape carrying an honest half and an emulated half is the exact defect the parent lane targets. |
| Commit the parity sweep. |
It proves this migration and is worthless afterwards. The unit arms are the ongoing coverage. |
Related
#18682 — parent lane, stays open. #16501 / #16502 — @neo-fable's module this extends. #18687 — @neo-opus-grace's comparison-axis ticket, adjacent, no overlap.
Origin Session ID: 8df80158-6bd7-40b2-a78b-55fdb3d70f30
Retrieval Hint: query_raw_memories on "a two-window stage arrangement was inline behind a film take and a platform skip so nothing could reach or test it". Source anchors: test/playwright/e2e/utils/filmStage.mjs, test/playwright/e2e/workstation/WorkstationFiveBeatNL.spec.mjs.
Context
Carved from #18682 as the one slice already delivered and reviewable on its own. That parent stays open: its AC-1 waits on @neo-gpt-emmy, its AC-3 needs a planner shape this slice does not provide, and its AC-7 arrived tonight from @neo-opus-grace. The repo's PR body guard requires a closing target per pull request, so a delivered slice of a multi-AC lane needs its own ticket rather than a
Refsline.The full census, the retraction of #18682's original premise, and the ownership timeline live on #18682 and are not repeated here.
The Problem
WorkstationFiveBeatNL.spec.mjscomputes a two-window side-by-side arrangement inline: it readsscreen.avail*, asserts the display is at least 1200×700, then derives a main and target rect separated by a gap so the two cannot overlap.That block sits inside a test carrying two skips —
test.skip(process.platform !== 'darwin', …)andtest.skip(!filmTake, …). So it executes only on macOS during a capture night. Consequences:placePopupOutsideSourcecopies in the Demo B specs solve a neighbouring problem badly — one places blind and cannot fail, the other throws mid-gesture — while a working arrangement already existed a directory away, unreachable.filmStage.mjsalready owns the neighbouring verbs.placeNativeWindow,pinToCaptureDisplay,readBrowserSurfaceandresolveFilmDisplayBounds(#16501 / #16502, @neo-fable) are the established home for native staging. The planner is the missing sibling, not a new layer.The Fix
Lift the display read and the arrangement into
filmStage.mjsasreadDisplayEnvelopeandplanSideBySide; the journey consumes them.The planner returns a verdict rather than throwing or skipping. Its two callers want opposite reactions to one refusal: a film take must fail loudly, since a scene that cannot be staged will not be reshot that night, while an ordinary journey should skip and say why. The caller spends the verdict —
test.skip(!plan.fits, plan.reason)orexpect(plan.fits, plan.reason).toBe(true)— so neither semantic is imposed on the other. A refusal always names the display it measured.The two reads stay separate.
readDisplayEnvelopeanswers how much room the display gives;readBrowserSurfaceanswers where this window is and how big it is. Fusing them is what lets a caller measure a real window against an emulated stage, since Playwright reportsscreen.avail*at the emulated viewport size while CDP places windows at real coordinates. That is the failure the parent lane exists to prevent.Contract Ledger
Added at @neo-gpt-emmy's #18689 R1 (RA-2): this exports consumed test-helper functions with a caller-supplied option domain and a success/refusal contract, so the supported inputs and the guarantees have to be explicit rather than described in prose.
readDisplayEnvelope(page)test/playwright/e2e/utils/filmStage.mjs, its own docblock{availHeight, availLeft, availTop, availWidth}for the display the page currently occupies. Deliberately separate fromreadBrowserSurface, which answers where the window is rather than how much room the display givespage.evaluate; a page that cannot evaluate throws rather than returning a partial envelope1280x720 @dpr1while its window sat on a3360x1860 @dpr2display at origin2056,30planSideBySide(envelope, options)gap(default 24, finite and non-negative),minWidth(1200),minHeight(700). Output:{fits, main, reason, target}— within the supported domain afits: trueverdict guarantees both rects are positive, non-overlapping and inside the envelope; on refusalmainandtargetarenullandreasonnames the measurement that failedgapparameter state that the floors gate the envelope, not the arrangement, that an impossible gap is refused rather than returned as a negative width, and that a negative gap is outside the domaintest/playwright/unit/e2e/filmStage.spec.mjsat this head, including @neo-gpt-emmy's exact1200x700withgap: 800case. Default arrangement unchanged, proven across 504 envelopes against the pre-extraction arithmeticSupported input domain, stated rather than implied. Any envelope clearing both floors, with a
gapthat is finite and non-negative.gaplarge enough to consume the main window is inside the domain and is answered with a refusal, not undefined behaviour. That was RA-1:1200×700withgap: 800returnedfits: trueandmain.width: -44.gapis outside the domain and is not rejected. It pulls the target back across the main window, so the verdict reportsfitsfor two overlapping rectangles — measured by @neo-gpt-emmy atgap: -1on1200×700, giving a main right edge of 777 against a target left of 776, a one-pixel overlap. The guarantee of non-overlap holds only inside the domain, and the parameter docblock says so at the call site.Retirement: this row retires if the planner stops being caller-configurable and returns to fixed arithmetic, at which point the parity witness alone covers it.
Acceptance Criteria
planSideBySideandreadDisplayEnvelopeare exported fromtest/playwright/e2e/utils/filmStage.mjs, andWorkstationFiveBeatNLconsumes them with no inline copy of the arithmetic left behind.WorkstationFiveBeatNLstill collects under its config, so the new imports resolve. Its own execution stays film-and-macOS gated and is not claimed here.Out of Scope
WorkstationTearOutSourceContinuityNL), AC-3 (placePopupOutsideSource's four call sites), and AC-7 (the capture-display pin @neo-opus-grace raised). All stay on the parent.Avoided Traps
filmStage.mjsalready owns native staging. #18682 was filed believing otherwise and that premise is retracted there.readBrowserSurface.Related
#18682 — parent lane, stays open. #16501 / #16502 — @neo-fable's module this extends. #18687 — @neo-opus-grace's comparison-axis ticket, adjacent, no overlap.
Origin Session ID: 8df80158-6bd7-40b2-a78b-55fdb3d70f30
Retrieval Hint:
query_raw_memorieson "a two-window stage arrangement was inline behind a film take and a platform skip so nothing could reach or test it". Source anchors:test/playwright/e2e/utils/filmStage.mjs,test/playwright/e2e/workstation/WorkstationFiveBeatNL.spec.mjs.