From dfd2f84afc4ca777174f85231c716118f2a0f0f0 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 9 Nov 2026 18:13:41 +0000 Subject: [PATCH 1/2] fix(skills): await the player seek in seam-gate #7378 made __player.seek() asynchronous (it resolves once the preview picture is ready). seam-gate.mjs seeked and read in the same tick, so every sample measured t=0: on a correct film each seam failed with exit-visible op 0.00 and exit-moving 0 px/s, and probe found no movers. Make the page-side seek/sample/scan helpers async and evaluate them with awaitPromise, and note the false-failure signature in the reference doc. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../motion-doctrine/references/seam-gate.md | 4 ++++ .../skills/motion-doctrine/scripts/seam-gate.mjs | 16 ++++++++-------- .../motion-doctrine/references/seam-gate.md | 4 ++++ .../skills/motion-doctrine/scripts/seam-gate.mjs | 16 ++++++++-------- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.agents/skills/motion-doctrine/references/seam-gate.md b/.agents/skills/motion-doctrine/references/seam-gate.md index 7bfeeaa1bc0..39fcb11dc85 100644 --- a/.agents/skills/motion-doctrine/references/seam-gate.md +++ b/.agents/skills/motion-doctrine/references/seam-gate.md @@ -91,3 +91,7 @@ Lives at the project root. One row per seam; this is the vector ledger as data. Velocities are measured on `getBoundingClientRect` (center for x/y, width-ratio for z), so ancestor wrapper transforms are automatically included. + +`__player.seek()` resolves asynchronously, so the gate awaits each seek before it reads. If every +seam fails the same way (`exit-visible op 0.00`, `exit-moving 0 px/s`) and `probe` finds no movers, +the reads landed before the seek did and the gate measured t=0, not the film. diff --git a/.agents/skills/motion-doctrine/scripts/seam-gate.mjs b/.agents/skills/motion-doctrine/scripts/seam-gate.mjs index 16893a18a7b..0d7b0a0d685 100644 --- a/.agents/skills/motion-doctrine/scripts/seam-gate.mjs +++ b/.agents/skills/motion-doctrine/scripts/seam-gate.mjs @@ -294,7 +294,7 @@ async function openPage(compUrl) { // ---------- in-page harness ---------- const HARNESS = `window.__seamGate = { - seek(t){ __player.pause(); __player.seek(t); void document.body.offsetHeight; }, + async seek(t){ __player.pause(); await __player.seek(t); void document.body.offsetHeight; }, cumOp(el){ let op = 1, n = el; while (n && n.nodeType === 1) { @@ -314,7 +314,7 @@ const HARNESS = `window.__seamGate = { return { cx: r.x + r.width/2, cy: r.y + r.height/2, w: r.width, h: r.height, op: this.cumOp(el), es: r.width / lw, onscreen }; }, - sample(t, sels){ this.seek(t); const o = {}; for (const s of sels) o[s] = this.read(s); return o; }, + async sample(t, sels){ await this.seek(t); const o = {}; for (const s of sels) o[s] = this.read(s); return o; }, pathOf(el){ if (el.id) return "#" + CSS.escape(el.id); const hf = el.getAttribute && el.getAttribute("data-hf-id"); @@ -330,8 +330,8 @@ const HARNESS = `window.__seamGate = { } return p.join(">"); }, - scan(t, rootSel, cap){ - this.seek(t); + async scan(t, rootSel, cap){ + await this.seek(t); const root = document.querySelector(rootSel || "#root"); if (!root) return []; const els = [root, ...root.querySelectorAll("*")].slice(0, cap || 900); @@ -389,7 +389,7 @@ async function verify() { ].filter(Boolean); const S = {}; for (const t of [tA1, tA2, tB1, tB2]) - S[t] = await evalJs(`__seamGate.sample(${t}, ${JSON.stringify([...new Set(sels)])})`); + S[t] = await evalJs(`__seamGate.sample(${t}, ${JSON.stringify([...new Set(sels)])})`, true); if (type === "cut") { const ex = seam.exit, @@ -470,8 +470,8 @@ async function verify() { // Z-sign scan: the incoming scene's OWN entrances must not fight the seam's Z sign if (en.axis === "z") { const scanRoot = en.scanRoot || en.selector; - const s1 = await evalJs(`__seamGate.scan(${tB1}, ${JSON.stringify(scanRoot)})`); - const s2 = await evalJs(`__seamGate.scan(${tB2}, ${JSON.stringify(scanRoot)})`); + const s1 = await evalJs(`__seamGate.scan(${tB1}, ${JSON.stringify(scanRoot)})`, true); + const s2 = await evalJs(`__seamGate.scan(${tB2}, ${JSON.stringify(scanRoot)})`, true); const m1 = new Map(s1.map((e) => [e.sel, e])); const offenders = []; for (const e2 of s2) { @@ -539,7 +539,7 @@ async function probe() { const dt = DT; const scans = {}; for (const tt of [t - win, t - dt, t + dt, t + win]) - scans[tt] = await evalJs(`__seamGate.scan(${Math.max(0, tt)}, "#root")`); + scans[tt] = await evalJs(`__seamGate.scan(${Math.max(0, tt)}, "#root")`, true); const join = (a, b) => { const m = new Map(a.map((e) => [e.sel, e])); return b.map((e2) => ({ e1: m.get(e2.sel), e2 })).filter((p) => p.e1); diff --git a/.claude/skills/motion-doctrine/references/seam-gate.md b/.claude/skills/motion-doctrine/references/seam-gate.md index 7bfeeaa1bc0..39fcb11dc85 100644 --- a/.claude/skills/motion-doctrine/references/seam-gate.md +++ b/.claude/skills/motion-doctrine/references/seam-gate.md @@ -91,3 +91,7 @@ Lives at the project root. One row per seam; this is the vector ledger as data. Velocities are measured on `getBoundingClientRect` (center for x/y, width-ratio for z), so ancestor wrapper transforms are automatically included. + +`__player.seek()` resolves asynchronously, so the gate awaits each seek before it reads. If every +seam fails the same way (`exit-visible op 0.00`, `exit-moving 0 px/s`) and `probe` finds no movers, +the reads landed before the seek did and the gate measured t=0, not the film. diff --git a/.claude/skills/motion-doctrine/scripts/seam-gate.mjs b/.claude/skills/motion-doctrine/scripts/seam-gate.mjs index 16893a18a7b..0d7b0a0d685 100644 --- a/.claude/skills/motion-doctrine/scripts/seam-gate.mjs +++ b/.claude/skills/motion-doctrine/scripts/seam-gate.mjs @@ -294,7 +294,7 @@ async function openPage(compUrl) { // ---------- in-page harness ---------- const HARNESS = `window.__seamGate = { - seek(t){ __player.pause(); __player.seek(t); void document.body.offsetHeight; }, + async seek(t){ __player.pause(); await __player.seek(t); void document.body.offsetHeight; }, cumOp(el){ let op = 1, n = el; while (n && n.nodeType === 1) { @@ -314,7 +314,7 @@ const HARNESS = `window.__seamGate = { return { cx: r.x + r.width/2, cy: r.y + r.height/2, w: r.width, h: r.height, op: this.cumOp(el), es: r.width / lw, onscreen }; }, - sample(t, sels){ this.seek(t); const o = {}; for (const s of sels) o[s] = this.read(s); return o; }, + async sample(t, sels){ await this.seek(t); const o = {}; for (const s of sels) o[s] = this.read(s); return o; }, pathOf(el){ if (el.id) return "#" + CSS.escape(el.id); const hf = el.getAttribute && el.getAttribute("data-hf-id"); @@ -330,8 +330,8 @@ const HARNESS = `window.__seamGate = { } return p.join(">"); }, - scan(t, rootSel, cap){ - this.seek(t); + async scan(t, rootSel, cap){ + await this.seek(t); const root = document.querySelector(rootSel || "#root"); if (!root) return []; const els = [root, ...root.querySelectorAll("*")].slice(0, cap || 900); @@ -389,7 +389,7 @@ async function verify() { ].filter(Boolean); const S = {}; for (const t of [tA1, tA2, tB1, tB2]) - S[t] = await evalJs(`__seamGate.sample(${t}, ${JSON.stringify([...new Set(sels)])})`); + S[t] = await evalJs(`__seamGate.sample(${t}, ${JSON.stringify([...new Set(sels)])})`, true); if (type === "cut") { const ex = seam.exit, @@ -470,8 +470,8 @@ async function verify() { // Z-sign scan: the incoming scene's OWN entrances must not fight the seam's Z sign if (en.axis === "z") { const scanRoot = en.scanRoot || en.selector; - const s1 = await evalJs(`__seamGate.scan(${tB1}, ${JSON.stringify(scanRoot)})`); - const s2 = await evalJs(`__seamGate.scan(${tB2}, ${JSON.stringify(scanRoot)})`); + const s1 = await evalJs(`__seamGate.scan(${tB1}, ${JSON.stringify(scanRoot)})`, true); + const s2 = await evalJs(`__seamGate.scan(${tB2}, ${JSON.stringify(scanRoot)})`, true); const m1 = new Map(s1.map((e) => [e.sel, e])); const offenders = []; for (const e2 of s2) { @@ -539,7 +539,7 @@ async function probe() { const dt = DT; const scans = {}; for (const tt of [t - win, t - dt, t + dt, t + win]) - scans[tt] = await evalJs(`__seamGate.scan(${Math.max(0, tt)}, "#root")`); + scans[tt] = await evalJs(`__seamGate.scan(${Math.max(0, tt)}, "#root")`, true); const join = (a, b) => { const m = new Map(a.map((e) => [e.sel, e])); return b.map((e2) => ({ e1: m.get(e2.sel), e2 })).filter((p) => p.e1); From b8a354e28cd9580da2103315e2dab20c31ee3d7b Mon Sep 17 00:00:00 2001 From: James Date: Mon, 9 Nov 2026 18:25:45 +0000 Subject: [PATCH 2/2] docs(skills): await the player seek in changelog-video spot checks Co-Authored-By: Claude Opus 5.5 (1M context) --- .agents/skills/changelog-video/SKILL.md | 2 +- .agents/skills/changelog-video/references/build-spec.md | 2 +- .claude/skills/changelog-video/SKILL.md | 2 +- .claude/skills/changelog-video/references/build-spec.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/skills/changelog-video/SKILL.md b/.agents/skills/changelog-video/SKILL.md index 6eb6bb73fbf..765bf050c83 100644 --- a/.agents/skills/changelog-video/SKILL.md +++ b/.agents/skills/changelog-video/SKILL.md @@ -155,7 +155,7 @@ scaffold. truth for the composition contract this skill produces against. 2. `seam-gate.mjs verify` — 0 fail. 3. Restart the preview server (it caches the bundle), spot-check 3-4 beats - via `__player.seek` on the raw comp page. + via `await __player.seek(t)` on the raw comp page (it resolves async). 4. Do NOT render unless the user asks. After a requested render, verify frames from the MP4 (`ffmpeg -ss … -frames:v 1`): captions present, background video not black, no tiny/frozen frames. diff --git a/.agents/skills/changelog-video/references/build-spec.md b/.agents/skills/changelog-video/references/build-spec.md index 6a70d7a6212..d0d6642c4a5 100644 --- a/.agents/skills/changelog-video/references/build-spec.md +++ b/.agents/skills/changelog-video/references/build-spec.md @@ -119,4 +119,4 @@ otherwise) and must stay flat 2D (no 3D ancestors). (0.14 sits under the VO; use a user-supplied track only when given one). - Preview server caches the bundle — RESTART after edits, then verify on the raw comp page (`/api/projects//preview/comp/index.html`) via - `window.__player.seek(t)`. + `await window.__player.seek(t)` (it resolves async; read after it settles). diff --git a/.claude/skills/changelog-video/SKILL.md b/.claude/skills/changelog-video/SKILL.md index 6eb6bb73fbf..765bf050c83 100644 --- a/.claude/skills/changelog-video/SKILL.md +++ b/.claude/skills/changelog-video/SKILL.md @@ -155,7 +155,7 @@ scaffold. truth for the composition contract this skill produces against. 2. `seam-gate.mjs verify` — 0 fail. 3. Restart the preview server (it caches the bundle), spot-check 3-4 beats - via `__player.seek` on the raw comp page. + via `await __player.seek(t)` on the raw comp page (it resolves async). 4. Do NOT render unless the user asks. After a requested render, verify frames from the MP4 (`ffmpeg -ss … -frames:v 1`): captions present, background video not black, no tiny/frozen frames. diff --git a/.claude/skills/changelog-video/references/build-spec.md b/.claude/skills/changelog-video/references/build-spec.md index 6a70d7a6212..d0d6642c4a5 100644 --- a/.claude/skills/changelog-video/references/build-spec.md +++ b/.claude/skills/changelog-video/references/build-spec.md @@ -119,4 +119,4 @@ otherwise) and must stay flat 2D (no 3D ancestors). (0.14 sits under the VO; use a user-supplied track only when given one). - Preview server caches the bundle — RESTART after edits, then verify on the raw comp page (`/api/projects//preview/comp/index.html`) via - `window.__player.seek(t)`. + `await window.__player.seek(t)` (it resolves async; read after it settles).