mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
* feat(cli): skipContrastInMotion moves to HF_TOOL_CONTEXT check-review The --skip-contrast-in-motion flag and HYPERFRAMES_CHECK_SKIP_CONTRAST_IN_MOTION env give way to a check-review capability in HF_TOOL_CONTEXT, so only a harness turns it on. The skill text and manifest go back to main. * feat(cli): check-review findingFrames saves one plain frame per finding time With HF_TOOL_CONTEXT check-review.findingFrames, every error/warning finding in layout or contrast with a real bbox gets frameTime (the middle sample of its longest held run, by the heldMs gap rule) and frame (the absolute path of an unannotated full frame shared by every finding at that time). One seek and screenshot per distinct time, capped at 60 times with errors first, into the run's snapshot folder; not listed in snapshots, history or events. Without the capability, output is unchanged. * docs(cli): finding-frame comments name the check-review capability, not a flag * fix(cli): validate finding frame times and preserve cancellation * fix(cli): count finding frame progress toward the check deadline
95 lines
3.4 KiB
TypeScript
95 lines
3.4 KiB
TypeScript
import { basename } from "node:path";
|
|
import { longestContiguousRun } from "./layoutAudit.js";
|
|
import type {
|
|
CheckBbox,
|
|
CheckFinding,
|
|
CheckFindingFrameRequest,
|
|
CheckReport,
|
|
} from "./checkTypes.js";
|
|
|
|
export const MAX_FINDING_FRAME_TIMES = 60;
|
|
|
|
type FramedSection = "layout" | "contrast";
|
|
const FRAMED_SECTIONS: readonly FramedSection[] = ["layout", "contrast"];
|
|
|
|
export interface FramedFinding {
|
|
section: FramedSection;
|
|
index: number;
|
|
time: number;
|
|
}
|
|
|
|
export interface FindingFramePlan {
|
|
requests: CheckFindingFrameRequest[];
|
|
findings: FramedFinding[];
|
|
skipped: number;
|
|
}
|
|
|
|
/** Errors then warnings with a real bbox; one frame per distinct time, at most MAX_FINDING_FRAME_TIMES. */
|
|
export function selectFindingFrames(report: CheckReport): FindingFramePlan {
|
|
const candidates = FRAMED_SECTIONS.flatMap((section) =>
|
|
report[section].findings.flatMap((finding, index) =>
|
|
(finding.severity === "error" || finding.severity === "warning") && hasRealBbox(finding.bbox)
|
|
? [{ section, index, finding }]
|
|
: [],
|
|
),
|
|
).sort((a, b) => severityRank(a.finding) - severityRank(b.finding));
|
|
const times = new Set<number>();
|
|
const findings: FramedFinding[] = [];
|
|
let skipped = 0;
|
|
for (const { section, index, finding } of candidates) {
|
|
const time = middleOfLongestRun(finding.times ?? [finding.time]);
|
|
if (time === undefined) continue;
|
|
if (!times.has(time) && times.size >= MAX_FINDING_FRAME_TIMES) {
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
times.add(time);
|
|
findings.push({ section, index, time });
|
|
}
|
|
const requests = [...times]
|
|
.sort((a, b) => a - b)
|
|
.map((time, order) => ({ time, filename: findingFrameFilename(order, time) }));
|
|
return { requests, findings, skipped };
|
|
}
|
|
|
|
function severityRank(finding: CheckFinding): number {
|
|
return finding.severity === "error" ? 0 : 1;
|
|
}
|
|
|
|
function hasRealBbox(bbox: CheckBbox): boolean {
|
|
return bbox.width > 0 && bbox.height > 0;
|
|
}
|
|
|
|
/** The middle sample of the longest held run (the `heldMs` gap rule); the earlier of two middles. */
|
|
export function middleOfLongestRun(times: number[]): number | undefined {
|
|
const run = longestContiguousRun(times.filter((time) => Number.isFinite(time) && time >= 0));
|
|
return run[Math.floor((run.length - 1) / 2)];
|
|
}
|
|
|
|
function findingFrameFilename(order: number, time: number): string {
|
|
return `finding-frame-${String(order).padStart(2, "0")}-at-${time.toFixed(3)}s.png`;
|
|
}
|
|
|
|
/** Copies each framed finding with its frame path and time; every other field is untouched. */
|
|
export function withFindingFrameFields(
|
|
report: CheckReport,
|
|
plan: FindingFramePlan,
|
|
written: string[],
|
|
): CheckReport {
|
|
const pathOf = new Map(written.map((path) => [basename(path), path]));
|
|
const filenameOf = new Map(plan.requests.map((request) => [request.time, request.filename]));
|
|
const next: CheckReport = { ...report };
|
|
for (const section of FRAMED_SECTIONS) {
|
|
const framed = plan.findings.filter((entry) => entry.section === section);
|
|
if (framed.length === 0) continue;
|
|
const findings: CheckFinding[] = [...report[section].findings];
|
|
for (const entry of framed) {
|
|
const frame = pathOf.get(filenameOf.get(entry.time) ?? "");
|
|
const finding = findings[entry.index];
|
|
if (frame && finding) findings[entry.index] = { ...finding, frame, frameTime: entry.time };
|
|
}
|
|
Object.assign(next, { [section]: { ...report[section], findings } });
|
|
}
|
|
return next;
|
|
}
|