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
623 changes: 615 additions & 8 deletions apps/gittensory-ui/public/openapi.json

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,17 @@ const localBranchScorerSchema = z
})
.strict();

const linkedIssueContextSchema = z
.object({
status: z.enum(["raw", "plausible", "validated", "invalid", "unavailable"]).optional(),
source: z.enum(["user_supplied", "official_mirror", "github_cache", "issue_quality", "missing"]).optional(),
issueNumbers: z.array(z.number().int().positive()).max(50).optional(),
solvedByPullRequests: z.array(z.number().int().positive()).max(50).optional(),
reason: z.string().max(MAX_LOCAL_BRANCH_TEXT_CHARS).optional(),
warnings: z.array(z.string().max(MAX_LOCAL_BRANCH_TEXT_CHARS)).max(20).optional(),
})
.strict();

const localBranchAnalysisSchema = z
.object({
login: z.string().min(1).max(MAX_LOCAL_BRANCH_REF_CHARS),
Expand Down Expand Up @@ -237,6 +248,7 @@ const scorePreviewSchema = z.object({
contributorLogin: z.string().min(1).optional(),
labels: z.array(z.string()).optional(),
linkedIssueMode: z.enum(["none", "standard", "maintainer"]).default("none"),
linkedIssueContext: linkedIssueContextSchema.optional(),
sourceTokenScore: z.number().min(0).optional(),
totalTokenScore: z.number().min(0).optional(),
sourceLines: z.number().min(0).optional(),
Expand Down Expand Up @@ -1246,6 +1258,7 @@ export function createApp() {
scoringSnapshot: snapshot,
scoringProfile,
issueQuality: issueQuality?.report,
gittensorSnapshot: context.gittensorSnapshot,
});
const response = { ...analysis, dataQuality: await loadRepoDataQuality(c.env, parsed.data.repoFullName) };
await persistSignal(c.env, "local-branch-analysis", `${parsed.data.login}:${parsed.data.repoFullName}:${parsed.data.branchName ?? parsed.data.headRef ?? "local"}`, parsed.data.repoFullName, response as unknown as Record<string, JsonValue>, analysis.generatedAt);
Expand Down
2 changes: 2 additions & 0 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,8 @@ function toCacheableGittensorSnapshot(snapshot: Partial<GittensorContributorSnap
// serialize, store, read, and parse during webhook processing.
repositories: [],
pullRequests: [],
issueMirrorAvailable: Boolean(snapshot.issueMirrorAvailable),
issues: [],
issueLabels: [],
};
}
Expand Down
21 changes: 21 additions & 0 deletions src/gittensor/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ export type GittensorContributorSnapshot = {
baseScore: number;
tokenScore: number;
}>;
issueMirrorAvailable?: boolean | undefined;
issues?: Array<{
repoFullName: string;
number: number;
state: string;
solvedByPullRequest?: number | null | undefined;
labels: string[];
}> | undefined;
issueLabels: string[];
};

Expand All @@ -156,6 +164,7 @@ export async function fetchGittensorContributorSnapshot(login: string): Promise<
const detection = await fetchOfficialGittensorMiner(login);
return detection.status === "confirmed" ? detection.snapshot : null;
} catch {
/* v8 ignore next -- fetchOfficialGittensorMiner converts network failures into an unavailable status; this is a last-resort guard. */
return null;
}
}
Expand Down Expand Up @@ -236,6 +245,8 @@ async function buildGittensorContributorSnapshot(miner: ConfirmedGittensorMinerS
},
repositories: (detail.repositories ?? []).map(toRepositoryEvaluation).filter((repo) => repo.pullRequests + repo.openIssues + repo.closedIssues > 0),
pullRequests: pullRequests.map(toPullRequest).filter((pr) => pr.repoFullName && pr.number > 0),
issueMirrorAvailable: issuesResult.status === "fulfilled",
issues: issues.map(toIssue).filter((issue) => issue.repoFullName && issue.number > 0),
issueLabels: issues.flatMap((issue) => (issue.labels ?? []).flatMap((label) => (label.name ? [label.name] : []))),
};
}
Expand Down Expand Up @@ -285,6 +296,16 @@ function toPullRequest(pr: GittensorPullRequestResponse): GittensorContributorSn
};
}

function toIssue(issue: NonNullable<GittensorMinerIssuesResponse["issues"]>[number]): NonNullable<GittensorContributorSnapshot["issues"]>[number] {
return {
repoFullName: issue.repo_full_name ?? "",
number: asNumber(issue.issue_number),
state: issue.state ?? "UNKNOWN",
solvedByPullRequest: issue.solved_by_pr ?? null,
labels: (issue.labels ?? []).flatMap((label) => (label.name ? [label.name] : [])),
};
}

function asNumber(value: unknown, fallback = 0): number {
if (typeof value === "number") return Number.isFinite(value) ? value : fallback;
if (typeof value === "string") {
Expand Down
12 changes: 12 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,23 @@ const agentPlanShape = {
repoFullName: z.string().min(3).optional(),
};

const linkedIssueContextShape = {
status: z.enum(["raw", "plausible", "validated", "invalid", "unavailable"]).optional(),
source: z.enum(["user_supplied", "official_mirror", "github_cache", "issue_quality", "missing"]).optional(),
issueNumbers: z.array(z.number().int().positive()).max(50).optional(),
solvedByPullRequests: z.array(z.number().int().positive()).max(50).optional(),
reason: z.string().optional(),
warnings: z.array(z.string()).max(20).optional(),
};

const scorePreviewShape = {
repoFullName: z.string().min(3),
targetType: z.enum(["planned_pr", "pull_request", "local_diff", "variant"]).default("local_diff"),
targetKey: z.string().optional(),
contributorLogin: z.string().min(1).optional(),
labels: z.array(z.string()).optional(),
linkedIssueMode: z.enum(["none", "standard", "maintainer"]).default("none"),
linkedIssueContext: z.object(linkedIssueContextShape).strict().optional(),
sourceTokenScore: z.number().min(0).optional(),
totalTokenScore: z.number().min(0).optional(),
sourceLines: z.number().min(0).optional(),
Expand Down Expand Up @@ -901,6 +911,7 @@ export class GittensoryMcp {
scoringSnapshot: snapshot,
scoringProfile,
issueQuality: issueQuality?.report,
gittensorSnapshot: context.gittensorSnapshot,
}),
dataQuality: await this.loadRepoDataQuality(input.repoFullName),
};
Expand Down Expand Up @@ -952,6 +963,7 @@ export class GittensoryMcp {
repositories,
syncStates,
repoStats,
gittensorSnapshot,
outcomeHistory,
};
}
Expand Down
36 changes: 35 additions & 1 deletion src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,17 @@ const ScoreGatesSchema = z.object({
});

const ScoreGateBlockerSchema = z.object({
code: z.enum(["repo_not_registered", "inactive_allocation", "base_token_gate", "open_pr_threshold", "credibility_floor", "review_penalty", "metadata_only"]),
code: z.enum([
"repo_not_registered",
"inactive_allocation",
"base_token_gate",
"open_pr_threshold",
"credibility_floor",
"review_penalty",
"metadata_only",
"linked_issue_invalid",
"linked_issue_unvalidated",
]),
severity: z.enum(["blocker", "reducer", "context"]),
detail: z.string(),
});
Expand All @@ -964,6 +974,19 @@ const ScoreGateDeltaSchema = z.object({
explanation: z.string(),
});

const LinkedIssueMultiplierDecisionSchema = z.object({
mode: z.enum(["none", "standard", "maintainer"]),
status: z.enum(["not_required", "raw", "plausible", "validated", "invalid", "unavailable"]),
source: z.enum(["none", "user_supplied", "official_mirror", "github_cache", "issue_quality", "missing"]),
eligible: z.boolean(),
issueNumbers: z.array(z.number()),
solvedByPullRequests: z.array(z.number()),
baseMultiplier: z.number(),
appliedMultiplier: z.number(),
reason: z.string(),
warnings: z.array(z.string()),
});

const ScoreScenarioPreviewSchema = z.object({
name: z.enum(["current", "cleanGates", "afterPendingMerges", "afterApprovedPrsMerge", "afterStalePrsClose", "linkedIssueFixed", "bestReasonableCase"]),
source: z.enum(["current_data", "user_supplied", "github_observed", "gittensory_projection"]),
Expand All @@ -973,6 +996,7 @@ const ScoreScenarioPreviewSchema = z.object({
effectiveEstimatedScore: z.number(),
underlyingPotentialScore: z.number(),
blockedBy: z.array(ScoreGateBlockerSchema),
linkedIssueMultiplier: LinkedIssueMultiplierDecisionSchema,
deltaExplanation: z.string(),
});

Expand All @@ -985,6 +1009,7 @@ export const ScorePreviewResultSchema = z
privateOnly: z.literal(true),
laneMath: z.record(z.number()),
scoreEstimate: ScoreEstimateSchema,
linkedIssueMultiplier: LinkedIssueMultiplierDecisionSchema,
gates: ScoreGatesSchema,
effectiveEstimatedScore: z.number(),
underlyingPotentialScore: z.number(),
Expand Down Expand Up @@ -1025,6 +1050,15 @@ export const IssueQualityReportSchema = z
number: z.number(),
title: z.string(),
lifecycle: z.enum(["open", "closed_not_solved", "solved", "valid_solved", "stale", "duplicate", "invalid"]).optional(),
linkage: z
.object({
status: z.enum(["raw", "plausible", "validated", "invalid", "unavailable"]),
source: z.enum(["official_mirror", "github_cache", "missing"]),
solvedByPullRequests: z.array(z.number()),
reason: z.string(),
warnings: z.array(z.string()),
})
.optional(),
status: z.enum(["ready", "needs_proof", "hold", "do_not_use"]),
score: z.number(),
reasons: z.array(z.string()),
Expand Down
Loading