Add HyperFrames skills
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011h6NB8JQtfPc6eRCefEzBB
This commit is contained in:
964 files changed
+142321
No files matched your search
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* check-rail-climax.cjs — enforce the Rail ↔ climax hand-off (PIPELINE.md).
|
||||
*
|
||||
* node check-rail-climax.cjs <project-dir>
|
||||
*
|
||||
* The embed climax is a PROMOTED word — lifted out of the rail into the hero
|
||||
* layer. It must NEVER also be revealed in the rail. This gate loads index.html
|
||||
* (the climax) and rail.html (the verbatim rail) in headless Chromium, finds the
|
||||
* climax's on-screen window + its word(s), then checks whether the rail reveals
|
||||
* any of those same words DURING that window. If it does, the promoted word is
|
||||
* duplicated on screen → exit 2 (the render aborts). Standard mode only.
|
||||
*
|
||||
* Conservative by design: only a CONFIRMED duplicate fails. Anything we can't
|
||||
* determine (no rail.html, no puppeteer, timeline never registers, no climax)
|
||||
* exits 0 — infra problems never block a render.
|
||||
*/
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
|
||||
const HF_ROOTS = [
|
||||
process.argv[2] && path.resolve(process.argv[2]), // project-owned helper dependencies
|
||||
process.env.HYPERFRAMES_ROOT,
|
||||
path.resolve(__dirname, "../../.."),
|
||||
path.join(os.homedir(), "Downloads", "hyperframes"),
|
||||
].filter(Boolean);
|
||||
|
||||
function findInBun(root, pkg, sub) {
|
||||
const cands = [path.join(root, "node_modules", pkg)];
|
||||
const bunDir = path.join(root, "node_modules", ".bun");
|
||||
try {
|
||||
if (fs.existsSync(bunDir))
|
||||
for (const d of fs.readdirSync(bunDir))
|
||||
if (d.startsWith(pkg + "@")) cands.push(path.join(bunDir, d, "node_modules", pkg));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
for (const c of cands) {
|
||||
const p = sub ? path.join(c, sub) : c;
|
||||
if (fs.existsSync(p)) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let puppeteer = null,
|
||||
gsapSource = null;
|
||||
for (const root of HF_ROOTS) {
|
||||
if (!puppeteer) {
|
||||
const p = findInBun(root, "puppeteer");
|
||||
if (p) {
|
||||
try {
|
||||
puppeteer = require(p);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
if (!gsapSource) {
|
||||
const g = findInBun(root, "gsap", path.join("dist", "gsap.min.js"));
|
||||
if (g) gsapSource = fs.readFileSync(g, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
const norm = (s) =>
|
||||
String(s)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, "");
|
||||
const toks = (s) =>
|
||||
String(s)
|
||||
.split(/\s+/)
|
||||
.map(norm)
|
||||
.filter((t) => t.length >= 2);
|
||||
|
||||
function ok(msg) {
|
||||
if (msg) console.log(msg);
|
||||
process.exit(0);
|
||||
} // can't-determine / pass → never blocks
|
||||
function fail(msg) {
|
||||
console.error(msg);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
async function newPage(browser, W, H) {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: W, height: H, deviceScaleFactor: 1 });
|
||||
if (gsapSource) {
|
||||
await page.evaluateOnNewDocument(gsapSource);
|
||||
await page.setRequestInterception(true);
|
||||
page.on("request", (req) => {
|
||||
const u = req.url();
|
||||
if (req.resourceType() === "script" && /gsap/i.test(u) && /^https?:/i.test(u)) req.abort();
|
||||
else req.continue();
|
||||
});
|
||||
}
|
||||
return page;
|
||||
}
|
||||
async function load(page, file) {
|
||||
await page.goto(`file://${file}`, { waitUntil: "load", timeout: 15000 });
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < 12000) {
|
||||
if (await page.evaluate(() => !!(window.__timelines && window.__timelines.main))) {
|
||||
try {
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
});
|
||||
} catch {}
|
||||
return true;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// visible text of a selector at time t, by seeking the page's main timeline.
|
||||
// Visibility must be EFFECTIVE (own opacity × every ancestor's): a rail line that
|
||||
// fades out as a CONTAINER leaves its word spans at opacity:1 — checking only the
|
||||
// span's own style false-positives on text that is actually invisible.
|
||||
async function visibleAt(page, t, selector, childSel) {
|
||||
return page.evaluate(
|
||||
(t, selector, childSel) => {
|
||||
const tl = window.__timelines.main;
|
||||
tl.seek(t);
|
||||
void document.body.offsetHeight;
|
||||
const eff = (el) => {
|
||||
let o = 1,
|
||||
n = el;
|
||||
while (n && n.nodeType === 1) {
|
||||
const cs = getComputedStyle(n);
|
||||
if (cs.display === "none" || cs.visibility === "hidden") return 0;
|
||||
o *= +cs.opacity;
|
||||
n = n.parentElement;
|
||||
}
|
||||
return o;
|
||||
};
|
||||
const out = [];
|
||||
for (const el of document.querySelectorAll(selector)) {
|
||||
if (eff(el) < 0.05) continue;
|
||||
const kids = childSel ? [...el.querySelectorAll(childSel)] : [];
|
||||
if (kids.length) {
|
||||
for (const k of kids) {
|
||||
if (eff(k) > 0.05) out.push(k.textContent);
|
||||
}
|
||||
} else if ((el.textContent || "").trim()) out.push(el.textContent);
|
||||
}
|
||||
return out.join(" ").trim();
|
||||
},
|
||||
t,
|
||||
selector,
|
||||
childSel,
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const project = path.resolve(process.argv[2] || "");
|
||||
if (!process.argv[2]) ok("[rail-climax] usage: check-rail-climax.cjs <project-dir>");
|
||||
const indexPath = path.join(project, "index.html");
|
||||
const railPath = path.join(project, "rail.html");
|
||||
if (!fs.existsSync(railPath) || !fs.existsSync(indexPath))
|
||||
ok("[rail-climax] no rail.html+index.html — not Standard, skipping");
|
||||
if (!puppeteer)
|
||||
ok(
|
||||
"[rail-climax] puppeteer unavailable — skipping (install project dependencies per embedded-captions SKILL.md Runtime prerequisites)",
|
||||
);
|
||||
|
||||
const exe =
|
||||
process.platform === "darwin"
|
||||
? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
: "/usr/bin/google-chrome";
|
||||
let browser;
|
||||
try {
|
||||
browser = await puppeteer.launch({
|
||||
headless: "new",
|
||||
executablePath: fs.existsSync(exe) ? exe : undefined,
|
||||
args: ["--disable-web-security", "--allow-file-access-from-files", "--disable-dev-shm-usage"],
|
||||
});
|
||||
} catch (e) {
|
||||
ok(`[rail-climax] could not launch Chromium — skipping (${e.message})`);
|
||||
}
|
||||
|
||||
try {
|
||||
// index.html → climax window + climax word tokens
|
||||
const ip = await newPage(browser, 1920, 1080);
|
||||
if (!(await load(ip, indexPath)))
|
||||
ok("[rail-climax] index timeline never registered — skipping");
|
||||
const dur =
|
||||
+(await ip.evaluate(() => {
|
||||
const r = document.querySelector("#root") || document.querySelector("[data-duration]");
|
||||
return (r && r.dataset && r.dataset.duration) || 0;
|
||||
})) || 30;
|
||||
const STEP = 0.1;
|
||||
const climaxTokens = new Set();
|
||||
let winIn = null,
|
||||
winOut = null;
|
||||
for (let t = 0; t <= dur + 0.001; t += STEP) {
|
||||
const txt = await visibleAt(ip, +t.toFixed(2), ".climax", "span,.w");
|
||||
if (txt) {
|
||||
toks(txt).forEach((w) => climaxTokens.add(w));
|
||||
if (winIn === null) winIn = t;
|
||||
winOut = t;
|
||||
}
|
||||
}
|
||||
if (!climaxTokens.size || winIn === null)
|
||||
ok("[rail-climax] no visible climax found in index.html — skipping");
|
||||
|
||||
// rail.html → words visible during [winIn, winOut]
|
||||
const rp = await newPage(browser, 1920, 1080);
|
||||
if (!(await load(rp, railPath))) ok("[rail-climax] rail timeline never registered — skipping");
|
||||
const hits = new Map(); // token -> first t seen
|
||||
for (let t = Math.max(0, winIn - STEP); t <= winOut + 0.001; t += STEP) {
|
||||
const railToks = new Set(toks(await visibleAt(rp, +t.toFixed(2), ".w", null)));
|
||||
for (const w of climaxTokens) if (railToks.has(w) && !hits.has(w)) hits.set(w, +t.toFixed(2));
|
||||
}
|
||||
|
||||
if (hits.size) {
|
||||
const list = [...hits.entries()]
|
||||
.map(([w, t]) => `"${w}" (rail shows it at t=${t}s)`)
|
||||
.join(", ");
|
||||
fail(
|
||||
`[rail-climax] ✗ DUPLICATE PROMOTED WORD: the climax word(s) ${[...climaxTokens].map((w) => `"${w}"`).join(", ")} ` +
|
||||
`are on screen during the climax window [${winIn.toFixed(2)}–${winOut.toFixed(2)}s], ` +
|
||||
`and the rail ALSO reveals: ${list}.\n` +
|
||||
` The promoted word must be handed off, never duplicated — see PIPELINE.md "Rail ↔ climax hand-off":\n` +
|
||||
` freeze the rail before the promoted word, let the climax carry it, hold the climax across the rail's\n` +
|
||||
` page-flip, and exit it at the end of the thought. The rail must never reveal the promoted word.`,
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`[rail-climax] ✓ ok — promoted word(s) ${[...climaxTokens].map((w) => `"${w}"`).join(", ")} not duplicated in the rail (window ${winIn.toFixed(2)}–${winOut.toFixed(2)}s)`,
|
||||
);
|
||||
} finally {
|
||||
await Promise.race([browser.close().catch(() => {}), new Promise((r) => setTimeout(r, 8000))]);
|
||||
}
|
||||
}
|
||||
main()
|
||||
.then(() => process.exit(0))
|
||||
.catch((e) => {
|
||||
console.error(`[rail-climax] (skipped — ${e.message})`);
|
||||
process.exit(0);
|
||||
});
|
||||
Reference in new issue
Block a user