Skip to content
Closed
37 changes: 37 additions & 0 deletions src/services/decision-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
import { loadIssueQualityReportMap } from "./issue-quality";
import { loadRepoOutcomePatternsMap } from "./repo-outcome-patterns";
import { evaluateRecommendationOutcomes } from "./recommendation-outcomes";
import type { AdvisoryAdviceItem, EligibilityGapEntry } from "../signals/reward-risk";
import type {
BountyRecord,
AgentRecommendationOutcomeRepoSummary,
Expand Down Expand Up @@ -166,6 +167,8 @@ export type ContributorDecisionPack = {
summary: string;
nextActions: string[];
openPrMonitor?: ContributorOpenPrMonitor | undefined;
advisoryAdvice: AdvisoryAdviceItem[];
eligibilityGapRepos: EligibilityGapEntry[];
};

export type DecisionPackRefreshNeeded = {
Expand Down Expand Up @@ -690,6 +693,8 @@ function buildContributorDecisionPack(args: {
summary: `${args.login} has ${topActions.length} ranked action(s), ${scoreBlockers.length} scoreability blocker(s), and ${repoDecisions.length} registered repo decision(s).${monitorSummary}${recommendationFeedbackSummary(recommendationOutcomeFeedback)}`,
nextActions: packNextActions,
openPrMonitor: monitor,
advisoryAdvice: buildDecisionPackAdvisoryAdvice(scoreBlockers),
eligibilityGapRepos: buildDecisionPackEligibilityGap(repoDecisions),
};
}

Expand Down Expand Up @@ -1117,6 +1122,36 @@ function emptyRecommendationOutcomeFeedback(login: string): AgentRecommendationO
};
}

const ADVISORY_SEVERITY_TO_LEVEL: Record<ScoreBlocker["severity"], AdvisoryAdviceItem["level"]> = {
critical: "CRITICAL",
warning: "WARNING",
info: "INFO",
};
const ADVISORY_SEVERITY_ORDER: Record<ScoreBlocker["severity"], number> = { critical: 0, warning: 1, info: 2 };
const OPEN_PR_PRESSURE_THRESHOLD = 4;

function buildDecisionPackAdvisoryAdvice(blockers: ScoreBlocker[]): AdvisoryAdviceItem[] {
return [...blockers]
.sort((a, b) => (ADVISORY_SEVERITY_ORDER[a.severity] ?? 3) - (ADVISORY_SEVERITY_ORDER[b.severity] ?? 3))
.slice(0, 10)
.map((blocker) => ({
level: ADVISORY_SEVERITY_TO_LEVEL[blocker.severity] ?? "INFO",
code: blocker.code,
message: blocker.detail,
}));
}

function buildDecisionPackEligibilityGap(decisions: RepoDecision[]): EligibilityGapEntry[] {
return decisions
.map((decision) => {
const currentOpenPrCount = decision.outcome?.openPullRequests ?? 0;
const prsNeededToUnlock = Math.max(0, currentOpenPrCount - OPEN_PR_PRESSURE_THRESHOLD);
return { repoFullName: decision.repoFullName, currentOpenPrCount, openPrThreshold: OPEN_PR_PRESSURE_THRESHOLD, prsNeededToUnlock };
})
.filter((entry) => entry.prsNeededToUnlock > 0 && entry.prsNeededToUnlock <= 5)
.sort((a, b) => a.prsNeededToUnlock - b.prsNeededToUnlock || a.repoFullName.localeCompare(b.repoFullName));
}

function scoreBlockersFor(repoFullName: string, lane: string, roleContext: RoleContext, outcome: ContributorOutcomeHistory["repoOutcomes"][number] | undefined): ScoreBlocker[] {
const blockers: ScoreBlocker[] = [];
const openPullRequests = outcome?.openPullRequests ?? 0;
Expand Down Expand Up @@ -1821,4 +1856,6 @@ export const __decisionPackInternals = {
sanitizeTradeoffPublicText,
buildRepoDecisionCounterfactualReasons,
sanitizeCounterfactualPublicText,
buildDecisionPackAdvisoryAdvice,
buildDecisionPackEligibilityGap,
};
115 changes: 115 additions & 0 deletions src/signals/reward-risk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export type RepoRewardRisk = {
issueMultiplier: number;
estimatedScoreIfClean: number;
currentEstimatedScore: number;
competitionFactor: number;
freshnessFactor: number;
};
scoreBlockers: string[];
riskBreakdown: {
Expand All @@ -103,6 +105,7 @@ export type RepoRewardRisk = {
currentPreview: ScorePreviewResult;
afterCleanupPreview: ScorePreviewResult;
actions: RewardRiskAction[];
advisoryAdvice: AdvisoryAdviceItem[];
whyThisHelps: string[];
nextActions: string[];
summary: string;
Expand All @@ -118,6 +121,22 @@ export type ContributorRewardRiskStrategy = {
reasoning: string[];
actionImpact: string[];
nextActions: string[];
eligibilityGap: EligibilityGapEntry[];
};

export type AdvisoryLevel = "CRITICAL" | "WARNING" | "TIP" | "INFO";

export type AdvisoryAdviceItem = {
level: AdvisoryLevel;
code: string;
message: string;
};

export type EligibilityGapEntry = {
repoFullName: string;
currentOpenPrCount: number;
openPrThreshold: number;
prsNeededToUnlock: number;
};

export type MaintainerNoiseReport = {
Expand Down Expand Up @@ -290,6 +309,8 @@ export function buildRepoRewardRisk(args: {
issueMultiplier: currentPreview.scoreEstimate.issueMultiplier,
estimatedScoreIfClean: afterCleanupPreview.scoreEstimate.estimatedMergedScore,
currentEstimatedScore: currentPreview.scoreEstimate.estimatedMergedScore,
competitionFactor: computeCompetitionFactor(args.issues, args.pullRequests),
freshnessFactor: computeFreshnessFactor(args.issues),
},
scoreBlockers,
riskBreakdown: {
Expand All @@ -306,6 +327,16 @@ export function buildRepoRewardRisk(args: {
currentPreview,
afterCleanupPreview,
actions,
advisoryAdvice: buildAdvisoryAdvice({
lane,
roleContext,
currentPreview,
repo: args.repo,
repoOutcome,
currentOpenPrCount,
queueHealth,
collisionsHighRiskCount: collisions.summary.highRiskCount,
}),
whyThisHelps,
nextActions: nextActions.length > 0 ? nextActions : ["Gather fresher repo and contributor evidence before acting."],
summary: `${args.repoFullName}: ${scoreBlockers.length > 0 ? "blocked or cautionary" : "scoreable"} private reward/risk context; top action ${actions[0]?.actionKind ?? "none"}.`,
Expand Down Expand Up @@ -380,6 +411,7 @@ export function buildContributorRewardRiskStrategy(args: {
reasoning: [...new Set(reasoning)],
actionImpact,
nextActions: nextActions.length > 0 ? nextActions : ["Refresh official Gittensor and GitHub backfill data, then rerun strategy."],
eligibilityGap: buildEligibilityGap(repoAnalyses),
};
}

Expand Down Expand Up @@ -775,6 +807,80 @@ function maintainerNextStepsFor(action: PullRequestReviewability["action"], nois
return ["Watch for tests, checks, linked context, or duplicate-risk changes before prioritizing review."];
}

function buildAdvisoryAdvice(args: {
lane: LaneAdvice;
roleContext: RoleContext;
currentPreview: ScorePreviewResult;
repo: RepositoryRecord | null;
repoOutcome: ContributorOutcomeHistory["repoOutcomes"][number] | undefined;
currentOpenPrCount: number;
queueHealth: QueueHealth;
collisionsHighRiskCount: number;
}): AdvisoryAdviceItem[] {
const items: AdvisoryAdviceItem[] = [];
if (!args.repo?.isRegistered) items.push({ level: "CRITICAL", code: "unregistered_repo", message: "Repository is not registered in the local snapshot." });
if (args.lane.lane === "inactive") items.push({ level: "CRITICAL", code: "inactive_lane", message: "Repository allocation is inactive." });
if (args.lane.lane === "unknown") items.push({ level: "CRITICAL", code: "unknown_lane", message: "Repository lane is unknown." });
if (args.currentOpenPrCount > args.currentPreview.gates.openPrThreshold) {
items.push({ level: "CRITICAL", code: "open_pr_threshold_exceeded", message: `Open PR count (${args.currentOpenPrCount}) exceeds the scoring threshold (${args.currentPreview.gates.openPrThreshold}).` });
}
if (args.currentPreview.gates.credibilityObserved < args.currentPreview.gates.credibilityFloor) {
items.push({ level: "CRITICAL", code: "credibility_below_floor", message: `Credibility (${round(args.currentPreview.gates.credibilityObserved)}) is below the floor (${args.currentPreview.gates.credibilityFloor}).` });
}
const closedPullRequestRate = args.repoOutcome?.closedPullRequestRate ?? 0;
if (closedPullRequestRate >= 0.35) {
items.push({ level: "WARNING", code: "high_closed_pr_rate", message: `Closed PR rate of ${percent(closedPullRequestRate)} creates credibility risk.` });
}
if (args.queueHealth.level === "critical" || args.queueHealth.level === "high") {
items.push({ level: "WARNING", code: "high_queue_burden", message: `Maintainer queue burden is ${args.queueHealth.level}.` });
}
if (args.collisionsHighRiskCount > 0) {
items.push({ level: "WARNING", code: "collision_risk", message: `${args.collisionsHighRiskCount} high-risk duplicate cluster(s) increase review friction.` });
}
if (args.roleContext.maintainerLane) {
items.push({ level: "TIP", code: "maintainer_lane", message: "Maintainer-lane activity is tracked separately from outside-contributor reward evidence." });
}
if (args.lane.lane === "issue_discovery") {
items.push({ level: "TIP", code: "issue_discovery_only", message: "This repo routes reward through issue discovery; direct PR lane value is minimal." });
}
if (args.currentOpenPrCount > 0 && args.currentOpenPrCount <= args.currentPreview.gates.openPrThreshold) {
items.push({ level: "INFO", code: "open_prs_within_threshold", message: `${args.currentOpenPrCount} open PR(s) are within the scoring threshold of ${args.currentPreview.gates.openPrThreshold}.` });
}
return items;
}

function computeCompetitionFactor(issues: IssueRecord[], pullRequests: PullRequestRecord[]): number {
const openIssues = issues.filter((issue) => issue.state === "open");
if (openIssues.length === 0) return 1;
const linkedIssueNumbers = new Set(pullRequests.filter((pr) => pr.state === "open").flatMap((pr) => pr.linkedIssues));
const competedCount = openIssues.filter((issue) => linkedIssueNumbers.has(issue.number)).length;
return round(clamp(1 - competedCount / openIssues.length, 0, 1));
}

function computeFreshnessFactor(issues: IssueRecord[]): number {
const openWithDate = issues.filter((issue) => issue.state === "open" && issue.createdAt);
if (openWithDate.length === 0) return 0.5;
const nowMs = Date.now();
const ages = openWithDate
.map((issue) => (nowMs - new Date(issue.createdAt!).getTime()) / (1000 * 60 * 60 * 24))
.sort((a, b) => a - b);
/* v8 ignore next -- openWithDate is non-empty here and the median index is always valid; the ?? 0 only satisfies noUncheckedIndexedAccess. */
const medianAge = ages[Math.floor(ages.length / 2)] ?? 0;
return round(clamp(Math.exp(-medianAge / 90), 0, 1));
}

function buildEligibilityGap(repoAnalyses: RepoRewardRisk[]): EligibilityGapEntry[] {
return repoAnalyses
.map((analysis) => ({
repoFullName: analysis.repoFullName,
currentOpenPrCount: analysis.riskBreakdown.openPullRequests,
openPrThreshold: analysis.currentPreview.gates.openPrThreshold,
prsNeededToUnlock: Math.max(0, analysis.riskBreakdown.openPullRequests - analysis.currentPreview.gates.openPrThreshold),
}))
.filter((entry) => entry.prsNeededToUnlock > 0 && entry.prsNeededToUnlock <= 5)
.sort((a, b) => a.prsNeededToUnlock - b.prsNeededToUnlock || a.repoFullName.localeCompare(b.repoFullName));
}

function sameRepo(left: string, right: string): boolean {
return left.toLowerCase() === right.toLowerCase();
}
Expand Down Expand Up @@ -808,3 +914,12 @@ function round(value: number): number {
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}

export const __rewardRiskInternals = {
buildAdvisoryAdvice,
computeCompetitionFactor,
computeFreshnessFactor,
buildEligibilityGap,
round,
clamp,
};
Loading
Loading