Skip to content

feat(review): add scroll-through GIF evidence capture#3688

Merged
JSONbored merged 1 commit into
mainfrom
feat/selfhost-visual-gif-capture
Jul 6, 2026
Merged

feat(review): add scroll-through GIF evidence capture#3688
JSONbored merged 1 commit into
mainfrom
feat/selfhost-visual-gif-capture

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • Deterministic, narrowly-scoped v1 (per the issue's own scoping requirement): a fixed 6-step scroll-through sequence per side (top → bottom, evenly spaced), captured as viewport-cropped PNGs and assembled into one animated GIF — evidence for scroll-linked behavior (parallax, reveal-on-scroll, a sticky header) that a static screenshot can't show. A page shorter than one viewport yields a single frame (nothing to animate).
  • src/review/visual/shot.ts — new captureScrollFrames, mirroring captureShot's SSRF guard / sub-request interception / auth-wall detection exactly (duplicated rather than shared, since this is security-sensitive code). Also fixes handleShot's ?key= serve path, which previously hardcoded content-type: image/png regardless of what was actually stored — now derived from the key's extension so a .gif object animates correctly.
  • New module-swap pair (same established pattern as feat(review): wire up existing pixel-diff into a 'Diff' column (self-host) #3674's pixel-diff column): src/review/visual/scroll-gif.ts is the Worker-safe no-op default; src/selfhost/stubs/scroll-gif.ts is the real self-host implementation (pngjs decode → gifenc quantize/encode, both zero-Node-dependency-but-swapped-for-consistency — see module header). scripts/build-selfhost.mjs swaps the specifier only when bundling src/server.ts; test/unit/worker-entry-boundary.test.ts's forbidden-identifier list now also covers gifenc.
  • src/review/visual/capture.ts — new captureScrollGif, cached on the same fingerprint scheme as the existing screenshot/diff uploads (a scroll capture is the most expensive thing this pipeline does — a re-review of the same head must never redo it). Gated on both review.visual.gif and isScrollGifAvailable() — hosted mode never even attempts a scroll frame, so there's zero added cost there regardless of a repo's config.
  • src/signals/focus-manifest.tsreview.visual.gif: boolean (default false), composes independently with review.visual.themes (feat(review): add review.visual.themes config for dark-mode capture #3678).
  • src/review/unified-comment-bridge.ts — new "Scroll preview" collapsible, rendered alongside "Visual preview" (never replacing it), same clickable-thumbnail markup/escaping.
  • Docs: .gittensory.yml.example + config/examples/gittensory.full.yml both document the new gif: key (kept byte-identical per config-templates.test.ts).

Part of #3607. Closes #3612.

Scope

  • src/review/visual/shot.ts, src/review/visual/capture.ts, src/review/visual/scroll-gif.ts (new), src/selfhost/stubs/scroll-gif.ts (new), src/selfhost/stubs/gifenc.d.ts (new — gifenc ships no types).
  • src/signals/focus-manifest.ts, src/review/unified-comment-bridge.ts.
  • scripts/build-selfhost.mjs, test/unit/worker-entry-boundary.test.ts.
  • .gittensory.yml.example, config/examples/gittensory.full.yml.
  • New dependency: gifenc (devDependency, matching pngjs/pixelmatch's existing classification — bundled by esbuild regardless of dep/devDep).
  • Tests: scroll-gif.test.ts (new), selfhost-scroll-gif-stub.test.ts (new), visual-shot.test.ts, visual-capture.test.ts, visual-collapsible.test.ts, focus-manifest.test.ts, signals-coverage.test.ts, visual-config-wiring.test.ts.

Validation

  • npx tsc --noEmit clean.
  • Full local suite: 498 files, 10055 passed / 7 skipped, 0 failed (npx vitest run).
  • git diff --check clean.
  • npm audit --audit-level=moderate — 0 vulnerabilities.
  • Real load-test (Docker browserless/chromium + puppeteer-core, a real 6-band 5400px page): 6 distinct frames captured in 2.45s, decode+quantize+encode in 173ms~4.1s total per side. Independently re-verified the produced GIF with Pillow (not my own encoder): 6 real frames, 500ms each, infinite loop, 1440×900, ~40KB. A single-viewport-height page correctly yields exactly 1 frame (no wasted animation of a static page).
  • Re: the plan's "load-test against the Workers CPU-ms ceiling" requirement — this doesn't actually apply to the shipped architecture: GIF assembly needs a PNG decode step (pngjs, Node Buffer), so — like feat(review): wire up existing pixel-diff into a 'Diff' column (self-host) #3674's pixel-diff — it's self-host-only by construction. The Cloudflare Worker (hosted) path never runs this code at all (isScrollGifAvailable() gates it before a single scroll frame is captured), so there's no Workers isolate CPU-ms exposure to measure. The self-host process is a normal long-running Node server, not subject to that limit; the real, relevant number is the ~4s-per-side wall-clock measured above.
  • Precise patch-coverage self-check: extracted every added/changed line from the diff (not just eyeballing file-level %) and cross-referenced against raw v8 statement/branch coverage. Zero gaps remain in any touched file's actual diff range (some file-level "uncovered" lines shown by the coverage summary are pre-existing gaps outside this PR's diff, e.g. in capturePage's older code — not part of this patch).
  • Self-host bundle verified twice: default (packages: external) and --all (bundle-everything, the Docker-image mode) both build cleanly with gifenc fully inlined, no leftover unresolved import.
  • No API/schema, wrangler binding, or DB migration changes — ui:openapi/cf-typegen/migrations not applicable.

Safety

  • Default (no review.visual.gif configured) is byte-identical to pre-(stretch) Short-clip/GIF evidence capture for non-static-visible changes #3612 behavior: no scroll frames captured, no "Scroll preview" section, no new R2/blob-store keys. Verified by dedicated tests in every touched file.
  • Hosted mode never executes any of the new capture/encode code, regardless of a repo's own .gittensory.yml — enforced by isScrollGifAvailable() short-circuiting in buildCapture, and separately enforced structurally by the Worker-entry-boundary test.
  • No secrets, wallets, hotkeys, trust scores, or reward values touched.
  • Stays within wantedPaths; does not touch site/, CNAME, or **/lovable/**.

@superagent-security

superagent-security Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@loopover-orb loopover-orb Bot added the gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. label Jul 6, 2026
@loopover-orb

loopover-orb Bot commented Jul 6, 2026

Copy link
Copy Markdown

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-07-06 05:50:31 UTC

21 files · 1 AI reviewer · no blockers · readiness 100/100 · CI green · clean

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review: This PR changes guardrail-protected path(s): package-lock.json (matched package-lock.json), package-lock.json (matched **/package-lock.json), package.json (matched package.json), package.json (matched **/package.json), src/selfhost/stubs/gifenc.d.ts (matched src/selfhost/**), and 1 more.

Review summary
Adds a deterministic 6-step scroll-through GIF capture (captureScrollFrames in shot.ts, scroll-gif.ts module-swap pair mirroring the #3674 pixel-diff pattern, capture.ts wiring, and a new 'Scroll preview' collapsible rendered alongside 'Visual preview'). The feature is gated behind review.visual.gif (default false) AND isScrollGifAvailable() (always false in hosted/Worker builds), so it is byte-identical to today for every non-opted-in repo and for the hosted service entirely. Fingerprint-based caching, auth-wall/SSRF handling, and graceful degradation (empty frames, null encode, cache-read/write failures) are all covered by extensive new tests, and the code traces cleanly to its stated #3612 scope.

Nits — 7 non-blocking
  • src/review/visual/shot.ts: captureScrollFrames duplicates captureShot's SSRF guard, sub-request interception, and auth-wall logic almost verbatim (deliberate per the PR's own rationale) — worth a code comment cross-reference or a shared test helper so a future security fix to one path doesn't silently miss the other.
  • src/selfhost/stubs/scroll-gif.ts: gif.writeFrame(...) passes { repeat: 0 } on every frame rather than just the first — harmless with gifenc but slightly misleading since 'repeat' is a global GIF property, not per-frame.
  • capture.ts's scroll-GIF capture is desktop-viewport only in this first cut (explicitly documented) — fine for v1, but confirm the follow-up for mobile is tracked so it isn't forgotten.
  • package.json: gifenc is added as a devDependency bundled into the self-host build via esbuild — verify this matches how pngjs itself (not just @​types/pngjs) is declared, since a build step that only installs production deps would break the self-host bundle.
  • src/review/unified-comment-bridge.ts's buildScrollPreviewCollapsible duplicates buildBeforeAfterCollapsible's attr/markdownCode escaping helpers inline rather than sharing them — minor DRY nit.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ Linked #3612
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 54 registered-repo PR(s), 46 merged, 439 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 54 PR(s), 439 issue(s).
Gate result ⚠️ Not blocking Advisory; not blocking this PR.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: Python, TypeScript, JavaScript, Ruby, Go, Kotlin, MDX, Shell
  • Official Gittensor activity: 54 PR(s), 439 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • No action.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.37%. Comparing base (4a7fa82) to head (2a71b44).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3688      +/-   ##
==========================================
+ Coverage   93.36%   93.37%   +0.01%     
==========================================
  Files         315      316       +1     
  Lines       32204    32296      +92     
  Branches    11812    11841      +29     
==========================================
+ Hits        30066    30158      +92     
  Misses       1507     1507              
  Partials      631      631              
Files with missing lines Coverage Δ
src/review/unified-comment-bridge.ts 99.42% <100.00%> (+0.06%) ⬆️
src/review/visual/capture.ts 89.34% <100.00%> (+2.21%) ⬆️
src/review/visual/scroll-gif.ts 100.00% <100.00%> (ø)
src/review/visual/shot.ts 89.62% <100.00%> (+5.90%) ⬆️
src/signals/focus-manifest.ts 99.09% <100.00%> (+<0.01%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 6, 2026
@JSONbored
JSONbored force-pushed the feat/selfhost-visual-gif-capture branch 3 times, most recently from 0bc75eb to 284f83b Compare July 6, 2026 04:47
@JSONbored
JSONbored force-pushed the feat/selfhost-visual-gif-capture branch 3 times, most recently from c573e98 to ab36e2c Compare July 6, 2026 05:35
@JSONbored
JSONbored force-pushed the feat/selfhost-visual-gif-capture branch from ab36e2c to 2a71b44 Compare July 6, 2026 05:44
@JSONbored
JSONbored merged commit 1baaece into main Jul 6, 2026
11 checks passed
@JSONbored
JSONbored deleted the feat/selfhost-visual-gif-capture branch July 6, 2026 05:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. manual-review Gittensor contributor context

Development

Successfully merging this pull request may close these issues.

(stretch) Short-clip/GIF evidence capture for non-static-visible changes

1 participant