docs(skills): gate blocked website captures

This commit is contained in:
Miguel Ángel
2026-07-31 20:30:07 +00:00
parent 9ae0007261
commit 30f3830741
5 changed files with 96 additions and 8 deletions
+23 -3
View File
@@ -52,15 +52,35 @@ Goal: Collect the source material, brand signals, and usable assets for the vide
Classify the input and choose the path. Explicit URL -> capture it and use the site for narration and assets. Pasted script/brief -> save verbatim as `user_script.txt`; `VO_MODE` (verbatim or restructured) comes from `BRIEF.md` — the intent layer asks it when a script arrives (ask once here only if the brief somehow lacks it). Then resolve capture target: URL in text -> use it; brand name only -> `WebSearch`, confirm URL in one line, then crawl; no URL/site (or the brief says don't scrape) -> no-capture path.
Run capture with: `npx hyperframes capture "<URL>" -o ./capture`
Run capture with: `npx hyperframes capture "<URL>" -o ./capture --json`. Keep the default
post-navigation budget unless the caller owns a smaller deadline; then pass a positive
`--capture-budget <milliseconds>` that leaves time for downstream work. `--timeout` controls page
navigation only. Use `--skip-vision` only when optional image captioning is intentionally disabled.
For a site tour or show-it-as-is brief, the captured page is the visual source of truth. Use the real screenshot instead of rebuilding the full website in HTML. If the shot needs internal movement, keep the screenshot as the base and overlay real captured assets at measured positions, or rebuild only the one component that moves. For a scroll shot, animate the viewport over `capture/screenshots/full-page.png` — the 1x plate of the whole document, pixel-exact for a 1920-wide viewport travelling down it. It is absent when the page was too tall to capture in one piece; fall back to the overlapping scroll-position shots in the same directory. Pushing in past 1:1 wants its own 2x capture of that region instead, since the plate has no headroom above 1x. Recreate the whole page only when the user explicitly asks for a stylized interpretation or the capture is unusable.
Inspect the command result and output directory immediately. A non-zero exit, JSON `ok: false`, or
`capture/BLOCKED.md` is a **hard stop** for the capture path: report the recorded reason and do not
consume partial screenshots, DOM, tokens, or assets. Do not manufacture a synthetic no-capture
fallback after a failed URL capture. Continue through the no-capture path only when the original
brief supplied the source material, or when the user explicitly switches to a provided screenshot
or brief after the failure.
Warnings such as `very little text content` together with an empty asset catalog are not proof of a
usable page. For a site tour or show-it-as-is brief, require trustworthy captured structure or a
provided screenshot; if neither exists, stop. Do not invent or rebuild the page merely because the
capture is unusable.
For a site tour or show-it-as-is brief, the captured page is the visual source of truth. Use the real screenshot instead of rebuilding the full website in HTML. If the shot needs internal movement, keep the screenshot as the base and overlay real captured assets at measured positions, or rebuild only the one component that moves. For a scroll shot, animate the viewport over `capture/screenshots/full-page.png` — the 1x plate of the whole document, pixel-exact for a 1920-wide viewport travelling down it. It is absent when the page was too tall to capture in one piece; fall back to the overlapping scroll-position shots in the same directory. Pushing in past 1:1 wants its own 2x capture of that region instead, since the plate has no headroom above 1x. Recreate the whole page only when the user explicitly asks for a stylized interpretation; an unusable capture alone is not authorization.
If `GEMINI_API_KEY`, `GOOGLE_API_KEY`, or an OpenRouter key exists, capture auto-captions assets into `capture/extracted/asset-descriptions.md`. This is not a review gate. Without a vision key, use DOM context and continue.
No-capture path: create `capture/extracted/tokens.json`, `capture/extracted/visible-text.txt`, `capture/extracted/asset-descriptions.md`, and `capture/assets/` by hand. `tokens.json` should be `{ "title": "", "description": "", "colors": [], "fonts": [] }`; fill title/description from the brief when possible. `visible-text.txt` contains the full brief or script. `asset-descriptions.md` should say no assets were captured unless the user gave asset notes.
**Gate:** `capture/extracted/tokens.json`, `capture/extracted/visible-text.txt`, `capture/extracted/asset-descriptions.md`, and `capture/assets/` exist; you can state the brand in one clear sentence. Treat `asset-descriptions.md` as the main asset inventory. If it is missing after real capture, stop and report capture incomplete. If `capture/BLOCKED.md` exists, follow it.
**Gate:** capture JSON reported `ok: true`; `capture/BLOCKED.md` does not exist;
`capture/extracted/tokens.json`, `capture/extracted/visible-text.txt`,
`capture/extracted/asset-descriptions.md`, and `capture/assets/` exist; and you can state the brand in
one clear sentence. Treat `asset-descriptions.md` as the main asset inventory. If it is missing after
real capture, stop and report capture incomplete. Warnings about a degraded optional phase are
acceptable only when this structural gate still passes.
---
@@ -0,0 +1,39 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { fileURLToPath } from "node:url";
function read(relativePath) {
return readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), "utf8");
}
test("product launch capture treats blocked output as a hard gate", () => {
const skill = read("../SKILL.md");
assert.match(skill, /hyperframes capture[^\n]+--json/);
assert.match(skill, /capture\/BLOCKED\.md[^\n]+hard stop/i);
assert.match(skill, /do not[\s\S]{0,120}synthetic[\s\S]{0,120}fallback/i);
assert.match(skill, /very little text[\s\S]{0,180}empty asset/i);
assert.match(skill, /show-it-as-is[\s\S]{0,240}provided screenshot/i);
});
test("CLI capture reference documents the two budgets and machine diagnostics", () => {
const reference = read("../../hyperframes-cli/references/init-and-scaffold.md");
assert.match(reference, /--capture-budget/);
assert.match(reference, /--skip-vision/);
assert.match(reference, /HYPERFRAMES_CAPTURE_PHASE/);
assert.match(reference, /BLOCKED\.md[^\n]+hard stop/i);
assert.match(reference, /--timeout[^\n]+navigation[^\n]+--capture-budget/i);
assert.match(reference, /fresh output directory/i);
assert.match(reference, /outer caller timeout[\s\S]{0,180}does not prove/i);
});
test("webpage motion workflow refuses blocked capture artifacts", () => {
const module = read("../../motion-graphics/categories/webpage/module.md");
assert.match(module, /BLOCKED\.md[\s\S]{0,120}stop/i);
assert.match(module, /provided[\s\S]{0,120}screenshot[\s\S]{0,120}explicit/i);
assert.match(module, /do not[^\n]+blocked[^\n]+capture/i);
assert.match(module, /asset-free fallback/i);
});