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
2 changes: 2 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,9 @@ export function createApp() {
repo,
issues,
pullRequests,
contributorPullRequests: context.contributorPullRequests,
recentMergedPullRequests,
repositories: context.repositories,
profile: context.profile,
outcomeHistory: context.outcomeHistory,
scoringSnapshot: snapshot,
Expand Down
38 changes: 36 additions & 2 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1918,14 +1918,24 @@ function toPullRequestRecord(repoFullName: string, pr: GitHubPullRequestPayload)
baseRef: pr.base?.ref,
htmlUrl: pr.html_url,
mergedAt: pr.merged_at,
isDraft: pr.draft ?? pr.isDraft,
mergeableState: pr.mergeable_state ?? pr.mergeableState ?? mergeableBooleanState(pr.mergeable),
reviewDecision: pr.reviewDecision,
body: pr.body,
labels: (pr.labels ?? []).flatMap((label) => (label.name ? [label.name] : [])),
linkedIssues: extractLinkedIssueNumbers(pr.body ?? ""),
};
}

function toPullRequestRecordFromRow(row: typeof pullRequests.$inferSelect): PullRequestRecord {
const payload = parseJson<{ body?: string | null; created_at?: string | null; updated_at?: string | null }>(row.payloadJson, {});
const payload = parseJson<{
body?: string | null;
created_at?: string | null;
updated_at?: string | null;
draft?: boolean | null;
mergeable_state?: string | null;
reviewDecision?: string | null;
}>(row.payloadJson, {});
return {
repoFullName: row.repoFullName,
number: row.number,
Expand All @@ -1938,6 +1948,9 @@ function toPullRequestRecordFromRow(row: typeof pullRequests.$inferSelect): Pull
baseRef: row.baseRef,
htmlUrl: row.htmlUrl,
mergedAt: row.mergedAt,
isDraft: payload.draft,
mergeableState: payload.mergeable_state,
reviewDecision: payload.reviewDecision,
body: payload.body,
createdAt: payload.created_at,
updatedAt: payload.updated_at ?? row.updatedAt,
Expand All @@ -1961,14 +1974,35 @@ function toIssueRecord(repoFullName: string, issue: GitHubIssuePayload): IssueRe
};
}

function compactGitHubPayload(payload: { body?: string | null; created_at?: string | null; updated_at?: string | null }): Record<string, JsonValue> {
function compactGitHubPayload(payload: {
body?: string | null;
created_at?: string | null;
updated_at?: string | null;
draft?: boolean | null;
isDraft?: boolean | null;
mergeable?: boolean | null;
mergeable_state?: string | null;
mergeableState?: string | null;
reviewDecision?: string | null;
}): Record<string, JsonValue> {
const draft = payload.draft ?? payload.isDraft;
const mergeableState = payload.mergeable_state ?? payload.mergeableState ?? mergeableBooleanState(payload.mergeable);
return {
body: truncateBody(payload.body),
created_at: payload.created_at ?? null,
updated_at: payload.updated_at ?? null,
...(draft !== undefined ? { draft } : {}),
...(mergeableState !== undefined ? { mergeable_state: mergeableState } : {}),
...(payload.reviewDecision !== undefined ? { reviewDecision: payload.reviewDecision } : {}),
};
}

function mergeableBooleanState(value: boolean | null | undefined): string | undefined {
if (value === true) return "mergeable";
if (value === false) return "blocked";
return undefined;
}

function truncateBody(body: string | null | undefined): string | null {
if (!body) return body ?? null;
return body.length > MAX_STORED_BODY_CHARS ? body.slice(0, MAX_STORED_BODY_CHARS) : body;
Expand Down
9 changes: 9 additions & 0 deletions src/github/backfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ type GitHubOpenPullRequestsResponse = {
state?: string;
url?: string;
body?: string | null;
isDraft?: boolean | null;
mergeable?: string | null;
reviewDecision?: string | null;
createdAt?: string | null;
updatedAt?: string | null;
authorAssociation?: string | null;
Expand Down Expand Up @@ -1125,6 +1128,9 @@ async function supplementOpenPullRequestsFromGraphQl(env: Env, repo: RepositoryR
state
url
body
isDraft
mergeable
reviewDecision
createdAt
updatedAt
authorAssociation
Expand All @@ -1151,6 +1157,9 @@ async function supplementOpenPullRequestsFromGraphQl(env: Env, repo: RepositoryR
...(pr.createdAt === undefined ? {} : { created_at: pr.createdAt }),
...(pr.updatedAt === undefined ? {} : { updated_at: pr.updatedAt }),
...(pr.body === undefined ? {} : { body: pr.body }),
...(pr.isDraft === undefined ? {} : { draft: pr.isDraft }),
...(pr.mergeable === undefined ? {} : { mergeableState: pr.mergeable }),
...(pr.reviewDecision === undefined ? {} : { reviewDecision: pr.reviewDecision }),
...(pr.author?.login ? { user: { login: pr.author.login } } : {}),
...(pr.authorAssociation ? { author_association: pr.authorAssociation } : {}),
head: { ...(pr.headRefOid ? { sha: pr.headRefOid } : {}), ...(pr.headRefName ? { ref: pr.headRefName } : {}) },
Expand Down
3 changes: 3 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,9 @@ export class GittensoryMcp {
repo,
issues,
pullRequests,
contributorPullRequests: context.contributorPullRequests,
recentMergedPullRequests,
repositories: context.repositories,
profile: context.profile,
outcomeHistory: context.outcomeHistory,
scoringSnapshot: snapshot,
Expand Down Expand Up @@ -798,6 +800,7 @@ export class GittensoryMcp {
});
return {
profile,
contributorPullRequests,
repositories,
syncStates,
repoStats,
Expand Down
15 changes: 13 additions & 2 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -829,8 +829,8 @@ const ScoreGateDeltaSchema = z.object({
});

const ScoreScenarioPreviewSchema = z.object({
name: z.enum(["current", "cleanGates", "afterPendingMerges", "linkedIssueFixed", "bestReasonableCase"]),
source: z.enum(["current_data", "user_supplied", "gittensory_projection"]),
name: z.enum(["current", "cleanGates", "afterPendingMerges", "afterApprovedPrsMerge", "afterStalePrsClose", "linkedIssueFixed", "bestReasonableCase"]),
source: z.enum(["current_data", "user_supplied", "github_observed", "gittensory_projection"]),
assumptions: z.array(z.string()),
scoreEstimate: ScoreEstimateSchema,
gates: ScoreGatesSchema,
Expand Down Expand Up @@ -1243,9 +1243,20 @@ export const LocalBranchAnalysisSchema = z
current: ScoreScenarioPreviewSchema,
bestReasonableCase: ScoreScenarioPreviewSchema,
afterPendingMerges: ScoreScenarioPreviewSchema.optional(),
afterApprovedPrsMerge: ScoreScenarioPreviewSchema.optional(),
afterStalePrsClose: ScoreScenarioPreviewSchema.optional(),
gateDeltas: z.array(ScoreGateDeltaSchema),
blockedBy: z.array(ScoreGateBlockerSchema),
}),
observedPullRequestScenarios: z.object({
approvedOrMergeable: z.number(),
stale: z.number(),
closed: z.number(),
draft: z.number(),
blocked: z.number(),
maintainerLane: z.number(),
notes: z.array(z.string()),
}),
rewardRisk: RepoRewardRiskSchema,
scoreBlockers: z.array(z.string()),
branchQualityBlockers: z.array(z.string()),
Expand Down
86 changes: 74 additions & 12 deletions src/scoring/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,16 @@ export type ScorePreviewInput = {
pendingMergedPrCount?: number | undefined;
pendingClosedPrCount?: number | undefined;
approvedPrCount?: number | undefined;
observedApprovedPrCount?: number | undefined;
observedStalePrCount?: number | undefined;
observedClosedPrCount?: number | undefined;
observedDraftPrCount?: number | undefined;
observedBlockedPrCount?: number | undefined;
observedMaintainerPrCount?: number | undefined;
expectedOpenPrCountAfterMerge?: number | undefined;
projectedCredibility?: number | undefined;
scenarioNotes?: string[] | undefined;
observedScenarioNotes?: string[] | undefined;
};

export type ScoreGateBlocker = {
Expand All @@ -48,8 +55,8 @@ export type ScoreGateDelta = {
};

export type ScoreScenarioPreview = {
name: "current" | "cleanGates" | "afterPendingMerges" | "linkedIssueFixed" | "bestReasonableCase";
source: "current_data" | "user_supplied" | "gittensory_projection";
name: "current" | "cleanGates" | "afterPendingMerges" | "afterApprovedPrsMerge" | "afterStalePrsClose" | "linkedIssueFixed" | "bestReasonableCase";
source: "current_data" | "user_supplied" | "github_observed" | "gittensory_projection";
assumptions: string[];
scoreEstimate: ScorePreviewResult["scoreEstimate"];
gates: ScorePreviewResult["gates"];
Expand Down Expand Up @@ -268,15 +275,29 @@ function buildScenarioPreviews(
contributorEvidence: ContributorEvidenceRecord | null | undefined,
current: ScoreCore,
): ScoreScenarioPreview[] {
const pendingCount = nonNegative(input.pendingMergedPrCount) + nonNegative(input.pendingClosedPrCount) + nonNegative(input.approvedPrCount);
const userPendingCount = nonNegative(input.pendingMergedPrCount) + nonNegative(input.pendingClosedPrCount) + nonNegative(input.approvedPrCount);
const observedApprovedCount = nonNegative(input.observedApprovedPrCount);
const observedCloseCount = nonNegative(input.observedStalePrCount) + nonNegative(input.observedClosedPrCount);
const combinedPendingCount = userPendingCount + observedApprovedCount + observedCloseCount;
const expectedOpenPrCountAfterMerge =
input.expectedOpenPrCountAfterMerge !== undefined ? nonNegative(input.expectedOpenPrCountAfterMerge) : Math.max(0, current.gates.openPrCount - pendingCount);
input.expectedOpenPrCountAfterMerge !== undefined ? nonNegative(input.expectedOpenPrCountAfterMerge) : Math.max(0, current.gates.openPrCount - userPendingCount);
const projectedCredibility =
input.projectedCredibility !== undefined
? clamp(input.projectedCredibility, 0, 1)
: pendingCount > 0
: userPendingCount > 0
? Math.max(current.gates.credibilityObserved, current.gates.credibilityFloor)
: current.gates.credibilityObserved;
const observedApprovalCredibility = observedApprovedCount > 0 ? Math.max(current.gates.credibilityObserved, current.gates.credibilityFloor) : current.gates.credibilityObserved;
const afterApprovedInput = {
...input,
openPrCount: Math.max(0, current.gates.openPrCount - observedApprovedCount),
credibility: observedApprovalCredibility,
};
const afterStaleInput = {
...input,
openPrCount: Math.max(0, current.gates.openPrCount - observedCloseCount),
credibility: current.gates.credibilityObserved,
};
const cleanGatesInput = {
...input,
openPrCount: Math.min(current.gates.openPrCount, current.gates.openPrThreshold),
Expand All @@ -293,8 +314,11 @@ function buildScenarioPreviews(
};
const bestReasonableInput = {
...linkedIssueInput,
openPrCount: Math.min(expectedOpenPrCountAfterMerge, current.gates.openPrThreshold),
credibility: Math.max(projectedCredibility, current.gates.credibilityFloor),
openPrCount: Math.min(
input.expectedOpenPrCountAfterMerge !== undefined ? expectedOpenPrCountAfterMerge : Math.max(0, current.gates.openPrCount - combinedPendingCount),
current.gates.openPrThreshold,
),
credibility: Math.max(projectedCredibility, observedApprovalCredibility, current.gates.credibilityFloor),
};
return [
scenario("current", "current_data", input, current, ["Current cached/account state and supplied local diff metadata."], repo),
Expand All @@ -303,22 +327,50 @@ function buildScenarioPreviews(
], repo),
scenario(
"afterPendingMerges",
pendingCount > 0 || input.expectedOpenPrCountAfterMerge !== undefined || input.projectedCredibility !== undefined ? "user_supplied" : "gittensory_projection",
userPendingCount > 0 || input.expectedOpenPrCountAfterMerge !== undefined || input.projectedCredibility !== undefined ? "user_supplied" : "gittensory_projection",
afterPendingInput,
computeScoreCore(afterPendingInput, repo, snapshot, contributorEvidence),
[
pendingCount > 0
? `${pendingCount} supplied pending approved/merged/closed PR(s) are treated as no longer open for this scenario.`
userPendingCount > 0
? `${userPendingCount} user-supplied pending approved/merged/closed PR(s) are treated as no longer open for this scenario.`
: "No pending merge/close count was supplied; this scenario preserves current open PR pressure.",
...(input.projectedCredibility !== undefined
? [`Projected credibility is user-supplied as ${roundScore(projectedCredibility)}.`]
: pendingCount > 0
? [`Projected credibility is raised to the current floor ${current.gates.credibilityFloor} because pending merges were supplied.`]
: userPendingCount > 0
? [`Projected credibility is raised to the current floor ${current.gates.credibilityFloor} because pending merges were supplied by the caller.`]
: []),
...(input.scenarioNotes ?? []),
],
repo,
),
scenario(
"afterApprovedPrsMerge",
"github_observed",
afterApprovedInput,
computeScoreCore(afterApprovedInput, repo, snapshot, contributorEvidence),
[
observedApprovedCount > 0
? `${observedApprovedCount} GitHub-observed approved or mergeable open PR(s) are treated as no longer open if they merge.`
: "No GitHub-observed approved or mergeable open PRs were available for this scenario.",
...(observedApprovedCount > 0 ? [`Projected credibility is raised to the current floor ${current.gates.credibilityFloor} after observed mergeable work lands.`] : []),
...observedScenarioNotes(input),
],
repo,
),
scenario(
"afterStalePrsClose",
"github_observed",
afterStaleInput,
computeScoreCore(afterStaleInput, repo, snapshot, contributorEvidence),
[
observedCloseCount > 0
? `${observedCloseCount} GitHub-observed stale or closed PR(s) are treated as no longer open if they close or withdraw.`
: "No GitHub-observed stale or closed PRs were available for this scenario.",
"Credibility is not increased in this scenario because stale or closed PR cleanup is not the same as merged work.",
...observedScenarioNotes(input),
],
repo,
),
scenario("linkedIssueFixed", "gittensory_projection", linkedIssueInput, computeScoreCore(linkedIssueInput, repo, snapshot, contributorEvidence), [
input.linkedIssueMode === "none" || !input.linkedIssueMode
? "A standard linked-issue/no-issue rationale multiplier is projected as present."
Expand All @@ -327,10 +379,20 @@ function buildScenarioPreviews(
scenario("bestReasonableCase", "gittensory_projection", bestReasonableInput, computeScoreCore(bestReasonableInput, repo, snapshot, contributorEvidence), [
"Combines plausible near-term gate cleanup: open PR pressure at threshold or below, credibility at floor or above, and linked-issue context where applicable.",
...(input.scenarioNotes ?? []),
...observedScenarioNotes(input),
], repo),
];
}

function observedScenarioNotes(input: ScorePreviewInput): string[] {
return [
...(nonNegative(input.observedDraftPrCount) > 0 ? [`${nonNegative(input.observedDraftPrCount)} draft PR(s) were excluded from likely-to-land projections.`] : []),
...(nonNegative(input.observedBlockedPrCount) > 0 ? [`${nonNegative(input.observedBlockedPrCount)} blocked PR(s) were excluded from likely-to-land projections.`] : []),
...(nonNegative(input.observedMaintainerPrCount) > 0 ? [`${nonNegative(input.observedMaintainerPrCount)} maintainer-lane PR(s) were kept out of outside-contributor projections.`] : []),
...(input.observedScenarioNotes ?? []),
];
}

function scenario(
name: ScoreScenarioPreview["name"],
source: ScoreScenarioPreview["source"],
Expand Down
2 changes: 2 additions & 0 deletions src/services/agent-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,9 @@ async function analyzeLocalBranch(env: Env, input: LocalBranchAnalysisInput): Pr
repo,
issues,
pullRequests,
contributorPullRequests,
recentMergedPullRequests,
repositories,
profile,
outcomeHistory,
scoringSnapshot,
Expand Down
Loading