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
169 changes: 169 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -8408,6 +8408,92 @@
"summary"
]
},
"SkippedPrAuditExport": {
"type": "object",
"properties": {
"generatedAt": {
"type": "string"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100
},
"hasMore": {
"type": "boolean"
},
"filters": {
"type": "object",
"properties": {
"repoFullName": {
"type": "string",
"nullable": true
},
"reason": {
"type": "string",
"nullable": true,
"enum": [
"surface_off",
"missing_author",
"bot_author",
"maintainer_author",
"miner_detection_unavailable",
"not_official_gittensor_miner",
null
]
},
"since": {
"type": "string",
"nullable": true
}
},
"required": [
"repoFullName",
"reason",
"since"
]
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"repoFullName": {
"type": "string"
},
"pullNumber": {
"type": "integer",
"minimum": 0,
"exclusiveMinimum": true
},
"reason": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"remediation": {
"type": "string"
}
},
"required": [
"repoFullName",
"pullNumber",
"reason",
"timestamp",
"remediation"
]
}
}
},
"required": [
"generatedAt",
"limit",
"hasMore",
"filters",
"items"
]
},
"CommandPreviewResponse": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -13060,6 +13146,89 @@
]
}
},
"/v1/app/skipped-pr-audit": {
"get": {
"parameters": [
{
"schema": {
"type": "string",
"example": "50"
},
"required": false,
"description": "Maximum rows to return, clamped from 1 to 100.",
"name": "limit",
"in": "query"
},
{
"schema": {
"type": "string",
"example": "JSONbored/gittensory"
},
"required": false,
"description": "Optional repository filter. Browser sessions must have control-panel access to this repo.",
"name": "repoFullName",
"in": "query"
},
{
"schema": {
"type": "string",
"enum": [
"surface_off",
"missing_author",
"bot_author",
"maintainer_author",
"miner_detection_unavailable",
"not_official_gittensor_miner"
],
"example": "not_official_gittensor_miner"
},
"required": false,
"description": "Optional PR skip reason filter.",
"name": "reason",
"in": "query"
},
{
"schema": {
"type": "string",
"example": "2026-05-30T00:00:00.000Z"
},
"required": false,
"description": "Optional lower timestamp bound.",
"name": "since",
"in": "query"
}
],
"responses": {
"200": {
"description": "Private bounded audit export for skipped PR public-surface decisions",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SkippedPrAuditExport"
}
}
}
},
"400": {
"description": "Invalid query"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Insufficient app role or repository scope"
}
},
"security": [
{
"GittensoryBearer": []
},
{
"GittensorySessionCookie": []
}
]
}
},
"/v1/app/commands/preview": {
"post": {
"responses": {
Expand Down
97 changes: 96 additions & 1 deletion src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
listDigestSubscriptionsForLogin,
listProductUsageDailyRollups,
listOpenPullRequests,
listPrVisibilitySkipAuditEvents,
listPullRequestFiles,
listPullRequestReviews,
listRecentMergedPullRequests,
Expand Down Expand Up @@ -177,7 +178,7 @@ import { buildPullRequestReviewability, type PullRequestReviewability } from "..
import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch";
import { MAX_LOCAL_SCORER_WARNING_CHARS, MAX_LOCAL_SCORER_WARNING_COUNT } from "../signals/local-scorer-diagnostics";
import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import { buildRepoSettingsPreview } from "../signals/settings-preview";
import { buildRepoSettingsPreview, type PublicSurfaceSkipReason } from "../signals/settings-preview";
import { buildGittensorConfigRecommendation, buildRegistrationReadiness, type InstallationHealthSummary } from "../signals/registration-readiness";
import { fileUpstreamDriftIssues, loadUpstreamStatus, refreshUpstreamDrift, registryHyperparameterDriftWarningsForRepo } from "../upstream/ruleset";
import type {
Expand Down Expand Up @@ -238,6 +239,14 @@ async function recordRouteProductUsage(

const MAX_LOCAL_BRANCH_REF_CHARS = 256;
const MAX_LOCAL_BRANCH_TEXT_CHARS = 4000;
const PR_VISIBILITY_SKIP_REASONS = [
"surface_off",
"missing_author",
"bot_author",
"maintainer_author",
"miner_detection_unavailable",
"not_official_gittensor_miner",
] as const satisfies readonly PublicSurfaceSkipReason[];

const preflightSchema = z.object({
repoFullName: z.string().min(3),
Expand All @@ -257,6 +266,15 @@ const localDiffPreflightSchema = preflightSchema.extend({
commitMessage: z.string().optional(),
});

const skippedPrAuditQuerySchema = z
.object({
limit: z.coerce.number().int().optional(),
repoFullName: z.string().trim().min(3).max(200).optional(),
reason: z.enum(PR_VISIBILITY_SKIP_REASONS).optional(),
since: z.string().trim().min(1).max(64).optional(),
})
.strict();

const localBranchChangedFileSchema = z
.object({
path: z.string().min(1).max(MAX_LOCAL_BRANCH_REF_CHARS),
Expand Down Expand Up @@ -838,6 +856,44 @@ export function createApp() {
});
});

app.get("/v1/app/skipped-pr-audit", async (c) => {
const identity = await authenticateRequestIdentity(c);
if (!identity) return c.json({ error: "unauthorized" }, 401);
const summary = await getRoleSummaryForIdentity(c.env, identity);
if (!summary.roles.some((role) => ["maintainer", "owner", "operator"].includes(role))) return c.json({ error: "insufficient_role" }, 403);

const parsed = skippedPrAuditQuerySchema.safeParse(c.req.query());
if (!parsed.success) return c.json({ error: "invalid_skipped_pr_audit_query", issues: parsed.error.issues }, 400);
const sinceIso = parsed.data.since ? toIsoQueryDate(parsed.data.since) : undefined;
if (parsed.data.since && !sinceIso) return c.json({ error: "invalid_since" }, 400);
const requestedRepo = parsed.data.repoFullName;
const repoFullNames = await skippedPrAuditRepoScope(c, identity, summary.roles, requestedRepo);
if (repoFullNames instanceof Response) return repoFullNames;
const page = await listPrVisibilitySkipAuditEvents(c.env, {
limit: clampInteger(parsed.data.limit ?? 50, 1, 100),
repoFullNames,
reason: parsed.data.reason,
sinceIso,
});
return c.json({
generatedAt: nowIso(),
limit: page.limit,
hasMore: page.hasMore,
filters: {
repoFullName: requestedRepo ?? null,
reason: parsed.data.reason ?? null,
since: sinceIso ?? null,
},
items: page.items.map((item) => ({
repoFullName: item.repoFullName,
pullNumber: item.pullNumber,
reason: item.reason,
timestamp: item.createdAt,
remediation: skippedPrAuditRemediation(item.reason),
})),
});
});

app.get("/v1/app/operator-dashboard", async (c) => {
const forbidden = await requireAppRole(c, ["operator"]);
if (forbidden) return forbidden;
Expand Down Expand Up @@ -3338,6 +3394,45 @@ async function requireSessionRepoAccess(
return c.json({ error: "forbidden_repo" }, 403);
}

async function skippedPrAuditRepoScope(
c: ProtectedRouteContext,
identity: AuthIdentity,
roles: ControlPanelRoleName[],
requestedRepo: string | undefined,
): Promise<string[] | undefined | Response> {
if (identity.kind !== "session" || roles.includes("operator")) return requestedRepo ? [requestedRepo] : undefined;
const scope = await loadControlPanelAccessScope(c.env, identity.actor);
const scopedRepoNames = new Set(scope.repositoryFullNames.map((name) => name.toLowerCase()));
if (requestedRepo) {
return scopedRepoNames.has(requestedRepo.toLowerCase()) ? [requestedRepo] : c.json({ error: "forbidden_repo" }, 403);
}
return scope.repositoryFullNames;
}

function skippedPrAuditRemediation(reason: string): string {
switch (reason) {
case "surface_off":
return "Enable a PR public surface or check runs in repository settings if maintainers want Gittensory to post.";
case "missing_author":
return "Retry after GitHub provides a resolvable pull request author.";
case "bot_author":
return "No action needed; bot-authored pull requests are intentionally kept quiet.";
case "maintainer_author":
return "Enable maintainer-authored PRs in repository settings only if those PRs should receive public GitHub App output.";
case "miner_detection_unavailable":
return "Retry after official Gittensor miner detection recovers; Gittensory skips instead of guessing.";
case "not_official_gittensor_miner":
return "No public action is needed unless the author should be recognized as an official Gittensor miner.";
default:
return "Review repository settings and installation health before reprocessing the pull request.";
}
}

function toIsoQueryDate(value: string): string | undefined {
const timestamp = Date.parse(value);
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : undefined;
}

function requiresApiToken(path: string): boolean {
if (path === "/health") return false;
if (path === "/v1/mcp/compatibility") return false;
Expand Down
Loading