()
| 67 | /** Acts from the focus timeline: each contiguous focus run becomes a cut of |
| 68 | * that window's recording, mapped via the recording-start anchors. */ |
| 69 | const actsFromTimeline = (): Act[] | null => { |
| 70 | const timeline = readTimeline(runDir); |
| 71 | if (!timeline || timeline.focus.length < 2) return null; |
| 72 | const terminalAnchor = timeline.anchors.terminal; |
| 73 | const browserAnchor = timeline.anchors.browser; |
| 74 | if (terminalAnchor === undefined || browserAnchor === undefined) return null; |
| 75 | |
| 76 | const browserEnd = probeSeconds(browserPath); |
| 77 | const toMedia = (window: Act["source"], wallMs: number) => |
| 78 | Math.max(0, (wallMs - (window === "terminal" ? terminalAnchor : browserAnchor)) / 1000); |
| 79 | |
| 80 | const acts: Act[] = []; |
| 81 | for (let i = 0; i < timeline.focus.length; i += 1) { |
| 82 | const current = timeline.focus[i]; |
| 83 | if (!current) continue; |
| 84 | const nextAt = timeline.focus[i + 1]?.at; |
| 85 | const from = i === 0 && current.window === "terminal" ? 0 : toMedia(current.window, current.at); |
| 86 | const mediaEnd = current.window === "terminal" ? castEnd : browserEnd; |
| 87 | const to = |
| 88 | nextAt === undefined ? mediaEnd : Math.min(toMedia(current.window, nextAt), mediaEnd); |
| 89 | if (to - from > 0.5) acts.push({ source: current.window, from, to }); |
| 90 | } |
| 91 | return acts.length >= 2 ? acts : null; |
| 92 | }; |
| 93 | |
| 94 | /** Fallback for runs without a timeline: one browser hop located by the |
| 95 | * narrator line, or the largest output gap in the cast. */ |
no test coverage detected