fix(cli): check's dense overlap pass takes at most 240 samples, at clip windows first (#5764)

* fix(cli): check's dense overlap pass takes at most 240 samples, at clip windows first

* fix(cli): a held overlap on a long film is still an error under the 240-sample bound
This commit is contained in:
Miguel Ángel
2026-10-17 07:23:17 +00:00
committed by GitHub
parent cc9e1bb33a
commit caea3863b0
5 changed files with 95 additions and 12 deletions
+41
View File
@@ -163,6 +163,7 @@ function fakeDriver(overrides: Partial<CheckAuditDriver> = {}): CheckAuditDriver
getDuration: vi.fn(async () => 9),
hasNoTimelineDeclaration: vi.fn(async () => false),
getTransitionBoundaries: vi.fn(async () => []),
getClipEdges: vi.fn(async () => []),
getCanvas: vi.fn(async () => ({ width: 1920, height: 1080 })),
findAmbiguousSelectors: vi.fn(async (_selectors: string[]) => []),
seek: vi.fn(async (_time: number) => undefined),
@@ -2312,6 +2313,46 @@ describe("dense motion-overlap re-sampling", () => {
expect(driver.collectOverlap).toHaveBeenCalled();
expect(report.layout.findings.some((f) => f.code === "content_overlap")).toBe(true);
});
it("keeps a long film's dense pass to a fixed number of seeks", async () => {
const clipEdges = Array.from({ length: 433 }, (_, i) => [i * 1.2, i * 1.2 + 1.1]).flat();
const driver = fakeDriver({
getDuration: vi.fn(async () => 540),
getClipEdges: async () => clipEdges,
});
await runScenario(driver);
expect(vi.mocked(driver.seekGeometry).mock.calls.length).toBeLessThanOrEqual(240);
});
it("still promotes an overlap held on a long film", async () => {
const held = (time: number): boolean => time >= 200 && time <= 260;
const driver = fakeDriver({
getDuration: vi.fn(async () => 540),
collectLayout: vi.fn(async (_time: number) => []),
collectOverlap: vi.fn(async (time: number) =>
held(time) ? [layoutIssue("warning", { time, code: "content_overlap" })] : [],
),
});
const { report } = await runScenario(driver);
expect(report.layout.findings).toContainEqual(
expect.objectContaining({ code: "content_overlap", severity: "error" }),
);
expect(vi.mocked(driver.seekGeometry).mock.calls.length).toBeLessThanOrEqual(240);
});
it("samples a short clip the uniform grid steps over", async () => {
const inShortClip = (time: number): boolean => time > 100.1 && time < 100.4;
const driver = fakeDriver({
getDuration: vi.fn(async () => 540),
getClipEdges: async () => [100.1, 100.4],
collectLayout: vi.fn(async (_time: number) => []),
collectOverlap: vi.fn(async (time: number) =>
inShortClip(time) ? [layoutIssue("warning", { time, code: "content_overlap" })] : [],
),
});
const { report } = await runScenario(driver);
expect(report.layout.findings.some((f) => f.code === "content_overlap")).toBe(true);
});
});
describe("scoped check", () => {
+11
View File
@@ -570,6 +570,7 @@ function createPageDriver(page: Page, setTime: (time: number) => void): CheckAud
getDuration: () => getCompositionDuration(page),
hasNoTimelineDeclaration: () => hasNoTimelineDeclaration(page),
getTransitionBoundaries: () => collectTweenBoundaries(page),
getClipEdges: () => collectClipEdges(page),
getCanvas: () =>
page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight })),
findAmbiguousSelectors: (selectors) => findAmbiguousSelectors(page, selectors),
@@ -644,6 +645,16 @@ async function getCompositionDuration(page: Page): Promise<number> {
});
}
async function collectClipEdges(page: Page): Promise<number[]> {
return page.evaluate(() => {
type Clip = { kind: string; start: number; duration: number };
const manifest = Reflect.get(window, "__clipManifest") as { clips?: Clip[] } | undefined;
return (manifest?.clips ?? [])
.filter((clip) => clip.kind !== "audio")
.flatMap((clip) => [clip.start, clip.start + clip.duration]);
});
}
async function collectTweenBoundaries(page: Page): Promise<number[]> {
// GSAP getter binding and parent-time conversion form one serialized algorithm.
// fallow-ignore-next-line complexity
+41 -11
View File
@@ -17,6 +17,7 @@ import {
buildLayoutSampleTimes,
buildTransitionSampleTimes,
collapseStaticLayoutIssues,
CONTENT_OVERLAP_HELD_ERROR_MS,
longestContiguousRunMs,
dedupeLayoutIssues,
limitLayoutIssues,
@@ -489,19 +490,35 @@ async function collectGridSamples(
// Dense grid catches mid-motion text collisions the sparse layout grid seeks past; text-only overlap collection is cheap enough to afford it.
const OVERLAP_SAMPLE_FPS = 8;
// Ceiling that bounds dense seeks; past ~75s the grid degrades below 8fps rather than growing the seek budget without limit.
const OVERLAP_MAX_SAMPLES = 600;
// A fixed ceiling, so check's work does not grow with film length: clip windows first, then a uniform fill.
const OVERLAP_MAX_SAMPLES = 240;
// Part of it re-looks at an overlap half a second on where the grid is too sparse to show it held.
const OVERLAP_HOLD_PROBES = 60;
function buildOverlapSampleTimes(duration: number): number[] {
function evenlyPicked(times: number[], count: number): number[] {
if (times.length <= count) return times;
const last = Math.max(1, count - 1);
return Array.from(
{ length: count },
(_, i) => times[Math.floor((i * (times.length - 1)) / last)] ?? 0,
);
}
function buildOverlapSampleTimes(duration: number, clipEdges: number[]): number[] {
if (!Number.isFinite(duration) || duration <= 0) return [];
const count = Math.min(
OVERLAP_MAX_SAMPLES,
OVERLAP_MAX_SAMPLES - OVERLAP_HOLD_PROBES,
Math.max(2, Math.ceil(duration * OVERLAP_SAMPLE_FPS) + 1),
);
const step = duration / (count - 1);
return mergeSampleTimes(
Array.from({ length: count }, (_, index) => Math.round(index * step * 1000) / 1000),
);
const inner = clipEdges.filter((t) => t > 0 && t < duration);
const cuts = inner.length > 0 ? mergeSampleTimes([0, duration], inner) : [];
// One sample inside every stretch where the visible clips stay the same, so a short overlap is never stepped over.
const windows = cuts.slice(1).map((end, i) => ((cuts[i] ?? 0) + end) / 2);
const picked = evenlyPicked(windows, count);
const fill = count - picked.length;
const step = duration / Math.max(1, fill - 1);
const uniform = Array.from({ length: fill }, (_, i) => i * step);
return mergeSampleTimes([...picked, ...uniform].map((t) => Math.round(t * 1000) / 1000));
}
/** Reruns content_overlap on a fine grid, unconditionally rather than gated on fingerprint change, since an animation aliased to the sparse grid collides between identical-fingerprint samples. */
@@ -512,15 +529,28 @@ async function collectMotionOverlapSamples(
range?: [number, number],
): Promise<void> {
const baseTimes = new Set(grid.layoutSamples);
const overlapTimes = buildOverlapSampleTimes(range ? range[1] - range[0] : grid.duration);
const offset = range?.[0] ?? 0;
const edges = (await driver.getClipEdges()).map((time) => time - offset);
const overlapTimes = buildOverlapSampleTimes(range ? range[1] - range[0] : grid.duration, edges);
const shiftedTimes = range
? mergeSampleTimes(overlapTimes.map((time) => time + range[0]))
: overlapTimes;
for (const time of shiftedTimes) {
const holdS = CONTENT_OVERLAP_HELD_ERROR_MS / 1000;
const end = range ? range[1] : grid.duration;
let probes = 0;
for (const [index, time] of shiftedTimes.entries()) {
if (baseTimes.has(time)) continue;
// Settle-free seek: collectOverlap reads getBoundingClientRect geometry, valid synchronously after setTime, so the dense pass skips the per-seek paint settle.
await driver.seekGeometry(time);
collected.layoutIssues.push(...(await driver.collectOverlap(time)));
const issues = await driver.collectOverlap(time);
collected.layoutIssues.push(...issues);
const next = shiftedTimes[index + 1] ?? Number.POSITIVE_INFINITY;
const probe = Math.round((time + holdS) * 1000) / 1000;
if (!issues.length || next - time <= 2 * holdS || probe > end || probes >= OVERLAP_HOLD_PROBES)
continue;
probes += 1;
await driver.seekGeometry(probe);
collected.layoutIssues.push(...(await driver.collectOverlap(probe)));
}
}
+1
View File
@@ -195,6 +195,7 @@ export interface CheckAuditDriver {
/** True when the root composition explicitly declares that it is intentionally timeline-free. */
hasNoTimelineDeclaration(): Promise<boolean>;
getTransitionBoundaries(): Promise<number[]>;
getClipEdges(): Promise<number[]>;
getCanvas(): Promise<Canvas>;
findAmbiguousSelectors(selectors: string[]): Promise<AnchoredLayoutIssue[]>;
seek(time: number): Promise<void>;
+1 -1
View File
@@ -189,7 +189,7 @@ export function dedupeLayoutIssues(issues: LayoutIssue[]): LayoutIssue[] {
}
// Persistence-tier thresholds (#U10): occurrences>=2 alone can't imply the 500ms floor under dense 8fps sampling, so content_overlap promotion requires a literal firstSeen..lastSeen span >= 500ms — stricter for sparse callers too.
const CONTENT_OVERLAP_HELD_ERROR_MS = 500;
export const CONTENT_OVERLAP_HELD_ERROR_MS = 500;
const HELD_ACROSS_SAMPLES_MIN_OCCURRENCES = 2;
// Tiering only applies to layout-audit.browser.js's own per-sample seek-grid