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:
Miguel Angel Simon Sierra
2026-07-10 13:27:52 -04:00
parent f4cef54b8b
commit 94f6de8b8d
14 changed files with 712 additions and 238 deletions
+12
View File
@@ -256,6 +256,7 @@ function createPageDriver(page: Page, setTime: (time: number) => void): CheckAud
await seekCompositionTimeline(page, time, SEEK_OPTIONS);
},
collectLayout: (time, tolerance) => collectLayout(page, time, tolerance),
collectLayoutGeometry: () => collectLayoutGeometry(page),
collectGeometryCandidates: (time, request) => collectGeometryCandidates(page, time, request),
collectMotionFrame: (time, selectors, scopes) =>
collectMotionFrame(page, time, selectors, scopes),
@@ -368,6 +369,15 @@ async function collectLayout(
return anchorLayoutIssues(page, raw.flatMap(parseLayoutIssue));
}
async function collectLayoutGeometry(page: Page): Promise<string> {
return page.evaluate(() => {
const geometry = Reflect.get(window, "__hyperframesLayoutGeometry");
if (typeof geometry !== "function") return "";
const result = Reflect.apply(geometry, window, []);
return typeof result === "string" ? result : "";
});
}
async function collectGeometryCandidates(
page: Page,
time: number,
@@ -814,6 +824,8 @@ function assignOptionalLayoutFields(issue: LayoutIssue, value: Record<string, un
if (containerRect) issue.containerRect = containerRect;
const overflow = parseOverflow(Reflect.get(value, "overflow"));
if (overflow) issue.overflow = overflow;
const coveredFraction = numberValue(value, "coveredFraction");
if (coveredFraction !== null) issue.coveredFraction = coveredFraction;
}
function recordField(value: unknown, key: string): Record<string, unknown> | null {