feat(cli): capture reports why a referenced asset is not in the folder (#3598)

Capture drops assets for four reasons and reported none of them, so a folder
with thirty images and a folder truncated to thirty images were the same
object. Every drop site was a bare `continue`, `break`, `return null` or an
empty `catch`, and the only signals downstream were two hand-written warning
strings that fired when the budget was already gone before a download pass
started, which is the one case where the pass could not say how much it lost.

`downloadAssets` and `downloadAndRewriteFonts` now return an `AssetDropCounts`
tally beside their result, incremented at the single line that performs each
drop. `capture --json` carries it as `dropped`; the human summary prints a
`Dropped:` line when it is non-zero.

Four reasons, three decisions and one failure:

  size-floor        fetched, then judged too small to be a real asset
  budget-exhausted  the post-navigation clock ran out before this one
  cap-reached       30 inline SVGs, 30 fonts, or 6 faces per family
  unavailable       the request or the write failed

A break now counts everything it did not reach rather than the one it stopped
on, because "how many did we lose" is the question and one is never the answer.

The two budget warnings are gone. Both existed only to cover the case where
the budget ran out before a pass was called, so both passes are now called
unconditionally: a zero budget makes each loop break on its first item and
record `budget-exhausted` for the rest, which costs no network and produces a
real number instead of the word "some". The single remaining warning is derived
from the tally, so the prose and the count cannot disagree.

Measured on a live capture of a large marketing site:

  default budget    232 kept, 91 dropped (39 size-floor, 20 cap-reached,
                    32 unavailable)
  15s budget         30 kept, 299 dropped (279 budget-exhausted, 20 cap-reached)

Same page, same command, and until now both runs described themselves the same
way.
This commit is contained in:
Miguel Ángel
2026-09-02 23:24:59 -04:00
committed by GitHub
parent 84ed587f33
commit ae7e530f2d
6 changed files with 358 additions and 61 deletions
+27 -1
View File
@@ -186,10 +186,35 @@ npx hyperframes capture https://example.com --json
Screenshots: 12
Assets: 45
Dropped: 9 (6 size-floor, 3 cap-reached)
Sections: 15
Fonts: sohne-var
```
`Dropped` is how many assets the page referenced that are **not** in the
folder, and why. It is printed only when it is non-zero, and `--json` always
carries it as a `dropped` object. Without it, a capture of a spare page and a
capture that a limit truncated are the same three-line summary, and the only
way to tell them apart is to open the page yourself.
| Reason | What it means |
| ------------------ | ------------------------------------------------------------------------------------------------------ |
| `size-floor` | Fetched, then judged too small to be a real asset rather than a spacer or tracking pixel. |
| `budget-exhausted` | `--capture-budget` ran out before this one was reached. Raise it, or pass `--skip-vision` to buy time. |
| `cap-reached` | A per-run or per-family limit was already met: 30 inline SVGs, 30 fonts, 6 faces per family. |
| `unavailable` | The request or the write failed: network error, timeout, refused address, bad status, disk. |
Three of those are decisions the capture made and one is a failure it hit, so a
run that is thin with an all-zero `dropped` is a thin page, and a run that is
thin with counts on it was cut short.
```json
{
"assets": 45,
"dropped": { "size-floor": 6, "budget-exhausted": 0, "cap-reached": 3, "unavailable": 0 }
}
```
| Flag | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--output, -o` | Output directory. Default `./capture`, then `./capture-2/`, `./capture-3/`, … if that name is taken. |
@@ -207,7 +232,8 @@ metadata, and contact sheets — plus whatever Lottie, video, and WebGL context
the page exposed. It is raw material for an agent, not a finished composition;
the `/product-launch-video` workflow uses it when a real product has to appear
on screen. Dynamic sites, protected pages, and unusual media loaders produce
partial results, so read the warnings and contact sheets before you build.
partial results, so read `dropped`, the warnings, and the contact sheets before
you build.
For AI image descriptions, set `GEMINI_API_KEY` in a `.env` file
(~$0.001/image), or `OPENROUTER_API_KEY` to route any vision model through