mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(cli): persistence-tiered findings, frozen-sweep guard, occlusion coverage
Layout findings now distinguish held defects from entrance/exit transients: a dynamic issue seen at a single grid sample demotes to info, while content_overlap held across two-plus samples (or 500ms+) promotes to error, resolving the long-standing re-promotion TODO. Static compositions keep their severity. check gains a sweep_static error when a 3s+ composition shows zero geometry change across every sample (a frozen timeline makes every green verdict unreliable); skipped when the motion sidecar already reported motion_frozen. text_occluded findings carry a coveredFraction; atomic labels (short, no whitespace) flag on any cover while prose needs 15%, since partial cover changes what a short label reads as. Deprecation-test scaffolding consolidates into deprecationTestHarness; tier logic and logger tests restructured under the complexity gate without suppression markers. Detection mechanics adapted from Adam Rosler's open-sourced visual-linter design (github.com/Adam-Rosler/hyperframes-visual-linter-design); the elementFromPoint paint model, opt-out attributes, and single-audit architecture are unchanged.
This commit is contained in:
@@ -18,6 +18,9 @@ export type LayoutIssueCode =
|
||||
| "text_occluded"
|
||||
| "caption_zone_collision"
|
||||
| "frame_out_of_frame"
|
||||
// Frozen-sweep guard (#U10) — a whole-run meta-finding, not a per-sample
|
||||
// geometry observation; never persistence-tiered (see `applyPersistenceTier`).
|
||||
| "sweep_static"
|
||||
// Motion-verification findings (#1437) — evaluated against the seeked timeline.
|
||||
| "motion_appears_late"
|
||||
| "motion_out_of_order"
|
||||
@@ -42,6 +45,9 @@ export interface LayoutIssue {
|
||||
rect: LayoutRect;
|
||||
containerRect?: LayoutRect;
|
||||
overflow?: LayoutOverflow;
|
||||
/** `text_occluded` only: approximate fraction (0-1) of the occlusion probe
|
||||
* grid that hit an opaque occluder — see layout-audit.browser.js. */
|
||||
coveredFraction?: number;
|
||||
fixHint?: string;
|
||||
}
|
||||
|
||||
@@ -164,7 +170,44 @@ export function dedupeLayoutIssues(issues: LayoutIssue[]): LayoutIssue[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function collapseStaticLayoutIssues(issues: LayoutIssue[]): LayoutIssue[] {
|
||||
// Persistence-tier thresholds (#U10, adapted from Adam Rosler's visual-linter
|
||||
// design). The approach doc frames these as held-duration floors — ignore
|
||||
// under ~250ms, re-promote content_overlap at >= ~500ms — measured against
|
||||
// the SAME firstSeen/lastSeen span this collapse step already tracks. At the
|
||||
// default 9-sample grid over a multi-second composition, a single collapsed
|
||||
// occurrence is held 0ms (one entrance/exit transient sample) and two
|
||||
// collapsed occurrences are already >= one sample-to-sample gap, which is
|
||||
// well past 500ms — so "held under 250ms" reduces to `occurrences <= 1` and
|
||||
// "held >= 500ms" reduces to `occurrences >= 2`. Tiering below is written in
|
||||
// those sample-count terms (the mapping the approach doc asks to document),
|
||||
// with the literal ms span (CONTENT_OVERLAP_HELD_ERROR_MS) kept as a fallback
|
||||
// for callers whose samples really are spaced close enough together for the
|
||||
// ms floor to matter on its own (dense `--at`/`--at-transitions` runs). The
|
||||
// ~250ms ignore floor needs no separate constant — see the occurrences <= 1
|
||||
// branch below.
|
||||
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
|
||||
// findings — the ones this collapse step's firstSeen/lastSeen span was built
|
||||
// to describe. `caption_zone_collision`/`frame_out_of_frame` (a different
|
||||
// script, U3) and the `motion_*`/`sweep_static` codes (evaluated once over
|
||||
// the whole run, not per grid sample) already carry their own singular
|
||||
// dedupe/severity semantics; re-interpreting their occurrence count as a
|
||||
// held-duration signal would misread it.
|
||||
const PERSISTENCE_TIERED_CODES: ReadonlySet<LayoutIssueCode> = new Set([
|
||||
"text_box_overflow",
|
||||
"clipped_text",
|
||||
"canvas_overflow",
|
||||
"container_overflow",
|
||||
"content_overlap",
|
||||
"text_occluded",
|
||||
]);
|
||||
|
||||
export function collapseStaticLayoutIssues(
|
||||
issues: LayoutIssue[],
|
||||
totalSampleCount?: number,
|
||||
): LayoutIssue[] {
|
||||
const groups = new Map<
|
||||
string,
|
||||
{
|
||||
@@ -193,13 +236,57 @@ export function collapseStaticLayoutIssues(issues: LayoutIssue[]): LayoutIssue[]
|
||||
existing.occurrences += 1;
|
||||
}
|
||||
|
||||
return [...groups.values()].map(({ issue, firstSeen, lastSeen, occurrences }) => ({
|
||||
...issue,
|
||||
time: firstSeen,
|
||||
firstSeen,
|
||||
lastSeen,
|
||||
occurrences,
|
||||
}));
|
||||
// A run that only ever sampled one point in time can't distinguish a
|
||||
// transient from a persistent finding — skip tiering entirely rather than
|
||||
// guess (see `applyPersistenceTier`).
|
||||
const sampleCount = totalSampleCount ?? new Set(issues.map((issue) => issue.time)).size;
|
||||
const multiSampleRun = sampleCount > 1;
|
||||
|
||||
return [...groups.values()].map(({ issue, firstSeen, lastSeen, occurrences }) =>
|
||||
applyPersistenceTier(
|
||||
{ ...issue, time: firstSeen, firstSeen, lastSeen, occurrences },
|
||||
multiSampleRun,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Held-duration severity tiering (#U10). A finding observed at only one
|
||||
* sample among several (held 0ms) is an entrance/exit transient, not a held
|
||||
* defect — demote to info so it stays in the data (verbose/--json output)
|
||||
* without gating the run. `content_overlap` specifically re-promotes from
|
||||
* warning to error once it's held long enough to be a real, sustained
|
||||
* collision rather than a crossfade/transition blip (resolves the TODO in
|
||||
* layout-audit.browser.js's `overlapIssue`). A finding held at every sample
|
||||
* (a genuinely static defect) is well past both thresholds and is left
|
||||
* untouched either way — persistence, not the code, decides the tier.
|
||||
*/
|
||||
function applyPersistenceTier(issue: LayoutIssue, multiSampleRun: boolean): LayoutIssue {
|
||||
if (!multiSampleRun) return issue;
|
||||
if (!PERSISTENCE_TIERED_CODES.has(issue.code)) return issue;
|
||||
|
||||
const occurrences = issue.occurrences ?? 1;
|
||||
// A single collapsed occurrence is held 0ms by construction (firstSeen ===
|
||||
// lastSeen) — always under the ignore floor, so occurrences <= 1 is a
|
||||
// complete (not approximate) test for "held under 250ms".
|
||||
if (occurrences <= 1) {
|
||||
return { ...issue, severity: "info" };
|
||||
}
|
||||
if (issue.code === "content_overlap" && isContentOverlapHeldLongEnough(issue, occurrences)) {
|
||||
return { ...issue, severity: "error" };
|
||||
}
|
||||
return issue;
|
||||
}
|
||||
|
||||
// Split out of applyPersistenceTier so the two independent "held long enough"
|
||||
// signals (sample count vs. wall-clock span) read as one boolean question
|
||||
// instead of adding a third compound branch to the tiering ladder above.
|
||||
function isContentOverlapHeldLongEnough(issue: LayoutIssue, occurrences: number): boolean {
|
||||
if (occurrences >= HELD_ACROSS_SAMPLES_MIN_OCCURRENCES) return true;
|
||||
const firstSeen = issue.firstSeen ?? issue.time;
|
||||
const lastSeen = issue.lastSeen ?? issue.time;
|
||||
const heldMs = (lastSeen - firstSeen) * 1000;
|
||||
return heldMs >= CONTENT_OVERLAP_HELD_ERROR_MS;
|
||||
}
|
||||
|
||||
export function limitLayoutIssues(
|
||||
|
||||
Reference in New Issue
Block a user