Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
684 changes: 669 additions & 15 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "^8.5.0",
"@cloudflare/puppeteer": "^1.1.0",
"@modelcontextprotocol/sdk": "1.29.0",
"@octokit/core": "^7.0.6",
"agents": "^0.16.2",
Expand All @@ -81,8 +82,8 @@
"github-actionlint": "^1.7.12",
"node-addon-api": "^8.8.0",
"node-gyp": "^12.4.0",
"playwright": "^1.61.0",
"pixelmatch": "^7.2.0",
"playwright": "^1.61.0",
"pngjs": "^7.0.0",
"tsx": "^4.22.4",
"typescript": "^5.9.3",
Expand All @@ -104,8 +105,7 @@
},
"vite": {
"esbuild": "^0.28.1"
},
"ws": "^8.21.0"
}
},
"main": "index.js",
"directories": {
Expand Down
20 changes: 20 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { z } from "zod";
import { analyzePRQueue, type AuthorRole, type ChecksStatus } from "../queue-intelligence";
import { completeGitHubWebOAuth, createSessionFromGitHubToken, pollGitHubDeviceFlow, startGitHubDeviceFlow, startGitHubWebOAuth } from "../auth/github-oauth";
import { enforceRateLimit, routeClassForPath } from "../auth/rate-limit";
import { handleShot } from "../review/visual/shot";
import { isScreenshotsEnabled } from "../review/visual-wire";
import {
BROWSER_SESSION_COOKIE,
GITHUB_OAUTH_STATE_COOKIE,
Expand Down Expand Up @@ -854,6 +856,24 @@ export function createApp() {
return c.json(buildShieldsBadge(quality, 600));
});

// Visual before/after screenshot endpoint (visual-capture port). PUBLIC + UNAUTHENTICATED by design: it
// lives OUTSIDE the /v1/ prefix, so requiresApiToken (which only gates path.startsWith('/v1/')) never
// touches it — GitHub's camo image proxy must fetch it without a bearer token. The handler itself enforces
// every security choke-point: ?key= validates the R2 prefix + rejects '..'; ?url= keeps the host allowlist
// (*.workers.dev / *.pages.dev / PUBLIC_SITE_ORIGIN) AND the isSafeHttpUrl SSRF guard. Inert flag-OFF: with
// GITTENSORY_REVIEW_SCREENSHOTS off nothing ever writes shots to R2, so ?key= 404s and ?url= still requires
// an allowlisted public host. The route's own Cache-Control headers (per mode) are set inside handleShot;
// the rate-limit middleware classifies it as 'normal' (a sane public class) via routeClassForPath.
// Flag-OFF = TRULY inert: when GITTENSORY_REVIEW_SCREENSHOTS is off nothing references this route (no comment
// carries a /gittensory/shot URL), so 404 it outright — that removes the on-demand `?url=` render surface
// entirely until the feature is deliberately enabled, rather than relying on the host allowlist alone.
app.get("/gittensory/shot", (c) => {
if (!isScreenshotsEnabled(c.env)) return c.notFound();
return handleShot(c.req.raw, c.env, {
...(c.env.PUBLIC_SITE_ORIGIN ? { productionUrl: c.env.PUBLIC_SITE_ORIGIN } : {}),
});
});

app.get("/v1/auth/github/start", async (c) => {
try {
const start = await startGitHubWebOAuth(c.env, c.req.url, c.req.query("returnTo"));
Expand Down
11 changes: 11 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ declare global {
* the PR diff is scanned for leaked secrets, surfacing a `secret_leak` blocker. Default OFF —
* unset/false keeps the review path byte-identical (no new branch is taken). */
GITTENSORY_REVIEW_SAFETY?: string;
/** Convergence (visual capture): when truthy, the review path captures a before/after screenshot for
* PRs that touch WEB-VISIBLE files (frontend pages / public OG images — see review/visual/paths.ts
* isVisualPath). "before" = production (PUBLIC_SITE_ORIGIN); "after" = the PR's preview deploy. Each shot
* is rendered via the BROWSER (Browser Rendering) binding, stored in the REVIEW_AUDIT R2 bucket, and
* embedded in the unified PR comment as a "Visual preview" table served from the PUBLIC /gittensory/shot
* route. Needs the BROWSER + REVIEW_AUDIT bindings; degrades gracefully (placeholders / dashes) without
* them. Backend .ts/.md/.json/.py PRs NEVER trigger capture. Capture runs for a repo ONLY IF this flag is
* ON *AND* the repo is in GITTENSORY_REVIEW_REPOS (the per-repo cutover allowlist) — see
* review/visual-wire.ts screenshotsAllowed. Default OFF — unset/false captures nothing (no render, no R2
* write, no comment change) so the review path is byte-identical to today. */
GITTENSORY_REVIEW_SCREENSHOTS?: string;
/** Convergence (grounding): when truthy, the AI reviewer prompt is GROUNDED — the PR's finished CI status
* + the FULL post-change content of the changed files are appended so a non-frontier model verifies its
* claims against reality instead of predicting CI / flagging symbols defined just outside the hunk.
Expand Down
29 changes: 28 additions & 1 deletion src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import {
refreshPullRequestDetails,
} from "../github/backfill";
import { contributorRepoStatsFromGittensor, fetchGittensorContributorSnapshot, fetchOfficialGittensorMiner, type GittensorContributorSnapshot, type OfficialGittensorMinerDetection } from "../gittensor/api";
import { createOrUpdateCheckRun, createOrUpdateErroredGateCheckRun, createOrUpdateGateCheckRun, createOrUpdateOverriddenGateCheckRun, createOrUpdatePendingGateCheckRun, createOrUpdateSkippedGateCheckRun, getInstallationId, getRepositoryCollaboratorPermission } from "../github/app";
import { createInstallationToken, createOrUpdateCheckRun, createOrUpdateErroredGateCheckRun, createOrUpdateGateCheckRun, createOrUpdateOverriddenGateCheckRun, createOrUpdatePendingGateCheckRun, createOrUpdateSkippedGateCheckRun, getInstallationId, getRepositoryCollaboratorPermission } from "../github/app";
import { AGENT_COMMAND_COMMENT_MARKER, createOrUpdateAgentCommandComment, createOrUpdatePrIntelligenceComment, PR_PANEL_COMMENT_MARKER } from "../github/comments";
import { gittensoryFooter, gittensorRepoEarnUrl } from "../github/footer";
import {
Expand Down Expand Up @@ -148,6 +148,9 @@ import {
type ContributorProfile,
} from "../signals/engine";
import { buildClosedUnifiedCommentBody, buildUnifiedCommentBody, isUnifiedReviewCommentEnabled } from "../review/unified-comment-bridge";
import { screenshotsAllowed } from "../review/visual-wire";
import { isVisualPath } from "../review/visual/paths";
import { buildCapture, type CaptureRoute } from "../review/visual/capture";
import type { MergeReadiness } from "../review/unified-comment";
import { buildIssueSlopAssessment, buildSlopAssessment, type SlopBand } from "../signals/slop";
import { runGittensoryAiSlopAdvisory } from "../services/ai-slop";
Expand Down Expand Up @@ -1823,6 +1826,29 @@ async function maybePublishPrPublicSurface(
...(pr.mergeableState ? { mergeStateLabel: pr.mergeableState } : {}),
...(failedChecks.length > 0 ? { failingChecks: failedChecks.map((check) => check.name) } : {}),
};
// Visual before/after capture (visual-capture port). Fires ONLY when (1) the global flag + per-repo
// cutover gate both allow it (screenshotsAllowed) AND (2) the PR touches WEB-VISIBLE files (isVisualPath
// — frontend pages / public OG images; backend .ts/.md/.json PRs never qualify). Fully wrapped in
// try/catch + defaults to [] so a capture failure (render timeout, missing binding, GitHub hiccup) can
// NEVER sink the review — it just omits the "Visual preview" section. Flag-OFF (default) ⇒ this block is
// skipped entirely and the unified comment is byte-identical.
let beforeAfter: CaptureRoute[] = [];
const visualFiles = unifiedFiles.map((file) => file.path).filter(isVisualPath);
if (screenshotsAllowed(env, repoFullName) && visualFiles.length > 0) {
try {
const token = await createInstallationToken(env, installationId);
const capture = await buildCapture(env, token, {
repoFullName,
prNumber: pr.number,
...(pr.headSha ? { headSha: pr.headSha } : {}),
...(pr.headRef ? { headRef: pr.headRef } : {}),
previewFromChecks: true,
}, visualFiles);
beforeAfter = capture.routes;
} catch (error) {
console.log(JSON.stringify({ ev: "visual_capture_error", repoFullName, pull: pr.number, message: errorMessage(error).slice(0, 200) }));
}
}
deterministicBody = buildUnifiedCommentBody({
gate: gateEvaluation,
...(aiReview !== undefined ? { aiReview } : {}),
Expand Down Expand Up @@ -1850,6 +1876,7 @@ async function maybePublishPrPublicSurface(
...(reviewConfig?.footerText ? { customText: reviewConfig.footerText } : {}),
}),
reRunLabel: `${PR_PANEL_RETRIGGER_MARKER} Re-run Gittensory review`,
...(beforeAfter.length > 0 ? { beforeAfter } : {}),
});
} else {
deterministicBody = buildPublicPrIntelligenceComment(commentArgs);
Expand Down
44 changes: 43 additions & 1 deletion src/review/unified-comment-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import type { AdvisoryFinding } from "../types";
import type { GateCheckConclusion, GateCheckEvaluation } from "../rules/advisory";
import type { PublicPrPanelSignalRow } from "../signals/engine";
import type { CaptureRoute } from "./visual/capture";
// Single-source the panel marker from its canonical home (the upsert reads it there); re-export so existing
// importers of `PR_PANEL_COMMENT_MARKER` from this module keep working. The unified body MUST prepend this
// verbatim or `createOrUpdatePrIntelligenceComment` posts a DUPLICATE instead of updating in place.
Expand Down Expand Up @@ -195,8 +196,43 @@ export type UnifiedCommentBridgeArgs = {
extraCollapsibles?: UnifiedCollapsible[] | undefined;
/** Headline brand (default "Gittensory review"). */
brand?: string | undefined;
/** Visual before/after capture routes (visual-capture port). When present + non-empty, a "Visual preview"
* collapsible (a markdown table of <img> tags pointing at the public /gittensory/shot URLs) is appended.
* Public-safe: only URLs + route paths — no private terms. Default OFF (the processor passes this only
* when screenshotsAllowed + the PR touches web-visible files). */
beforeAfter?: CaptureRoute[] | undefined;
};

/**
* Build the "Visual preview" collapsible from the before/after capture routes — a markdown table of image
* cells pointing at the public /gittensory/shot URLs. Uses GitHub markdown image syntax `![](url)` rather
* than raw `<img>` tags ON PURPOSE: the unified renderer's `details()` HTML-escapes a collapsible body (a
* security control so caller text can't inject structure-changing HTML), which would turn a literal `<img>`
* into inert `&lt;img&gt;` text — markdown image syntax has no angle brackets, so it survives the escape and
* still renders as an image. Public-safe by construction: every cell is a route path or a shot URL (no
* private rubric/scoring terms). Returns null when nothing is renderable (no route has any shot URL), so the
* section is omitted entirely rather than showing an empty table.
*/
export function buildBeforeAfterCollapsible(routes: CaptureRoute[]): UnifiedCollapsible | null {
const rows = routes
.filter((route) => route.beforeUrl || route.afterUrl || route.beforeUrlMobile || route.afterUrlMobile)
.map((route) => {
// Escape `(`/`)`/`]` in the URL so a crafted shot URL can't break out of the markdown image token; the
// URLs are first-party (we mint them), but this keeps the cell robust regardless.
const cell = (url: string | undefined): string => (url ? `![preview](${url.replace(/[()\]]/g, encodeURIComponent)})` : "—");
return `| \`${route.path.replace(/\|/g, "\\|")}\` | ${cell(route.beforeUrl)} | ${cell(route.afterUrl)} |`;
});
if (rows.length === 0) return null;
const body = [
"| Route | Before (production) | After (this PR's preview) |",
"| --- | --- | --- |",
...rows,
"",
"_Before = production · After = this PR's preview deploy._",
].join("\n");
return { title: "Visual preview", body };
}

/**
* Build the unified PR-review comment body from gittensory's live data. Returns a string that STARTS with
* the panel marker (so the existing upsert updates in place) followed by the rendered unified comment.
Expand Down Expand Up @@ -228,13 +264,19 @@ export function buildUnifiedCommentBody(args: UnifiedCommentBridgeArgs): string
const visibleRows = args.panelRows.filter((row) => args.reviewFields?.[row.key] !== false);
const signals = panelRowsToSignalRows(visibleRows);

// Visual-capture port: when before/after routes are present, append a "Visual preview" collapsible to the
// extra sections. Flag-OFF (the processor passes no beforeAfter) ⇒ extraCollapsibles is unchanged.
const visualCollapsible = args.beforeAfter && args.beforeAfter.length > 0 ? buildBeforeAfterCollapsible(args.beforeAfter) : null;
const extraCollapsibles =
visualCollapsible !== null ? [...(args.extraCollapsibles ?? []), visualCollapsible] : args.extraCollapsibles;

const body = renderUnifiedReviewComment(input, {
brand: args.brand ?? "Gittensory review",
readinessScore: args.readinessTotal,
signals,
footerMarkdown: args.footerMarkdown,
...(args.reRunLabel !== undefined ? { reRunLabel: args.reRunLabel } : {}),
...(args.extraCollapsibles !== undefined ? { extraCollapsibles: args.extraCollapsibles } : {}),
...(extraCollapsibles !== undefined ? { extraCollapsibles } : {}),
});

// Prepend the marker verbatim (matching the legacy body, which leads with the marker then a blank line)
Expand Down
29 changes: 29 additions & 0 deletions src/review/visual-wire.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Convergence (visual capture) feature flag + per-repo gate wiring.
//
// Single env switch: GITTENSORY_REVIEW_SCREENSHOTS. Default OFF (unset/"false") — when OFF the processor
// never calls buildCapture, so the review path is byte-identical to today. Truthy follows the codebase
// convention (`/^(1|true|yes|on)$/i`, same as isSafetyEnabled / isUnifiedReviewCommentEnabled).
//
// As with every other per-PR converged feature, capture runs on a given PR's repo ONLY IF the global flag is
// ON *AND* the repo is in the per-repo cutover allowlist (GITTENSORY_REVIEW_REPOS). The AND with
// isConvergenceRepoAllowed is MANDATORY — it lets the cutover roll forward/back one repo at a time and keeps
// a globally-on-but-not-listed deploy dormant.

import { isConvergenceRepoAllowed } from "./cutover-gate";

/** True when the visual-capture global flag is enabled. Flag-OFF (default) → no capture is attempted. */
export function isScreenshotsEnabled(env: { GITTENSORY_REVIEW_SCREENSHOTS?: string | undefined }): boolean {
return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_SCREENSHOTS ?? "");
}

/**
* True when visual capture is allowed for `repoFullName`: the global flag is ON *AND* the repo is in the
* per-repo cutover allowlist. Both must hold — a globally-on flag alone never activates capture for an
* unlisted repo (the dormant default).
*/
export function screenshotsAllowed(
env: { GITTENSORY_REVIEW_SCREENSHOTS?: string | undefined; GITTENSORY_REVIEW_REPOS?: string | undefined },
repoFullName: string,
): boolean {
return isScreenshotsEnabled(env) && isConvergenceRepoAllowed(env, repoFullName);
}
Loading