Files
wehub-resource-sync 85453da49f
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Docs / Validate docs (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Sync skills to ClawHub / Publish changed skills (push) Waiting to run
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:58:35 +08:00

195 lines
6.6 KiB
JavaScript

#!/usr/bin/env node
/**
* check-overflow.cjs — mode-agnostic frame-overflow WARNING for custom mode.
*
* Template mode has check-occlusion.cjs (which also flags frame-edge overflow),
* but custom mode runs no gates. This is the cheap safety net: it loads the
* rendered index.html, seeks the GSAP timeline across the clip, and flags ANY
* visible text element (regardless of class) whose box leaves the canvas — i.e.
* captions that fall off-frame (the bug we otherwise only catch by eye).
*
* WARNING ONLY — never fails the build (custom designs may bleed intentionally).
* Exit 0 if it ran (with or without findings); exit 3 if it couldn't run.
*
* Usage: node check-overflow.cjs <project-dir>
*/
const path = require("path");
const fs = require("fs");
const os = require("os");
const HF_ROOTS = [
process.env.HYPERFRAMES_ROOT,
path.resolve(__dirname, "../../.."),
path.join(os.homedir(), "Downloads", "hyperframes"),
].filter(Boolean);
let puppeteer = null;
for (const root of HF_ROOTS) {
const cands = [path.join(root, "node_modules", "puppeteer")];
const bunDir = path.join(root, "node_modules", ".bun");
try {
if (fs.existsSync(bunDir)) {
for (const d of fs.readdirSync(bunDir))
if (d.startsWith("puppeteer@"))
cands.push(path.join(bunDir, d, "node_modules", "puppeteer"));
}
} catch {
/* ignore */
}
for (const p of cands) {
try {
if (fs.existsSync(p)) {
puppeteer = require(p);
break;
}
} catch {}
}
if (puppeteer) break;
}
if (!puppeteer) {
console.error("[overflow] puppeteer not found");
process.exit(3);
}
async function main() {
const projectDir = process.argv[2];
const htmlName = process.argv[3] || "index.html"; // Standard mode passes "rail.html" to gate the rail too
const indexPath = path.resolve(projectDir, htmlName);
if (!fs.existsSync(indexPath)) {
console.error(`[overflow] missing ${indexPath}`);
process.exit(2);
}
const html = fs.readFileSync(indexPath, "utf8");
const num = (re, d) => {
const m = html.match(re);
return m ? parseFloat(m[1]) : d;
};
const W = num(/data-width="([0-9.]+)"/, 1920);
const H = num(/data-height="([0-9.]+)"/, 1080);
const DUR = num(/data-duration="([0-9.]+)"/, 8);
const exe =
process.platform === "darwin"
? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
: "/usr/bin/google-chrome";
const browser = await puppeteer.launch({
headless: "new",
executablePath: fs.existsSync(exe) ? exe : undefined,
args: [
"--disable-web-security",
"--allow-file-access-from-files",
`--window-size=${W},${H}`,
"--disable-dev-shm-usage",
],
});
try {
const page = await browser.newPage();
await page.setViewport({ width: Math.round(W), height: Math.round(H), deviceScaleFactor: 1 });
const waitTL = async () => {
const t0 = Date.now();
while (Date.now() - t0 < 12000) {
if (await page.evaluate(() => !!(window.__timelines && window.__timelines.main)))
return true;
await new Promise((r) => setTimeout(r, 200));
}
return false;
};
await page.goto(`file://${indexPath}`, { waitUntil: "load", timeout: 20000 });
let hasTL = await waitTL();
if (!hasTL) {
// GSAP loads from CDN — a blip leaves no timeline; retry once
await page.reload({ waitUntil: "load", timeout: 20000 }).catch(() => {});
hasTL = await waitTL();
}
if (!hasTL) {
// NEVER claim "ok" when we couldn't actually evaluate the animated layout.
console.error(
"[overflow] ⚠ timeline did not register (GSAP CDN blocked?) — overflow check " +
"INCONCLUSIVE; eyeball the render for off-frame captions.",
);
await browser.close();
process.exit(0);
}
await page.evaluate(async () => {
try {
await document.fonts.ready;
} catch {}
});
const times = Array.from({ length: 9 }, (_, i) => +((DUR * i) / 8).toFixed(2));
const found = new Map(); // key text → worst offense
for (const t of times) {
await page.evaluate((t) => {
window.__timelines.main.seek(t);
void document.body.offsetHeight;
}, t);
await new Promise((r) => setTimeout(r, 25));
const offenders = await page.evaluate(
(W, H) => {
const M = 2; // tolerance px
const root = document.querySelector("#stage") || document.body;
const out = [];
for (const el of root.querySelectorAll("*")) {
if (el.tagName === "VIDEO" || el.tagName === "AUDIO") continue;
const own = [...el.childNodes]
.filter((n) => n.nodeType === 3)
.map((n) => n.textContent.trim())
.join(" ")
.trim();
if (!own) continue;
const cs = getComputedStyle(el);
if (
cs.display === "none" ||
cs.visibility === "hidden" ||
parseFloat(cs.opacity) < 0.06
)
continue;
const b = el.getBoundingClientRect();
if (b.width === 0 || b.height === 0) continue;
const off = {
left: Math.max(0, Math.round(-b.left - M)),
right: Math.max(0, Math.round(b.right - W - M)),
top: Math.max(0, Math.round(-b.top - M)),
bottom: Math.max(0, Math.round(b.bottom - H - M)),
};
if (off.left || off.right || off.top || off.bottom)
out.push({ text: own.slice(0, 42), off });
}
return out;
},
W,
H,
);
for (const o of offenders) {
const sides = Object.entries(o.off)
.filter(([, v]) => v > 0)
.map(([s, v]) => `${s} ${v}px`)
.join(", ");
const prev = found.get(o.text);
const score = Object.values(o.off).reduce((a, b) => a + b, 0);
if (!prev || score > prev.score) found.set(o.text, { sides, score, t });
}
}
if (found.size === 0) {
console.log(`[overflow] ok — no caption text leaves the ${W}x${H} frame`);
} else {
console.error(
`[overflow] ⚠ ${found.size} caption(s) leave the frame (custom mode — WARNING only, not blocking):`,
);
for (const [text, info] of found)
console.error(` "${text}" → off-frame: ${info.sides} (@${info.t}s)`);
console.error(
`[overflow] if unintentional, reposition/resize; if it's deliberate bleed, ignore.`,
);
}
} finally {
await browser.close();
}
}
main().catch((e) => {
console.error("[overflow]", e.message);
process.exit(3);
});