/visual-evidence — mine a company's visual evidence (blind)
Turn visual-evidence X into store/<domain>/visual.md: cited, blind, falsifiable evidence cards + a prose impression a creative director reads in ~5 seconds. The contract is modules/VISUAL.md — read it first (the card schema, the closed sets, and the boundary). This skill is the recipe half; the lint is scripts/visualcheck.py.
What this does NOT do: no score, no quality field, no ranking. If you catch yourself wanting a number, stop — that's the parked layer (BACKLOG). visualcheck.py fails the file if a score: field appears.
The whole game is blinding. The miners must judge only what's visible — never the company's reputation, dossier, or live site. So: do not read profile.md, Notion, or the live web during this run, and synthesize the impression only from the returned cards. The capturing agent saw the dossier; that's why visual evidence is a separate, blind pass, never folded into /research-company.
Resolve the engine root + the capture
ENGINE="$(cd "$(dirname "$(realpath "$0")")/../.." 2>/dev/null && pwd)" # the repo holding this skill
# canonical fallback: "/Users/brianbolze/Library/Mobile Documents/com~apple~CloudDocs/Web Research"
cd "$ENGINE" # run every `python3 scripts/…` command below from here — the paths are engine-root-relative
The company must already be captured. store/<slug>/captures/<date>/.payloads/<page>.png is the input. No capture → tell the user to run /research-company <domain> first (this skill never scrapes).
After resolving the slug and confirming the capture exists, stamp the run clock:
RUN_STARTED_AT="$(python3 "$WEB_RESEARCH_HOME/scripts/runrecord.py" now)"
The loop
1. Tile the cached screenshots (Tier-A, free).
python3 scripts/tile.py --slug <slug> --pages homepage pricing <…> # curate to real page screenshots
Pick the pages that carry the visual system (homepage + 2–4 signal pages); skip capture-experiment variants (homepage_enhanced_lazyload.png, etc.). Tiles + overview-480w.png land in captures/<date>/tiles/.
2. QA gate — clean tiles, or remediate (vision, the load-bearing step). Read each page's overview-480w.png and spot-check tiles. Capture hygiene is phase one, not cleanup — a contaminated tile is unusable evidence, not a poor-design example. Flag:
- modal / cookie banner / newsletter overlay covering content,
- grey or blank hero (WebGL/canvas that didn't render), black media/video cards, lazy-load gaps,
- mid-animation capture (faded reveals, count-ups still at 0.0), full-page compositing artifacts (repeated hero).
For each flagged page, decide:
- Exclude the contaminated tile(s) if the rest of the page is clean → note them;
qa_status: exclusions-noted. - Re-render (Tier-B) when the page's evidence depends on a region the cached shot got statically wrong — grey/WebGL hero, black media, lazy-load gaps, unsettled animation — or an overlay covers the content. Add
--dismissfor the overlay case:python3 "$ENGINE/scripts/shoot.py" "https://<page url>" --out-dir "$ENGINE/store/<slug>/captures/<source-capture>/tiles/<page>" [--dismiss]shoot.pydrives system Chrome (real WebGL), warm-scrolls to load lazy media, then reduced-motion + settles, tiles, and emitsoverview-480w.pngfor the QA gate.--dismissclears overlays through the page's own affordances — Escape + clicks on dismiss-buttons scoped to overlay-shaped elements; no vendor denylist, no CSS-hide (probe: structural-hide harmed real nav 4/8 sites; affordance-only cleared 5/5 dismissable overlays, 0/8 harm). Replace the page's tiles with the re-rendered set;qa_status: recapture-used. (No Firecrawl; needsplaywright— if your resolvedpython3lacks it, call the interpreter that has it, e.g. a pyenv binary.)- The page URL comes from the capture's stamp — never grep the body. Read it from the cleaned
.md:grep -m1 '^source_url:' store/<slug>/captures/<date>/<page>.md. The body opens with nav/CTA links, so grepping it mis-picks (functionhealth → amy.*/signupCTA). Pre-stamp captures (predate thesource_urlheader) fall back to that page'ssourceURLin.payloads/manifest.jsonlif it survives; otherwise re-capture rather than guess. - Tier-B is a comparison, not an automatic upgrade. Check the re-render against the cached shot before adopting — sometimes cached wins (alange-soehne, 2026-06-16: live Chrome rendered the hero black + scroll-locked while the cached hero was clean). If the re-render is worse, keep cached and ship
exclusions-noted. - Render into the
source_capture(dossier) date dir, the samecaptures/<date>/tile.pytiled — not<today>— sovisual.mdcites one tiles root andsource_capturestays the dossier date (VISUAL.md).captured_atcarries today. - Dual-render only when WebGL/lazy and an overlay contaminate one page — the cached payload can't be the faithful baseline, so render twice into sibling dirs:
tiles/<page>(faithful, no flag) +tiles/<page>__dismissed(--dismiss). Cards cite the__dismissedset; the faithful tiles are kept on disk as the comparison view. (The WebGL-incompleteness that triggers this can read as intentional dark design — spot the flat-dark hero before classing a page overlay-only.) - Timed/marketing modals (newsletter, "10% off") — if there's a dismiss control (Escape, "No thanks", "×"),
--dismissmay clear it in one shot. If the modal re-arms during the warm-scroll loop (gethealthspan, 2026-06-16 — see BACKLOG) or has no dismiss path, a guard fires (below); exclude or caveat the tile instead. - Pass
--out-dirabsolute (as above), and never disable the sandbox to dodge a path-permission /getcwderror — that once left a sticky lockdown that killed a run. - Loud-not-silent. A
WARNINGon stderr (and the matching manifest field) means a guard fired — don't ignore it:dismiss_cleared=false(the overlay didn't clear — closed shadow root / off-list labels; compare to cached, exclude or caveat),scroll_locked(still locked after dismiss — fall back to cached or exclude), a thin/interstitial page (bot-wall / login-gate / splash captured as the page — exclude or use a non-walled path), or a missingoverview-480w.png(magick failed — fall back to tile spot-check).
- The page URL comes from the capture's stamp — never grep the body. Read it from the cleaned
- If a page can't be made clean at all, drop it. If nothing is clean, decline and record it in
## Provenance— don't mine defects out of broken captures.
Assemble the active tile list (all kept tile paths, repo-relative) and the exclusions (path + reason).
3. Blind mining + judge (the workflow). Hand the active tiles to the fan-out — 4 family miners in parallel (each a fresh, tiles-only agent) → judge/prune. The miners are blind by construction; StructuredOutput validates every card:
Workflow({ scriptPath: "skills/visual-evidence/mine.workflow.js",
args: { slug: "<slug>", engineRoot: "$ENGINE", tiles: [<active tile paths, repo-relative>], exclusions: [{path, reason}] } })
It returns accepted_cards (with ids), rejected_cards, and judge notes, with tile paths already repo-relative — the workflow absolutizes them for the blind reads and relativizes on return, so author them straight into visual.md (no hand-rewrite). The four miners default to Sonnet — it holds Opus's calibration even on the dark-gradient seduction case at ~⅕ the per-token cost (experiment); override with a minerModel arg. The judge inherits the session model (Opus on an Opus session) — the cross-family prune/merge is the higher-reasoning step, and it absorbs Sonnet's chattier output.
4. Synthesize + write store/<slug>/visual.md per modules/VISUAL.md:
- First, spot-check every
poorstructural card against its native tile. Miners and judge are both blind — neither can tell a mid-animation/compositing artifact (double-rendered row, cards mid-flight, blank icons) from a genuine layout defect, so a capture artifact can slip through as apoorcard. This is the one check the workflow can't self-perform; drop any such card before writing. - Frontmatter:
schema_version: "1.0",domain,captured_at(today — when tiles were mined),source_capture(the dossier capture date this layer pairs with; stays the dossier date even when Tier-B re-renders fresh tiles today — see VISUAL.md),qa_status(from step 2). ## Visual & brand impression— ≤120 words, only from the accepted cards, every claim citing a card id ([typography_01]). A lens over the cards, never new assertion. This is the brief's deliverable.## Evidence cards— the accepted cards as ayamlblock (the schema in VISUAL.md).## Provenance— tiles read, exclusions named, whether Tier-B was used and for which pages, and a snapshot caveat.
5. Lint. python3 scripts/visualcheck.py --slug <slug> — must exit 0 (tile paths valid + active, closed sets, ≥1 tell per card, impression cites ids, and no score anywhere). Fix anything it flags.
6. Record the run, then report.
Write the run record before you report — the run is not done until it's written (this is the step agents drop). Record what actually ran:
python3 "$WEB_RESEARCH_HOME/scripts/runrecord.py" write \
--slug <slug> \
--verb visual-evidence \
--started-at "$RUN_STARTED_AT" \
--artifact visual.md \
--components-json '[{"tool":"claude-code","model":"sonnet","role":"visual-miner:typography_hierarchy"},{"tool":"claude-code","model":"sonnet","role":"visual-miner:layout_composition_components"},{"tool":"claude-code","model":"sonnet","role":"visual-miner:color_brand_imagery"},{"tool":"claude-code","model":"sonnet","role":"visual-miner:iconography_illustration"}]'
Tool is env-detected for both Claude Code and Codex — no --tool needed. Pass --model <id> if you know it — the model you were told at session start; else unknown. components must be the miners that actually ran — never copy the four above as boilerplate. The four-miner list is correct only when you ran the mine.workflow.js blind fan-out (use the minerModel model if it was overridden). If the workflow runner was unavailable and you did a degraded manual pass (no fan-out — e.g. a Codex session without the Workflow tool), drop the four miners, record the single pass you actually did (or omit --components-json), and pass --status partial. A Tier-B Playwright render is deterministic shell work, not an LLM component; mention it in --note only if it matters.
Then report. One line: company → store/<slug>/visual.md, N cards across the four families, qa_status, any pages that needed Tier-B re-render.
Why a workflow (not hand-rolled sub-agents)
The fan-out is 4 blind miners + a judge with schema-validated card output. The workflow buys three things a loose Agent fan-out doesn't: structural blinding (each miner is a fresh context with only tile paths — it can't reach the dossier), schema validation (vision agents emit messy YAML; StructuredOutput retries until each card is well-formed), and determinism + reuse (the same script, re-runnable, validate-many in parallel). The miner and judge prompts live in mine.workflow.js — that's their single source of truth, so there's no protocol doc to drift from it.