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
222 changes: 222 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2554,6 +2554,201 @@
"dataQuality"
]
},
"RepoOutcomeEvidenceCompleteness": {
"type": "object",
"properties": {
"pullRequestsAnalyzed": {
"type": "number"
},
"withFileDetail": {
"type": "number"
},
"withReviewDetail": {
"type": "number"
},
"withCheckDetail": {
"type": "number"
},
"filesCompletenessRatio": {
"type": "number"
},
"reviewsCompletenessRatio": {
"type": "number"
},
"checksCompletenessRatio": {
"type": "number"
},
"fullyDecidedWithDetail": {
"type": "number"
},
"status": {
"type": "string",
"enum": [
"complete",
"partial",
"missing"
]
}
},
"required": [
"pullRequestsAnalyzed",
"withFileDetail",
"withReviewDetail",
"withCheckDetail",
"filesCompletenessRatio",
"reviewsCompletenessRatio",
"checksCompletenessRatio",
"fullyDecidedWithDetail",
"status"
]
},
"RepoOutcomePatterns": {
"type": "object",
"properties": {
"repoFullName": {
"type": "string"
},
"generatedAt": {
"type": "string"
},
"lane": {
"type": "string",
"enum": [
"direct_pr",
"issue_discovery",
"split",
"inactive",
"unknown"
]
},
"primaryLanguage": {
"type": "string",
"nullable": true
},
"sampleSize": {
"type": "number"
},
"totals": {
"type": "object",
"additionalProperties": {
"type": "number"
}
},
"outsideContributorMergeRate": {
"type": "number"
},
"maintainerLaneMergeRate": {
"type": "number"
},
"dimensions": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {
"nullable": true
}
}
},
"successPatterns": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {
"nullable": true
}
}
},
"riskPatterns": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {
"nullable": true
}
}
},
"evidenceCompleteness": {
"$ref": "#/components/schemas/RepoOutcomeEvidenceCompleteness"
},
"findings": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Finding"
}
},
"summary": {
"type": "string"
}
},
"required": [
"repoFullName",
"generatedAt",
"lane",
"primaryLanguage",
"sampleSize",
"totals",
"outsideContributorMergeRate",
"maintainerLaneMergeRate",
"dimensions",
"successPatterns",
"riskPatterns",
"evidenceCompleteness",
"findings",
"summary"
]
},
"RepoOutcomePatternsResponse": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"ready"
]
},
"source": {
"type": "string",
"enum": [
"snapshot",
"computed"
]
},
"repoFullName": {
"type": "string"
},
"generatedAt": {
"type": "string"
},
"ageSeconds": {
"type": "number"
},
"freshness": {
"type": "string",
"enum": [
"fresh",
"stale"
]
},
"patterns": {
"$ref": "#/components/schemas/RepoOutcomePatterns"
},
"dataQuality": {
"type": "object",
"additionalProperties": {
"nullable": true
}
}
},
"required": [
"status",
"source",
"repoFullName",
"generatedAt",
"ageSeconds",
"freshness",
"patterns"
]
},
"RegistrationReadiness": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -10037,6 +10232,33 @@
]
}
},
"/v1/repos/{owner}/{repo}/outcome-patterns": {
"get": {
"responses": {
"200": {
"description": "Cached or freshly-computed per-repo accepted/rejected PR outcome patterns with freshness envelope and explicit evidence-completeness",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepoOutcomePatternsResponse"
}
}
}
},
"404": {
"description": "Repo is unknown or has no outcome-pattern coverage yet"
}
},
"security": [
{
"GittensoryBearer": []
},
{
"GittensorySessionCookie": []
}
]
}
},
"/v1/repos/{owner}/{repo}/registration-readiness": {
"get": {
"responses": {
Expand Down
15 changes: 15 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ import {
import { buildWeeklyValueReport, generateWeeklyValueReport, loadWeeklyValueReport } from "../services/weekly-value-report";
import { loadOrComputeIssueQualityResponse } from "../services/issue-quality";
import { loadOrComputeBurdenForecastResponse } from "../services/burden-forecast";
import { loadOrComputeRepoOutcomePatternsResponse } from "../services/repo-outcome-patterns";
import {
buildBountyAdvisory,
buildBurdenForecast,
Expand Down Expand Up @@ -1381,6 +1382,13 @@ export function createApp() {
return c.json(reviewability);
});

app.get("/v1/repos/:owner/:repo/outcome-patterns", async (c) => {
const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`;
const response = await buildRepoOutcomePatternsResponse(c.env, fullName);
if (!response) return c.json({ error: "repo_outcome_patterns_not_found", repoFullName: fullName }, 404);
return c.json(response);
});

app.get("/v1/contributors/:login/profile", async (c) => {
const login = c.req.param("login");
const [github, pullRequests, issues, cachedRepoStats, gittensorSnapshot] = await Promise.all([
Expand Down Expand Up @@ -2292,6 +2300,13 @@ async function loadInstallationHealthSummary(env: Env, repo: RepositoryRecord |
/* v8 ignore stop */
}

async function buildRepoOutcomePatternsResponse(env: Env, fullName: string) {
const response = await loadOrComputeRepoOutcomePatternsResponse(env, fullName);
if (!response) return null;
const dataQuality = await loadRepoDataQuality(env, fullName);
return attachDataQuality(response as unknown as Record<string, unknown>, dataQuality);
}

async function buildRegistrationReadinessResponse(env: Env, fullName: string) {
/* v8 ignore start -- Registration readiness route-level shaping over covered signal helpers. */
const intelligence = await buildRepoIntelligenceResponse(env, fullName);
Expand Down
12 changes: 12 additions & 0 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1688,6 +1688,12 @@ export async function listPullRequestFiles(env: Env, fullName: string, pullNumbe
return rows.map(toPullRequestFileRecord);
}

export async function listRepoPullRequestFiles(env: Env, fullName: string): Promise<PullRequestFileRecord[]> {
const db = getDb(env.DB);
const rows = await db.select().from(pullRequestFiles).where(eq(pullRequestFiles.repoFullName, fullName)).limit(2000);
return rows.map(toPullRequestFileRecord);
}

export async function upsertPullRequestReview(env: Env, review: PullRequestReviewRecord): Promise<void> {
const db = getDb(env.DB);
await db
Expand Down Expand Up @@ -1726,6 +1732,12 @@ export async function listPullRequestReviews(env: Env, fullName: string, pullNum
return rows.map(toPullRequestReviewRecord);
}

export async function listRepoPullRequestReviews(env: Env, fullName: string): Promise<PullRequestReviewRecord[]> {
const db = getDb(env.DB);
const rows = await db.select().from(pullRequestReviews).where(eq(pullRequestReviews.repoFullName, fullName)).limit(2000);
return rows.map(toPullRequestReviewRecord);
}

export async function upsertCheckSummary(env: Env, check: CheckSummaryRecord): Promise<void> {
const db = getDb(env.DB);
await db
Expand Down
28 changes: 28 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { loadContributorDecisionPackForServing, repoDecisionFromPack } from "../
import { loadOrComputeIssueQualityResponse } from "../services/issue-quality";
import { loadOrComputeBurdenForecastResponse } from "../services/burden-forecast";
import { buildMcpClientTelemetry } from "../services/client-telemetry";
import { loadOrComputeRepoOutcomePatternsResponse } from "../services/repo-outcome-patterns";
import {
buildBountyAdvisory,
buildCollisionReport,
Expand Down Expand Up @@ -326,6 +327,15 @@ export class GittensoryMcp {
async (input) => this.toolResult(await this.getBurdenForecast(input)),
);

server.registerTool(
"gittensory_get_repo_outcome_patterns",
{
description: "Return cached or freshly-computed per-repo accepted/rejected PR outcome patterns: what maintainers actually merge or close, separated from maintainer-lane activity, with a freshness marker and explicit evidence-completeness.",
inputSchema: ownerRepoShape,
},
async (input) => this.toolResult(await this.getRepoOutcomePatterns(input)),
);

server.registerTool(
"gittensory_get_contributor_profile",
{
Expand Down Expand Up @@ -640,6 +650,24 @@ export class GittensoryMcp {
};
}

private async getRepoOutcomePatterns(input: { owner: string; repo: string }): Promise<ToolPayload> {
const fullName = `${input.owner}/${input.repo}`;
const response = await loadOrComputeRepoOutcomePatternsResponse(this.env, fullName);
if (!response) {
return {
summary: `Gittensory has no cached repo outcome patterns for ${fullName}.`,
data: { status: "not_found", repoFullName: fullName },
};
}
return {
summary:
response.source === "snapshot"
? `Gittensory repo outcome patterns for ${fullName} (cached, ${response.freshness}).`
: `Gittensory repo outcome patterns for ${fullName} (computed from cached metadata).`,
data: response as unknown as Record<string, unknown>,
};
}

private async loadOpenQueueCounts(fullName: string): Promise<{ openIssues: number; openPullRequests: number }> {
const [totals, openIssues, openPullRequests] = await Promise.all([
getLatestRepoGithubTotalsSnapshot(this.env, fullName),
Expand Down
46 changes: 46 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,52 @@ export const ContributorPatternReportSchema = z
})
.openapi("ContributorPatternReport");

export const RepoOutcomeEvidenceCompletenessSchema = z
.object({
pullRequestsAnalyzed: z.number(),
withFileDetail: z.number(),
withReviewDetail: z.number(),
withCheckDetail: z.number(),
filesCompletenessRatio: z.number(),
reviewsCompletenessRatio: z.number(),
checksCompletenessRatio: z.number(),
fullyDecidedWithDetail: z.number(),
status: z.enum(["complete", "partial", "missing"]),
})
.openapi("RepoOutcomeEvidenceCompleteness");

export const RepoOutcomePatternsSchema = z
.object({
repoFullName: z.string(),
generatedAt: z.string(),
lane: z.enum(["direct_pr", "issue_discovery", "split", "inactive", "unknown"]),
primaryLanguage: z.string().nullable(),
sampleSize: z.number(),
totals: z.record(z.number()),
outsideContributorMergeRate: z.number(),
maintainerLaneMergeRate: z.number(),
dimensions: z.array(z.record(z.unknown())),
successPatterns: z.array(z.record(z.unknown())),
riskPatterns: z.array(z.record(z.unknown())),
evidenceCompleteness: RepoOutcomeEvidenceCompletenessSchema,
findings: z.array(FindingSchema),
summary: z.string(),
})
.openapi("RepoOutcomePatterns");

export const RepoOutcomePatternsResponseSchema = z
.object({
status: z.enum(["ready"]),
source: z.enum(["snapshot", "computed"]),
repoFullName: z.string(),
generatedAt: z.string(),
ageSeconds: z.number(),
freshness: z.enum(["fresh", "stale"]),
patterns: RepoOutcomePatternsSchema,
dataQuality: z.record(z.unknown()).optional(),
})
.openapi("RepoOutcomePatternsResponse");

export const RepoFitRecommendationSchema = z
.object({
login: z.string(),
Expand Down
Loading