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
363 changes: 348 additions & 15 deletions apps/gittensory-ui/public/openapi.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,9 @@ const scorePreviewSchema = z.object({
existingContributorTokenScore: z.number().min(0).optional(),
prAgeHours: z.number().min(0).optional(),
openPrCount: z.number().int().min(0).optional(),
mergedPullRequests: z.number().int().min(0).optional(),
validSolvedIssues: z.number().int().min(0).optional(),
issueCredibility: z.number().min(0).max(1).optional(),
credibility: z.number().min(0).max(1).optional(),
changesRequestedCount: z.number().int().min(0).optional(),
duplicateRiskCount: z.number().int().min(0).optional(),
Expand Down
25 changes: 24 additions & 1 deletion src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,9 @@ const ScoreEstimateSchema = z.object({
credibilityMultiplier: z.number(),
reviewPenaltyMultiplier: z.number(),
openPrMultiplier: z.number(),
openIssueMultiplier: z.number(),
mergedHistoryMultiplier: z.number(),
issueDiscoveryHistoryMultiplier: z.number(),
timeDecayMultiplier: z.number(),
estimatedMergedScore: z.number(),
pendingSaturationScore: z.number(),
Expand All @@ -1279,6 +1282,14 @@ const ScoreGatesSchema = z.object({
reviewCollateralMultiplier: z.number(),
credibilityFloor: z.number(),
credibilityObserved: z.number(),
openIssueThreshold: z.number(),
openIssueCount: z.number(),
mergedPrFloor: z.number(),
mergedPullRequests: z.number().optional(),
validSolvedIssuesFloor: z.number(),
validSolvedIssues: z.number().optional(),
issueCredibilityFloor: z.number(),
issueCredibility: z.number().optional(),
});

const BranchEligibilitySchema = z.object({
Expand All @@ -1298,20 +1309,32 @@ const ScoreGateBlockerSchema = z.object({
"inactive_allocation",
"base_token_gate",
"open_pr_threshold",
"open_issue_threshold",
"merged_pr_history_floor",
"issue_discovery_validity_floor",
"credibility_floor",
"review_penalty",
"metadata_only",
"linked_issue_invalid",
"linked_issue_unvalidated",
"branch_ineligible",
"branch_eligibility_missing",
"duplicate_risk",
"stale_work",
]),
severity: z.enum(["blocker", "reducer", "context"]),
detail: z.string(),
});

const ScoreGateDeltaSchema = z.object({
gate: z.enum(["open_pr_threshold", "credibility_floor", "linked_issue_multiplier"]),
gate: z.enum([
"open_pr_threshold",
"open_issue_threshold",
"merged_pr_history_floor",
"issue_discovery_validity_floor",
"credibility_floor",
"linked_issue_multiplier",
]),
current: z.string(),
projected: z.string(),
explanation: z.string(),
Expand Down
2 changes: 2 additions & 0 deletions src/scenarios/scenario-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ const PUBLIC_BLOCKER_TEXT: Partial<Record<ScoreGateBlocker["code"], string>> = {
base_token_gate: "The change size may be too small to meet the contribution threshold.",
open_pr_threshold: "Too many concurrent open PRs exist; landing or closing some would help.",
open_issue_threshold: "Too many open issues exist; closing excess issues would help.",
merged_pr_history_floor: "Merged PR history on this repo is below the upstream eligibility floor.",
issue_discovery_validity_floor: "Valid solved-issue history or issue credibility is below the upstream issue-discovery floor.",
credibility_floor: "Contributor credibility evidence is below the expected floor.",
review_penalty: "Review churn history may reduce the contribution quality signal.",
metadata_only: "Only metadata signals are available; detailed analysis requires full context.",
Expand Down
148 changes: 144 additions & 4 deletions src/scoring/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export type ScorePreviewInput = {
openPrCount?: number | undefined;
/** Contributor's current open-issue count for the repo, used for the open-issue spam gate (#808). */
openIssueCount?: number | undefined;
/** Repo-level merged PR count for upstream contributor-history eligibility (#808). */
mergedPullRequests?: number | undefined;
/** Count of valid solved issues for upstream issue-discovery eligibility (#808). */
validSolvedIssues?: number | undefined;
/** Issue-discovery credibility for upstream issue-discovery eligibility (#808). */
issueCredibility?: number | undefined;
credibility?: number | undefined;
changesRequestedCount?: number | undefined;
fixedBaseScore?: number | undefined;
Expand Down Expand Up @@ -107,6 +113,8 @@ export type ScoreGateBlocker = {
| "base_token_gate"
| "open_pr_threshold"
| "open_issue_threshold"
| "merged_pr_history_floor"
| "issue_discovery_validity_floor"
| "credibility_floor"
| "review_penalty"
| "metadata_only"
Expand All @@ -121,7 +129,13 @@ export type ScoreGateBlocker = {
};

export type ScoreGateDelta = {
gate: "open_pr_threshold" | "open_issue_threshold" | "credibility_floor" | "linked_issue_multiplier";
gate:
| "open_pr_threshold"
| "open_issue_threshold"
| "merged_pr_history_floor"
| "issue_discovery_validity_floor"
| "credibility_floor"
| "linked_issue_multiplier";
current: string;
projected: string;
explanation: string;
Expand Down Expand Up @@ -164,6 +178,10 @@ export type ScorePreviewResult = {
reviewPenaltyMultiplier: number;
openPrMultiplier: number;
openIssueMultiplier: number;
/** Upstream merged-PR history floor (#808). 0 when below MIN_VALID_MERGED_PRS; 1 when unknown or eligible. */
mergedHistoryMultiplier: number;
/** Upstream issue-discovery validity floor (#808). 0 when below MIN_VALID_SOLVED_ISSUES or MIN_ISSUE_CREDIBILITY. */
issueDiscoveryHistoryMultiplier: number;
/** Upstream sigmoid time-decay multiplier (#703). 1 = no decay (fresh PR, or feature off). */
timeDecayMultiplier: number;
estimatedMergedScore: number;
Expand All @@ -182,6 +200,15 @@ export type ScorePreviewResult = {
credibilityObserved: number;
openIssueThreshold: number;
openIssueCount: number;
mergedPrFloor: number;
/** Observed merged PR count when supplied or inferred from contributor evidence; absent when unknown. */
mergedPullRequests?: number | undefined;
validSolvedIssuesFloor: number;
/** Observed valid solved-issue count when supplied; absent when unknown. */
validSolvedIssues?: number | undefined;
issueCredibilityFloor: number;
/** Observed issue-discovery credibility when supplied; absent when unknown. */
issueCredibility?: number | undefined;
};
branchEligibility: BranchEligibilityResult;
effectiveEstimatedScore: number;
Expand Down Expand Up @@ -217,6 +244,10 @@ export function buildScorePreview(args: {
...(!current.gates.baseTokenGatePassed ? ["Increase meaningful source change size or scope clarity before relying on this preview."] : []),
...(current.scoreEstimate.openPrMultiplier === 0 ? ["Land or close existing open PRs before opening more concurrent work."] : []),
...(current.scoreEstimate.openIssueMultiplier === 0 ? ["Close excess open issues to stay within the open-issue spam threshold."] : []),
...(current.scoreEstimate.mergedHistoryMultiplier === 0 ? ["Build merged PR history on this repo before relying on this preview; upstream requires a minimum merged count."] : []),
...(current.scoreEstimate.issueDiscoveryHistoryMultiplier === 0
? ["Build valid solved-issue history and issue credibility before relying on issue-discovery scoring on this repo."]
: []),
...(current.scoreEstimate.credibilityMultiplier < 1 ? ["Build or wait for contributor credibility evidence before relying on this preview."] : []),
...(current.scoreEstimate.reviewPenaltyMultiplier < 1 ? ["Reduce review churn with tighter tests and clearer evidence."] : []),
...(current.scoreEstimate.labelMultiplier <= 1 && Object.keys(args.repo?.registryConfig?.labelMultipliers ?? {}).length > 0
Expand Down Expand Up @@ -365,13 +396,40 @@ function computeScoreCore(
Math.floor(nonNegative(input.existingContributorTokenScore) / constant(constants, "OPEN_ISSUE_SPAM_TOKEN_SCORE_PER_SLOT")),
);
const openIssueMultiplier = openIssueCount <= openIssueThreshold ? 1 : 0;
const mergedPrFloor = constant(constants, "MIN_VALID_MERGED_PRS");
const mergedPullRequestsObserved = resolveMergedPullRequests(input, contributorEvidence);
const mergedHistoryMultiplier =
mergedPullRequestsObserved === undefined ? 1 : mergedPullRequestsObserved >= mergedPrFloor ? 1 : 0;
const validSolvedIssuesFloor = constant(constants, "MIN_VALID_SOLVED_ISSUES");
const issueCredibilityFloor = constant(constants, "MIN_ISSUE_CREDIBILITY");
const validSolvedIssuesObserved = input.validSolvedIssues !== undefined ? nonNegative(input.validSolvedIssues) : undefined;
const issueCredibilityObserved = input.issueCredibility !== undefined ? clamp(input.issueCredibility, 0, 1) : undefined;
// Issue-discovery validity mirrors upstream's separate issue lane — only gate previews that
// actually claim linked-issue / issue-discovery scoring, not every repo with a non-zero share.
const issueDiscoveryRelevant = (input.linkedIssueMode ?? "none") !== "none";
const issueDiscoveryHistoryKnown = validSolvedIssuesObserved !== undefined && issueCredibilityObserved !== undefined;
const issueDiscoveryHistoryMultiplier =
!issueDiscoveryRelevant || !issueDiscoveryHistoryKnown
? 1
: validSolvedIssuesObserved >= validSolvedIssuesFloor && issueCredibilityObserved >= issueCredibilityFloor
? 1
: 0;
// Upstream time-decay (#703): mirrors upstream's `scored.time_decay_multiplier` applied to a PR's score.
// Opt-in + env-gated (default off). A fresh PR (prAgeHours below the grace period) yields 1.0, so a normal
// new-PR preview is unchanged even when enabled — only an aged-PR projection decays.
// Per-repo curve (#703): the repo's registry `scoring.time_decay` overrides overlay the snapshot defaults.
const timeDecayMultiplier = input.applyTimeDecay ? calculateTimeDecay(nonNegative(input.prAgeHours), constants, config?.timeDecay) : 1;
const estimatedMergedScore = roundScore(
baseScore * labelMultiplier * issueMultiplier * credibilityMultiplier * reviewPenaltyMultiplier * openPrMultiplier * openIssueMultiplier * timeDecayMultiplier,
baseScore *
labelMultiplier *
issueMultiplier *
credibilityMultiplier *
reviewPenaltyMultiplier *
openPrMultiplier *
openIssueMultiplier *
mergedHistoryMultiplier *
issueDiscoveryHistoryMultiplier *
timeDecayMultiplier,
);
const pendingSaturationScore = roundScore(saturationBaseScore);
return {
Expand All @@ -393,6 +451,8 @@ function computeScoreCore(
reviewPenaltyMultiplier: roundScore(reviewPenaltyMultiplier),
openPrMultiplier,
openIssueMultiplier,
mergedHistoryMultiplier,
issueDiscoveryHistoryMultiplier,
timeDecayMultiplier: roundScore(timeDecayMultiplier),
estimatedMergedScore,
pendingSaturationScore,
Expand All @@ -408,6 +468,12 @@ function computeScoreCore(
credibilityObserved,
openIssueThreshold,
openIssueCount,
mergedPrFloor,
...(mergedPullRequestsObserved !== undefined ? { mergedPullRequests: mergedPullRequestsObserved } : {}),
validSolvedIssuesFloor,
...(validSolvedIssuesObserved !== undefined ? { validSolvedIssues: validSolvedIssuesObserved } : {}),
issueCredibilityFloor,
...(issueCredibilityObserved !== undefined ? { issueCredibility: issueCredibilityObserved } : {}),
},
};
}
Expand Down Expand Up @@ -454,12 +520,25 @@ function buildScenarioPreviews(
const cleanGatesInput = {
...input,
openPrCount: Math.min(current.gates.openPrCount, current.gates.openPrThreshold),
openIssueCount: Math.min(current.gates.openIssueCount, current.gates.openIssueThreshold),
credibility: Math.max(current.gates.credibilityObserved, current.gates.credibilityFloor),
...(current.gates.mergedPullRequests !== undefined
? { mergedPullRequests: Math.max(current.gates.mergedPullRequests, current.gates.mergedPrFloor) }
: {}),
...(current.gates.validSolvedIssues !== undefined
? { validSolvedIssues: Math.max(current.gates.validSolvedIssues, current.gates.validSolvedIssuesFloor) }
: {}),
...(current.gates.issueCredibility !== undefined
? { issueCredibility: Math.max(current.gates.issueCredibility, current.gates.issueCredibilityFloor) }
: {}),
};
const afterPendingInput = {
...input,
openPrCount: expectedOpenPrCountAfterMerge,
credibility: projectedCredibility,
...(current.gates.mergedPullRequests !== undefined
? { mergedPullRequests: nonNegative(current.gates.mergedPullRequests) + mergeReadyPending }
: {}),
};
const linkedIssueInput = withValidatedLinkedIssueScenario(input);
const bestReasonableInput = {
Expand All @@ -472,11 +551,25 @@ function buildScenarioPreviews(
// "best reasonable case" can clear the open-issue gate just like it clears open-PR pressure.
openIssueCount: Math.min(current.gates.openIssueCount, current.gates.openIssueThreshold),
credibility: Math.max(projectedCredibility, observedApprovalCredibility, current.gates.credibilityFloor),
...(current.gates.mergedPullRequests !== undefined
? {
mergedPullRequests: Math.max(
nonNegative(current.gates.mergedPullRequests) + mergeReadyPending,
current.gates.mergedPrFloor,
),
}
: {}),
...(current.gates.validSolvedIssues !== undefined
? { validSolvedIssues: Math.max(current.gates.validSolvedIssues, current.gates.validSolvedIssuesFloor) }
: {}),
...(current.gates.issueCredibility !== undefined
? { issueCredibility: Math.max(current.gates.issueCredibility, current.gates.issueCredibilityFloor) }
: {}),
};
return [
scenario("current", "current_data", input, current, ["Current cached/account state and supplied local diff metadata."], repo),
scenario("cleanGates", "gittensory_projection", cleanGatesInput, computeScoreCore(cleanGatesInput, repo, snapshot, contributorEvidence), [
"Open PR and credibility gates are projected as cleared; branch metadata is otherwise unchanged.",
"Open PR, open-issue, credibility, and contributor-history gates are projected as cleared; branch metadata is otherwise unchanged.",
], repo),
scenario(
"afterPendingMerges",
Expand Down Expand Up @@ -535,7 +628,7 @@ function buildScenarioPreviews(
: "Linked issue mode was already supplied; this scenario projects solved-by-PR validation where needed.",
], repo),
scenario("bestReasonableCase", "gittensory_projection", bestReasonableInput, computeScoreCore(bestReasonableInput, repo, snapshot, contributorEvidence), [
"Combines plausible near-term gate cleanup: open PR pressure at threshold or below, open-issue spam pressure at threshold or below, credibility at floor or above, and linked-issue context where applicable.",
"Combines plausible near-term gate cleanup: open PR pressure at threshold or below, open-issue spam pressure at threshold or below, credibility at floor or above, contributor merged-history and issue-discovery validity at floor or above, and linked-issue context where applicable.",
...(input.scenarioNotes ?? []),
...observedScenarioNotes(input),
], repo),
Expand Down Expand Up @@ -627,6 +720,24 @@ function blockedByFor(input: ScorePreviewInput, repo: RepositoryRecord | null, c
},
]
: []),
...(core.scoreEstimate.mergedHistoryMultiplier === 0
? [
{
code: "merged_pr_history_floor" as const,
severity: "blocker" as const,
detail: `Merged PR count ${core.gates.mergedPullRequests} is below upstream floor ${core.gates.mergedPrFloor}.`,
},
]
: []),
...(core.scoreEstimate.issueDiscoveryHistoryMultiplier === 0
? [
{
code: "issue_discovery_validity_floor" as const,
severity: "blocker" as const,
detail: `Issue-discovery history (${core.gates.validSolvedIssues} valid solved, credibility ${roundScore(core.gates.issueCredibility!)}) is below upstream floors (${core.gates.validSolvedIssuesFloor} valid solved, ${core.gates.issueCredibilityFloor} credibility).`,
},
]
: []),
...(core.gates.credibilityObserved < core.gates.credibilityFloor
? [
{
Expand Down Expand Up @@ -709,6 +820,26 @@ function buildGateDeltas(current: ScoreCore, scenarios: ScoreScenarioPreview[]):
},
]
: []),
...(current.scoreEstimate.mergedHistoryMultiplier !== best.scoreEstimate.mergedHistoryMultiplier
? [
{
gate: "merged_pr_history_floor" as const,
current: `${current.gates.mergedPullRequests}/${current.gates.mergedPrFloor} merged PRs, multiplier ${current.scoreEstimate.mergedHistoryMultiplier}`,
projected: `${best.gates.mergedPullRequests}/${best.gates.mergedPrFloor} merged PRs, multiplier ${best.scoreEstimate.mergedHistoryMultiplier}`,
explanation: `Merged PR history changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${best.scoreEstimate.estimatedMergedScore}.`,
},
]
: []),
...(current.scoreEstimate.issueDiscoveryHistoryMultiplier !== best.scoreEstimate.issueDiscoveryHistoryMultiplier
? [
{
gate: "issue_discovery_validity_floor" as const,
current: `${current.gates.validSolvedIssues} valid solved / ${roundScore(current.gates.issueCredibility!)} credibility, multiplier ${current.scoreEstimate.issueDiscoveryHistoryMultiplier}`,
projected: `${best.gates.validSolvedIssues} valid solved / ${roundScore(best.gates.issueCredibility!)} credibility, multiplier ${best.scoreEstimate.issueDiscoveryHistoryMultiplier}`,
explanation: `Issue-discovery validity changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${best.scoreEstimate.estimatedMergedScore}.`,
},
]
: []),
...(current.gates.credibilityObserved !== best.gates.credibilityObserved || current.scoreEstimate.credibilityMultiplier !== best.scoreEstimate.credibilityMultiplier
? [
{
Expand Down Expand Up @@ -1010,6 +1141,15 @@ function normalizeBranchEligibility(input: ScorePreviewInput): BranchEligibility
};
}

function resolveMergedPullRequests(
input: Pick<ScorePreviewInput, "mergedPullRequests">,
contributorEvidence?: ContributorEvidenceRecord | null,
): number | undefined {
if (input.mergedPullRequests !== undefined) return nonNegative(input.mergedPullRequests);
const fromEvidence = Number(contributorEvidence?.payload?.mergedPullRequests);
return Number.isFinite(fromEvidence) ? nonNegative(fromEvidence) : undefined;
}

function inferCredibility(evidence?: ContributorEvidenceRecord | null): number {
const payload = evidence?.payload;
const merged = Number(payload?.mergedPullRequests ?? 0);
Expand Down
5 changes: 4 additions & 1 deletion src/signals/local-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,9 @@ function buildLocalScoreInput(args: {
nonCodeLines: nonCodeLineCount,
openPrCount: args.outcomeHistory.totals.openPullRequests,
openIssueCount: args.repoOutcome?.openIssues ?? args.outcomeHistory.totals.openIssues,
mergedPullRequests: args.repoOutcome?.mergedPullRequests ?? args.outcomeHistory.totals.mergedPullRequests,
validSolvedIssues: args.repoOutcome?.validSolvedIssues ?? args.outcomeHistory.totals.validSolvedIssues,
issueCredibility: args.repoOutcome?.issueCredibility ?? args.outcomeHistory.totals.issueCredibility,
credibility: args.repoOutcome?.credibility ?? args.outcomeHistory.totals.credibility,
metadataOnly: scorer?.mode !== "gittensor_root" && scorer?.mode !== "external_command",
pendingMergedPrCount: args.input.pendingMergedPrCount,
Expand Down Expand Up @@ -1003,7 +1006,7 @@ function branchQualityBlockersFor(preflight: LocalDiffPreflightResult, localFind

function accountStateBlockersFor(scorePreview: ScorePreviewResult): string[] {
return scorePreview.blockedBy
.filter((blocker) => ["repo_not_registered", "inactive_allocation", "open_pr_threshold", "credibility_floor"].includes(blocker.code))
.filter((blocker) => ["repo_not_registered", "inactive_allocation", "open_pr_threshold", "open_issue_threshold", "merged_pr_history_floor", "issue_discovery_validity_floor", "credibility_floor"].includes(blocker.code))
.map((blocker) => blocker.detail)
.filter(unique);
}
Expand Down
Loading
Loading