mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
feat(check): opt-in --layout proseCoverageFloor (#2834)
* feat(check): opt-in --layout proseCoverageFloor for text_occluded Keep the default prose coverage floor at 0.15 for all callers, and allow stricter agents (e.g. Zephyr) to lower it via --layout "proseCoverageFloor=0.05" without changing other layout gates. Co-authored-by: Cursor <cursoragent@cursor.com> * style(check): collapse --layout comments and docs to one line Co-authored-by: Cursor <cursoragent@cursor.com> * fix(check): strict proseCoverageFloor parse + pin 0.07 floor tests Reject trailing-garbage fractions that Number.parseFloat would accept, and pin the existing ~0.07 coverage fixture for default vs floor=0.05 (atomic labels unchanged) plus a collectLayout forwarding assertion. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(check): share parseNumberStrict across layout and frame-check Sweep the sibling --frame-check tol parser (and caption fractions) onto the same strict Number() helper so trailing garbage cannot prefix-parse. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1141,6 +1141,57 @@ describe("frame-check flag grammar", () => {
|
|||||||
});
|
});
|
||||||
expect(() => parseFrameCheck("bogus=1")).toThrow("Invalid --frame-check");
|
expect(() => parseFrameCheck("bogus=1")).toThrow("Invalid --frame-check");
|
||||||
expect(() => parseFrameCheck("tol=-2")).toThrow("Invalid --frame-check");
|
expect(() => parseFrameCheck("tol=-2")).toThrow("Invalid --frame-check");
|
||||||
|
expect(() => parseFrameCheck("tol=4px")).toThrow("Invalid --frame-check");
|
||||||
|
expect(() => parseFrameCheck("tol=2garbage")).toThrow("Invalid --frame-check");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("layout flag grammar", () => {
|
||||||
|
it("parses proseCoverageFloor and rejects malformed specs", async () => {
|
||||||
|
const { parseLayout } = await import("./check.js");
|
||||||
|
expect(parseLayout(undefined)).toBeUndefined();
|
||||||
|
expect(parseLayout("proseCoverageFloor=0.05")).toEqual({ proseCoverageFloor: 0.05 });
|
||||||
|
expect(parseLayout("proseCoverageFloor=0")).toEqual({ proseCoverageFloor: 0 });
|
||||||
|
expect(parseLayout("proseCoverageFloor=1")).toEqual({ proseCoverageFloor: 1 });
|
||||||
|
expect(() => parseLayout(true)).toThrow("Invalid --layout");
|
||||||
|
expect(() => parseLayout("")).toThrow("Invalid --layout");
|
||||||
|
expect(() => parseLayout("bogus=1")).toThrow("Invalid --layout");
|
||||||
|
expect(() => parseLayout("proseCoverageFloor=-0.1")).toThrow("Invalid --layout");
|
||||||
|
expect(() => parseLayout("proseCoverageFloor=1.1")).toThrow("Invalid --layout");
|
||||||
|
expect(() => parseLayout("proseCoverageFloor=0.05garbage")).toThrow("Invalid --layout");
|
||||||
|
expect(() => parseLayout("proseCoverageFloor=0.1%")).toThrow("Invalid --layout");
|
||||||
|
expect(() => parseLayout("proseCoverageFloor=")).toThrow("Invalid --layout");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("threads --layout into the check pipeline options", async () => {
|
||||||
|
const { report } = await runScenario(fakeDriver());
|
||||||
|
const runPipeline = vi.fn(async (_project: ProjectDir, _options: CheckOptions) => report);
|
||||||
|
vi.spyOn(console, "log").mockImplementation(() => undefined);
|
||||||
|
const command = createCheckCommand({
|
||||||
|
resolveProject: () => PROJECT,
|
||||||
|
runPipeline,
|
||||||
|
withMeta: (value) => value,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runCommand(command, {
|
||||||
|
rawArgs: ["--json", "--layout", "proseCoverageFloor=0.05"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(runPipeline).toHaveBeenCalledWith(
|
||||||
|
PROJECT,
|
||||||
|
expect.objectContaining({
|
||||||
|
layout: { proseCoverageFloor: 0.05 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards layout options into driver.collectLayout", async () => {
|
||||||
|
const collectLayout = vi.fn(async (_time: number, _tolerance: number, _layout?: unknown) => []);
|
||||||
|
await runScenario(fakeDriver({ collectLayout }), { layout: { proseCoverageFloor: 0.05 } });
|
||||||
|
expect(collectLayout).toHaveBeenCalled();
|
||||||
|
expect(collectLayout).toHaveBeenCalledWith(expect.any(Number), expect.any(Number), {
|
||||||
|
proseCoverageFloor: 0.05,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
type CheckReport,
|
type CheckReport,
|
||||||
type CheckSection,
|
type CheckSection,
|
||||||
} from "../utils/checkPipeline.js";
|
} from "../utils/checkPipeline.js";
|
||||||
import type { CaptionZoneOptions, FrameCheckOptions } from "../utils/checkTypes.js";
|
import type { CaptionZoneOptions, FrameCheckOptions, LayoutOptions } from "../utils/checkTypes.js";
|
||||||
|
|
||||||
export const examples: Example[] = [
|
export const examples: Example[] = [
|
||||||
["Run the full verification gate", "hyperframes check"],
|
["Run the full verification gate", "hyperframes check"],
|
||||||
@@ -120,6 +120,10 @@ export function createCheckCommand(
|
|||||||
description:
|
description:
|
||||||
'Bare --frame-check uses defaults (tol=2px, severity=warning, seek=.5; breach floor=max(120px, 6% of shorter canvas edge)); or pass "severity=error;seek=.25,.75;tol=4" to tune',
|
'Bare --frame-check uses defaults (tol=2px, severity=warning, seek=.5; breach floor=max(120px, 6% of shorter canvas edge)); or pass "severity=error;seek=.25,.75;tol=4" to tune',
|
||||||
},
|
},
|
||||||
|
layout: {
|
||||||
|
type: "string",
|
||||||
|
description: 'Layout knobs: "proseCoverageFloor=0.05" (0–1; default 0.15).',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async run({ args }) {
|
async run({ args }) {
|
||||||
const asJson = args.json === true;
|
const asJson = args.json === true;
|
||||||
@@ -168,6 +172,7 @@ function parseCheckOptions(args: Record<string, unknown>): CheckOptions {
|
|||||||
snapshots: args.snapshots === true,
|
snapshots: args.snapshots === true,
|
||||||
captionZone: parseCaptionZone(args["caption-zone"]),
|
captionZone: parseCaptionZone(args["caption-zone"]),
|
||||||
frameCheck: parseFrameCheck(args["frame-check"]),
|
frameCheck: parseFrameCheck(args["frame-check"]),
|
||||||
|
layout: parseLayout(args.layout),
|
||||||
autoProxy: args.proxy as boolean | undefined,
|
autoProxy: args.proxy as boolean | undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -176,6 +181,8 @@ const CAPTION_ZONE_FIELDS = new Set(["x0", "y0", "x1", "y1", "severity", "seek"]
|
|||||||
|
|
||||||
const FRAME_CHECK_FIELDS = new Set(["severity", "seek", "tol"]);
|
const FRAME_CHECK_FIELDS = new Set(["severity", "seek", "tol"]);
|
||||||
|
|
||||||
|
const LAYOUT_FIELDS = new Set(["proseCoverageFloor"]);
|
||||||
|
|
||||||
// Mirrors --caption-zone's spec grammar so the EF bridge's severity/seek/tol
|
// Mirrors --caption-zone's spec grammar so the EF bridge's severity/seek/tol
|
||||||
// options survive the migration instead of being silently dropped by a
|
// options survive the migration instead of being silently dropped by a
|
||||||
// boolean flag (bare --frame-check keeps today's defaults).
|
// boolean flag (bare --frame-check keeps today's defaults).
|
||||||
@@ -206,8 +213,8 @@ function parseFrameCheckFields(value: string): Map<string, string> {
|
|||||||
|
|
||||||
function parseFrameCheckTolerance(raw: string | undefined): number | undefined {
|
function parseFrameCheckTolerance(raw: string | undefined): number | undefined {
|
||||||
if (raw === undefined) return undefined;
|
if (raw === undefined) return undefined;
|
||||||
const tol = Number.parseFloat(raw);
|
const tol = parseNumberStrict(raw);
|
||||||
if (!Number.isFinite(tol) || tol < 0) throw frameCheckError();
|
if (tol === null || tol < 0) throw frameCheckError();
|
||||||
return tol;
|
return tol;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +224,52 @@ function frameCheckError(): Error {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse `--layout "proseCoverageFloor=0.05"` (semicolon-separated key=value, like caption-zone). */
|
||||||
|
export function parseLayout(value: unknown): LayoutOptions | undefined {
|
||||||
|
if (value === undefined || value === null || value === false) return undefined;
|
||||||
|
if (value === true || value === "") throw layoutError();
|
||||||
|
if (typeof value !== "string") throw layoutError();
|
||||||
|
const fields = parseLayoutFields(value);
|
||||||
|
const proseCoverageFloor = parseProseCoverageFloor(fields.get("proseCoverageFloor"));
|
||||||
|
if (proseCoverageFloor === undefined) throw layoutError();
|
||||||
|
return { proseCoverageFloor };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLayoutFields(value: string): Map<string, string> {
|
||||||
|
const fields = new Map<string, string>();
|
||||||
|
for (const part of value.split(";")) {
|
||||||
|
const trimmed = part.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
const separator = trimmed.indexOf("=");
|
||||||
|
if (separator <= 0) throw layoutError();
|
||||||
|
const key = trimmed.slice(0, separator).trim();
|
||||||
|
const entry = trimmed.slice(separator + 1).trim();
|
||||||
|
if (!LAYOUT_FIELDS.has(key) || fields.has(key)) throw layoutError();
|
||||||
|
fields.set(key, entry);
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProseCoverageFloor(raw: string | undefined): number | undefined {
|
||||||
|
if (raw === undefined) return undefined;
|
||||||
|
const floor = parseNumberStrict(raw);
|
||||||
|
if (floor === null || floor < 0 || floor > 1) throw layoutError();
|
||||||
|
return floor;
|
||||||
|
}
|
||||||
|
|
||||||
|
function layoutError(): Error {
|
||||||
|
return new Error(
|
||||||
|
'Invalid --layout: use "proseCoverageFloor=0.05" with a fraction from 0 to 1 (inclusive)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reject trailing garbage that Number.parseFloat would silently accept (`4px`, `0.05abc`). */
|
||||||
|
function parseNumberStrict(raw: string): number | null {
|
||||||
|
if (raw === "") return null;
|
||||||
|
const value = Number(raw);
|
||||||
|
return Number.isFinite(value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
function parseCaptionZone(value: unknown): CaptionZoneOptions | undefined {
|
function parseCaptionZone(value: unknown): CaptionZoneOptions | undefined {
|
||||||
if (value === undefined || value === null) return undefined;
|
if (value === undefined || value === null) return undefined;
|
||||||
const fields = parseCaptionFields(captionZoneString(value));
|
const fields = parseCaptionFields(captionZoneString(value));
|
||||||
@@ -279,8 +332,8 @@ function requiredCaptionFraction(fields: Map<string, string>, key: string): numb
|
|||||||
|
|
||||||
function captionFraction(value: string | undefined): number | null {
|
function captionFraction(value: string | undefined): number | null {
|
||||||
if (value === undefined || value === "") return null;
|
if (value === undefined || value === "") return null;
|
||||||
const parsed = Number(value);
|
const parsed = parseNumberStrict(value);
|
||||||
return Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : null;
|
return parsed !== null && parsed >= 0 && parsed <= 1 ? parsed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function captionSeverity(value: string | undefined): "error" | "warning" | undefined {
|
function captionSeverity(value: string | undefined): "error" | "warning" | undefined {
|
||||||
|
|||||||
@@ -935,7 +935,8 @@
|
|||||||
// (the pre-#U10 behaviour). Longer prose survives a nibbled edge; only flag
|
// (the pre-#U10 behaviour). Longer prose survives a nibbled edge; only flag
|
||||||
// once a real share of it is covered — see `occludedTextIssue`.
|
// once a real share of it is covered — see `occludedTextIssue`.
|
||||||
const ATOMIC_LABEL_MAX_CHARS = 16;
|
const ATOMIC_LABEL_MAX_CHARS = 16;
|
||||||
const PROSE_COVERAGE_FLOOR = 0.15;
|
// Default prose floor — callers may lower via auditLayout({ proseCoverageFloor }).
|
||||||
|
const DEFAULT_PROSE_COVERAGE_FLOOR = 0.15;
|
||||||
|
|
||||||
function isAtomicLabel(text) {
|
function isAtomicLabel(text) {
|
||||||
return text.length > 0 && text.length <= ATOMIC_LABEL_MAX_CHARS && !/\s/.test(text);
|
return text.length > 0 && text.length <= ATOMIC_LABEL_MAX_CHARS && !/\s/.test(text);
|
||||||
@@ -995,12 +996,8 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Catches the blind spot the overflow checks miss: text that fits its box
|
// text_occluded: atomic labels flag at any hit; prose needs coveredFraction >= proseCoverageFloor (default 0.15).
|
||||||
// perfectly but is covered by a later sibling/overlay. An atomic label
|
function occludedTextIssue(element, time, proseCoverageFloor) {
|
||||||
// (short, no whitespace) flags at any coverage; ordinary prose only flags
|
|
||||||
// once coveredFraction clears PROSE_COVERAGE_FLOOR, since a sliver of edge
|
|
||||||
// cover on a paragraph is usually a styling artifact, not a reading defect.
|
|
||||||
function occludedTextIssue(element, time) {
|
|
||||||
if (hasAllowOcclusionFlag(element)) return null;
|
if (hasAllowOcclusionFlag(element)) return null;
|
||||||
if (!hasVisibleTextInk(element)) return null;
|
if (!hasVisibleTextInk(element)) return null;
|
||||||
const textRect = textRectFor(element, true);
|
const textRect = textRectFor(element, true);
|
||||||
@@ -1012,7 +1009,7 @@
|
|||||||
textRects.length > 0 ? textRects : [textRect],
|
textRects.length > 0 ? textRects : [textRect],
|
||||||
);
|
);
|
||||||
if (!occluder) return null;
|
if (!occluder) return null;
|
||||||
if (!isAtomicLabel(text) && coveredFraction < PROSE_COVERAGE_FLOOR) return null;
|
if (!isAtomicLabel(text) && coveredFraction < proseCoverageFloor) return null;
|
||||||
return {
|
return {
|
||||||
code: "text_occluded",
|
code: "text_occluded",
|
||||||
severity: "error",
|
severity: "error",
|
||||||
@@ -1420,6 +1417,10 @@
|
|||||||
const time = options && typeof options.time === "number" ? options.time : 0;
|
const time = options && typeof options.time === "number" ? options.time : 0;
|
||||||
const tolerance =
|
const tolerance =
|
||||||
options && typeof options.tolerance === "number" ? Math.max(0, options.tolerance) : 2;
|
options && typeof options.tolerance === "number" ? Math.max(0, options.tolerance) : 2;
|
||||||
|
const proseCoverageFloor =
|
||||||
|
options && typeof options.proseCoverageFloor === "number"
|
||||||
|
? Math.min(1, Math.max(0, options.proseCoverageFloor))
|
||||||
|
: DEFAULT_PROSE_COVERAGE_FLOOR;
|
||||||
const root =
|
const root =
|
||||||
document.querySelector("[data-composition-id][data-width][data-height]") ||
|
document.querySelector("[data-composition-id][data-width][data-height]") ||
|
||||||
document.querySelector("[data-composition-id]") ||
|
document.querySelector("[data-composition-id]") ||
|
||||||
@@ -1437,7 +1438,7 @@
|
|||||||
const clipped = clippedTextIssue(element, time, tolerance);
|
const clipped = clippedTextIssue(element, time, tolerance);
|
||||||
if (clipped) issues.push(clipped);
|
if (clipped) issues.push(clipped);
|
||||||
issues.push(...textOverflowIssues(element, root, rootRect, time, tolerance));
|
issues.push(...textOverflowIssues(element, root, rootRect, time, tolerance));
|
||||||
const occluded = occludedTextIssue(element, time);
|
const occluded = occludedTextIssue(element, time, proseCoverageFloor);
|
||||||
if (occluded) issues.push(occluded);
|
if (occluded) issues.push(occluded);
|
||||||
const invisible = invisibleTextIssue(element, time);
|
const invisible = invisibleTextIssue(element, time);
|
||||||
if (invisible) issues.push(invisible);
|
if (invisible) issues.push(invisible);
|
||||||
|
|||||||
@@ -1786,6 +1786,26 @@ describe("layout-audit.browser occlusion", () => {
|
|||||||
expect(issues.some((issue) => issue.code === "text_occluded")).toBe(false);
|
expect(issues.some((issue) => issue.code === "text_occluded")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("flags ~0.07 prose when proseCoverageFloor is lowered to 0.05", () => {
|
||||||
|
const issues = auditCoverageScene({
|
||||||
|
text: "This paragraph is long enough to read as ordinary prose, not a label.",
|
||||||
|
hitCount: 2,
|
||||||
|
proseCoverageFloor: 0.05,
|
||||||
|
});
|
||||||
|
const occluded = issues.find((issue) => issue.code === "text_occluded");
|
||||||
|
expect(occluded).toBeDefined();
|
||||||
|
expect(occluded?.coveredFraction).toBe(0.07);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still flags an atomic label at ~0.07 when proseCoverageFloor is 0.05", () => {
|
||||||
|
const issues = auditCoverageScene({
|
||||||
|
text: "SUBSCRIBE",
|
||||||
|
hitCount: 2,
|
||||||
|
proseCoverageFloor: 0.05,
|
||||||
|
});
|
||||||
|
expect(issues.some((issue) => issue.code === "text_occluded")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("flags prose once coverage clears the 0.15 floor", () => {
|
it("flags prose once coverage clears the 0.15 floor", () => {
|
||||||
// 5/27 ≈ 0.185, comfortably over the ~0.15 prose floor.
|
// 5/27 ≈ 0.185, comfortably over the ~0.15 prose floor.
|
||||||
const issues = auditCoverageScene({
|
const issues = auditCoverageScene({
|
||||||
@@ -2042,6 +2062,7 @@ function occlusionProbePoints(textRect: RectInput): Array<{ x: number; y: number
|
|||||||
function auditCoverageScene(options: {
|
function auditCoverageScene(options: {
|
||||||
text: string;
|
text: string;
|
||||||
hitCount: number;
|
hitCount: number;
|
||||||
|
proseCoverageFloor?: number;
|
||||||
}): ReturnType<typeof runAudit> {
|
}): ReturnType<typeof runAudit> {
|
||||||
const textRect = { left: 200, top: 500, width: 600, height: 80 };
|
const textRect = { left: 200, top: 500, width: 600, height: 80 };
|
||||||
document.body.innerHTML = `
|
document.body.innerHTML = `
|
||||||
@@ -2065,7 +2086,11 @@ function auditCoverageScene(options: {
|
|||||||
return document.getElementById(isHit ? "overlay" : "headline");
|
return document.getElementById(isHit ? "overlay" : "headline");
|
||||||
};
|
};
|
||||||
installAuditScript();
|
installAuditScript();
|
||||||
return runAudit();
|
return runAudit(
|
||||||
|
options.proseCoverageFloor === undefined
|
||||||
|
? undefined
|
||||||
|
: { proseCoverageFloor: options.proseCoverageFloor },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function auditOcclusionScene(options: {
|
function auditOcclusionScene(options: {
|
||||||
@@ -2318,13 +2343,17 @@ interface AuditIssue {
|
|||||||
coveredFraction?: number;
|
coveredFraction?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function runAudit(): AuditIssue[] {
|
function runAudit(options?: { proseCoverageFloor?: number }): AuditIssue[] {
|
||||||
const audit = (
|
const audit = (
|
||||||
window as unknown as {
|
window as unknown as {
|
||||||
__hyperframesLayoutAudit: (options: { time: number; tolerance: number }) => AuditIssue[];
|
__hyperframesLayoutAudit: (options: {
|
||||||
|
time: number;
|
||||||
|
tolerance: number;
|
||||||
|
proseCoverageFloor?: number;
|
||||||
|
}) => AuditIssue[];
|
||||||
}
|
}
|
||||||
).__hyperframesLayoutAudit;
|
).__hyperframesLayoutAudit;
|
||||||
return audit({ time: 1, tolerance: 2 });
|
return audit({ time: 1, tolerance: 2, ...options });
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectedRangeElement(selected: Node | null): Element | null {
|
function selectedRangeElement(selected: Node | null): Element | null {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import type {
|
|||||||
ContrastAuditEntry,
|
ContrastAuditEntry,
|
||||||
ContrastCapture,
|
ContrastCapture,
|
||||||
GeometryCandidateRequest,
|
GeometryCandidateRequest,
|
||||||
|
LayoutOptions,
|
||||||
MotionSpecResolution,
|
MotionSpecResolution,
|
||||||
OffPivotFrame,
|
OffPivotFrame,
|
||||||
OffPivotRotationSample,
|
OffPivotRotationSample,
|
||||||
@@ -353,7 +354,7 @@ function createPageDriver(page: Page, setTime: (time: number) => void): CheckAud
|
|||||||
setTime(time);
|
setTime(time);
|
||||||
await seekCompositionTimeline(page, time, DENSE_GEOMETRY_SEEK_OPTIONS);
|
await seekCompositionTimeline(page, time, DENSE_GEOMETRY_SEEK_OPTIONS);
|
||||||
},
|
},
|
||||||
collectLayout: (time, tolerance) => collectLayout(page, time, tolerance),
|
collectLayout: (time, tolerance, layout) => collectLayout(page, time, tolerance, layout),
|
||||||
collectOverlap: (time) => collectOverlap(page, time),
|
collectOverlap: (time) => collectOverlap(page, time),
|
||||||
collectLayoutGeometry: () => collectLayoutGeometry(page),
|
collectLayoutGeometry: () => collectLayoutGeometry(page),
|
||||||
collectRotationSample: (time) => collectRotationSample(page, time),
|
collectRotationSample: (time) => collectRotationSample(page, time),
|
||||||
@@ -457,15 +458,22 @@ async function collectLayout(
|
|||||||
page: Page,
|
page: Page,
|
||||||
time: number,
|
time: number,
|
||||||
tolerance: number,
|
tolerance: number,
|
||||||
|
layout?: LayoutOptions,
|
||||||
): Promise<AnchoredLayoutIssue[]> {
|
): Promise<AnchoredLayoutIssue[]> {
|
||||||
const raw = await page.evaluate(
|
const raw = await page.evaluate(
|
||||||
(options: { time: number; tolerance: number }) => {
|
(options: { time: number; tolerance: number; proseCoverageFloor?: number }) => {
|
||||||
const audit = Reflect.get(window, "__hyperframesLayoutAudit");
|
const audit = Reflect.get(window, "__hyperframesLayoutAudit");
|
||||||
if (typeof audit !== "function") return [];
|
if (typeof audit !== "function") return [];
|
||||||
const result = Reflect.apply(audit, window, [options]);
|
const result = Reflect.apply(audit, window, [options]);
|
||||||
return Array.isArray(result) ? result : [];
|
return Array.isArray(result) ? result : [];
|
||||||
},
|
},
|
||||||
{ time, tolerance },
|
{
|
||||||
|
time,
|
||||||
|
tolerance,
|
||||||
|
...(typeof layout?.proseCoverageFloor === "number"
|
||||||
|
? { proseCoverageFloor: layout.proseCoverageFloor }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return anchorLayoutIssues(page, raw.flatMap(parseLayoutIssue));
|
return anchorLayoutIssues(page, raw.flatMap(parseLayoutIssue));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ async function collectGridSamples(
|
|||||||
// that render time — never a stale bbox from an earlier/later sample.
|
// that render time — never a stale bbox from an earlier/later sample.
|
||||||
const issuesAtTime: AnchoredLayoutIssue[] = [];
|
const issuesAtTime: AnchoredLayoutIssue[] = [];
|
||||||
if (layoutSet.has(time)) {
|
if (layoutSet.has(time)) {
|
||||||
const layoutIssues = await driver.collectLayout(time, options.tolerance);
|
const layoutIssues = await driver.collectLayout(time, options.tolerance, options.layout);
|
||||||
collected.layoutIssues.push(...layoutIssues);
|
collected.layoutIssues.push(...layoutIssues);
|
||||||
issuesAtTime.push(...layoutIssues);
|
issuesAtTime.push(...layoutIssues);
|
||||||
collected.geometrySignatures.push(await driver.collectLayoutGeometry());
|
collected.geometrySignatures.push(await driver.collectLayoutGeometry());
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export interface CheckOptions {
|
|||||||
snapshots: boolean;
|
snapshots: boolean;
|
||||||
captionZone?: CaptionZoneOptions;
|
captionZone?: CaptionZoneOptions;
|
||||||
frameCheck?: FrameCheckOptions;
|
frameCheck?: FrameCheckOptions;
|
||||||
|
/** Opt-in layout-audit knobs (`--layout "proseCoverageFloor=0.05"`). Defaults stay in the browser audit. */
|
||||||
|
layout?: LayoutOptions;
|
||||||
/** Explicit --proxy/--no-proxy override; undefined preserves project config. */
|
/** Explicit --proxy/--no-proxy override; undefined preserves project config. */
|
||||||
autoProxy?: boolean;
|
autoProxy?: boolean;
|
||||||
}
|
}
|
||||||
@@ -37,6 +39,12 @@ export interface FrameCheckOptions {
|
|||||||
seek?: number[];
|
seek?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Layout-audit tuning passed through to `__hyperframesLayoutAudit`. All fields optional. */
|
||||||
|
export interface LayoutOptions {
|
||||||
|
/** Prose `text_occluded` coveredFraction floor (0–1; default 0.15); atomic labels still flag at any hit. */
|
||||||
|
proseCoverageFloor?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type CheckSeverity = "error" | "warning" | "info";
|
export type CheckSeverity = "error" | "warning" | "info";
|
||||||
|
|
||||||
export interface CheckBbox {
|
export interface CheckBbox {
|
||||||
@@ -175,7 +183,11 @@ export interface CheckAuditDriver {
|
|||||||
seek(time: number): Promise<void>;
|
seek(time: number): Promise<void>;
|
||||||
/** Settle-free seek for the geometry-only dense content_overlap pass; only collectOverlap consumes it, and getBoundingClientRect is valid synchronously after setTime. */
|
/** Settle-free seek for the geometry-only dense content_overlap pass; only collectOverlap consumes it, and getBoundingClientRect is valid synchronously after setTime. */
|
||||||
seekGeometry(time: number): Promise<void>;
|
seekGeometry(time: number): Promise<void>;
|
||||||
collectLayout(time: number, tolerance: number): Promise<AnchoredLayoutIssue[]>;
|
collectLayout(
|
||||||
|
time: number,
|
||||||
|
tolerance: number,
|
||||||
|
layout?: LayoutOptions,
|
||||||
|
): Promise<AnchoredLayoutIssue[]>;
|
||||||
/** content_overlap only, for the dense re-sampling grid — catches transient text collisions the sparse grid seeks past. */
|
/** content_overlap only, for the dense re-sampling grid — catches transient text collisions the sparse grid seeks past. */
|
||||||
collectOverlap(time: number): Promise<AnchoredLayoutIssue[]>;
|
collectOverlap(time: number): Promise<AnchoredLayoutIssue[]>;
|
||||||
/** Frozen-sweep guard (#U10): an opaque per-sample geometry+opacity
|
/** Frozen-sweep guard (#U10): an opaque per-sample geometry+opacity
|
||||||
|
|||||||
Reference in New Issue
Block a user