@loopover/engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/LICENSE +661 -0
- package/README.md +676 -0
- package/dist/advisory/gate-advisory.d.ts +151 -0
- package/dist/advisory/gate-advisory.js +512 -0
- package/dist/ai-policy-map.d.ts +97 -0
- package/dist/ai-policy-map.js +377 -0
- package/dist/ams-policy-spec.d.ts +56 -0
- package/dist/ams-policy-spec.js +165 -0
- package/dist/calibration-dashboard.d.ts +23 -0
- package/dist/calibration-dashboard.js +65 -0
- package/dist/calibration-trend.d.ts +40 -0
- package/dist/calibration-trend.js +60 -0
- package/dist/contributor-fit.d.ts +24 -0
- package/dist/contributor-fit.js +50 -0
- package/dist/discovery-index-contract.d.ts +74 -0
- package/dist/discovery-index-contract.js +184 -0
- package/dist/discovery-soft-claim.d.ts +35 -0
- package/dist/discovery-soft-claim.js +50 -0
- package/dist/duplicate-winner.d.ts +57 -0
- package/dist/duplicate-winner.js +98 -0
- package/dist/feasibility.d.ts +27 -0
- package/dist/feasibility.js +86 -0
- package/dist/finding-severity-calibration.d.ts +116 -0
- package/dist/finding-severity-calibration.js +457 -0
- package/dist/fleet-run-manifest.d.ts +41 -0
- package/dist/fleet-run-manifest.js +151 -0
- package/dist/focus-manifest/guidance.d.ts +21 -0
- package/dist/focus-manifest/guidance.js +222 -0
- package/dist/focus-manifest.d.ts +826 -0
- package/dist/focus-manifest.js +2730 -0
- package/dist/gate-verdict-calibration.d.ts +111 -0
- package/dist/gate-verdict-calibration.js +411 -0
- package/dist/github/constants.d.ts +1 -0
- package/dist/github/constants.js +1 -0
- package/dist/github/sanitize-public-comment.d.ts +1 -0
- package/dist/github/sanitize-public-comment.js +26 -0
- package/dist/goal-model.d.ts +7 -0
- package/dist/goal-model.js +87 -0
- package/dist/governor/action-mode.d.ts +55 -0
- package/dist/governor/action-mode.js +74 -0
- package/dist/governor/budget-cap.d.ts +49 -0
- package/dist/governor/budget-cap.js +34 -0
- package/dist/governor/chokepoint.d.ts +58 -0
- package/dist/governor/chokepoint.js +269 -0
- package/dist/governor/kill-switch.d.ts +38 -0
- package/dist/governor/kill-switch.js +62 -0
- package/dist/governor/rate-limit.d.ts +40 -0
- package/dist/governor/rate-limit.js +63 -0
- package/dist/governor/reputation-throttle.d.ts +50 -0
- package/dist/governor/reputation-throttle.js +94 -0
- package/dist/governor/run-halt.d.ts +39 -0
- package/dist/governor/run-halt.js +83 -0
- package/dist/governor/self-plagiarism.d.ts +47 -0
- package/dist/governor/self-plagiarism.js +200 -0
- package/dist/governor/write-rate-limit.d.ts +52 -0
- package/dist/governor/write-rate-limit.js +132 -0
- package/dist/governor-ledger.d.ts +25 -0
- package/dist/governor-ledger.js +91 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +120 -0
- package/dist/issue-plan-decomposition.d.ts +18 -0
- package/dist/issue-plan-decomposition.js +56 -0
- package/dist/issue-rag-query.d.ts +12 -0
- package/dist/issue-rag-query.js +35 -0
- package/dist/local-scorer.d.ts +34 -0
- package/dist/local-scorer.js +36 -0
- package/dist/metadata-best-min-score.d.ts +9 -0
- package/dist/metadata-best-min-score.js +9 -0
- package/dist/metadata-best-pick.d.ts +9 -0
- package/dist/metadata-best-pick.js +9 -0
- package/dist/metadata-min-score.d.ts +9 -0
- package/dist/metadata-min-score.js +13 -0
- package/dist/metadata-top-min-score.d.ts +9 -0
- package/dist/metadata-top-min-score.js +13 -0
- package/dist/metadata-top-pick.d.ts +9 -0
- package/dist/metadata-top-pick.js +13 -0
- package/dist/miner/acceptance-criteria.d.ts +43 -0
- package/dist/miner/acceptance-criteria.js +77 -0
- package/dist/miner/agent-sdk-driver.d.ts +39 -0
- package/dist/miner/agent-sdk-driver.js +162 -0
- package/dist/miner/attempt-log.d.ts +29 -0
- package/dist/miner/attempt-log.js +100 -0
- package/dist/miner/attempt-metering.d.ts +39 -0
- package/dist/miner/attempt-metering.js +67 -0
- package/dist/miner/cli-subprocess-driver.d.ts +68 -0
- package/dist/miner/cli-subprocess-driver.js +272 -0
- package/dist/miner/coding-agent-driver.d.ts +33 -0
- package/dist/miner/coding-agent-driver.js +36 -0
- package/dist/miner/coding-agent-invoke.d.ts +11 -0
- package/dist/miner/coding-agent-invoke.js +80 -0
- package/dist/miner/coding-agent-mode.d.ts +25 -0
- package/dist/miner/coding-agent-mode.js +43 -0
- package/dist/miner/driver-factory.d.ts +82 -0
- package/dist/miner/driver-factory.js +169 -0
- package/dist/miner/harness-submission-trigger.d.ts +37 -0
- package/dist/miner/harness-submission-trigger.js +46 -0
- package/dist/miner/iterate-loop.d.ts +97 -0
- package/dist/miner/iterate-loop.js +278 -0
- package/dist/miner/iterate-policy.d.ts +98 -0
- package/dist/miner/iterate-policy.js +98 -0
- package/dist/miner/lint-guard.d.ts +50 -0
- package/dist/miner/lint-guard.js +68 -0
- package/dist/miner/local-write-tools.d.ts +81 -0
- package/dist/miner/local-write-tools.js +113 -0
- package/dist/miner/loop-reentry-policy.d.ts +36 -0
- package/dist/miner/loop-reentry-policy.js +53 -0
- package/dist/miner/repo-map.d.ts +51 -0
- package/dist/miner/repo-map.js +268 -0
- package/dist/miner/self-review-adapter.d.ts +106 -0
- package/dist/miner/self-review-adapter.js +83 -0
- package/dist/miner/submission-gate.d.ts +40 -0
- package/dist/miner/submission-gate.js +89 -0
- package/dist/miner/worktree-allocator.d.ts +59 -0
- package/dist/miner/worktree-allocator.js +60 -0
- package/dist/miner/worktree-pool.d.ts +55 -0
- package/dist/miner/worktree-pool.js +65 -0
- package/dist/miner-goal-lane-fit.d.ts +18 -0
- package/dist/miner-goal-lane-fit.js +76 -0
- package/dist/miner-goal-spec.d.ts +139 -0
- package/dist/miner-goal-spec.js +281 -0
- package/dist/miner-prediction-metrics.d.ts +16 -0
- package/dist/miner-prediction-metrics.js +61 -0
- package/dist/miner-telemetry.d.ts +32 -0
- package/dist/miner-telemetry.js +87 -0
- package/dist/objective-anchor.d.ts +125 -0
- package/dist/objective-anchor.js +401 -0
- package/dist/opportunity-competition.d.ts +6 -0
- package/dist/opportunity-competition.js +29 -0
- package/dist/opportunity-freshness.d.ts +18 -0
- package/dist/opportunity-freshness.js +55 -0
- package/dist/opportunity-metadata.d.ts +53 -0
- package/dist/opportunity-metadata.js +204 -0
- package/dist/opportunity-ranker.d.ts +46 -0
- package/dist/opportunity-ranker.js +74 -0
- package/dist/pairwise-calibration.d.ts +49 -0
- package/dist/pairwise-calibration.js +106 -0
- package/dist/phase7-calibration-loop.d.ts +136 -0
- package/dist/phase7-calibration-loop.js +437 -0
- package/dist/plan-blocked.d.ts +6 -0
- package/dist/plan-blocked.js +24 -0
- package/dist/plan-completed.d.ts +5 -0
- package/dist/plan-completed.js +6 -0
- package/dist/plan-completion.d.ts +6 -0
- package/dist/plan-completion.js +7 -0
- package/dist/plan-empty.d.ts +5 -0
- package/dist/plan-empty.js +6 -0
- package/dist/plan-export.d.ts +27 -0
- package/dist/plan-export.js +86 -0
- package/dist/plan-failure.d.ts +5 -0
- package/dist/plan-failure.js +6 -0
- package/dist/plan-overall-status.d.ts +6 -0
- package/dist/plan-overall-status.js +25 -0
- package/dist/plan-pending.d.ts +5 -0
- package/dist/plan-pending.js +6 -0
- package/dist/plan-progress-complete.d.ts +7 -0
- package/dist/plan-progress-complete.js +10 -0
- package/dist/plan-ready.d.ts +6 -0
- package/dist/plan-ready.js +12 -0
- package/dist/plan-running.d.ts +5 -0
- package/dist/plan-running.js +6 -0
- package/dist/plan-skipped.d.ts +5 -0
- package/dist/plan-skipped.js +6 -0
- package/dist/plan-step-count.d.ts +5 -0
- package/dist/plan-step-count.js +6 -0
- package/dist/plan-step-stats.d.ts +5 -0
- package/dist/plan-step-stats.js +6 -0
- package/dist/plan-templates.d.ts +25 -0
- package/dist/plan-templates.js +107 -0
- package/dist/plan-terminated.d.ts +6 -0
- package/dist/plan-terminated.js +9 -0
- package/dist/portfolio/non-convergence.d.ts +37 -0
- package/dist/portfolio/non-convergence.js +57 -0
- package/dist/portfolio/queue.d.ts +30 -0
- package/dist/portfolio/queue.js +150 -0
- package/dist/predicted-gate.d.ts +103 -0
- package/dist/predicted-gate.js +218 -0
- package/dist/prompt-packet.d.ts +11 -0
- package/dist/prompt-packet.js +34 -0
- package/dist/ranked-opportunity-best-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-best-min-score.js +9 -0
- package/dist/ranked-opportunity-best-pick.d.ts +8 -0
- package/dist/ranked-opportunity-best-pick.js +9 -0
- package/dist/ranked-opportunity-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-min-score.js +13 -0
- package/dist/ranked-opportunity-top-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-top-min-score.js +13 -0
- package/dist/review/advisory-ai-routing-config.d.ts +8 -0
- package/dist/review/advisory-ai-routing-config.js +40 -0
- package/dist/review/check-names.d.ts +1 -0
- package/dist/review/check-names.js +1 -0
- package/dist/review/cla-check.d.ts +49 -0
- package/dist/review/cla-check.js +71 -0
- package/dist/review/diff-file-priority.d.ts +1 -0
- package/dist/review/diff-file-priority.js +12 -0
- package/dist/review/enrichment-analyzer-names.d.ts +3 -0
- package/dist/review/enrichment-analyzer-names.js +63 -0
- package/dist/review/guardrail-config.d.ts +10 -0
- package/dist/review/guardrail-config.js +62 -0
- package/dist/review/linked-issue-hard-rules-config.d.ts +4 -0
- package/dist/review/linked-issue-hard-rules-config.js +79 -0
- package/dist/review/linked-issue-label-propagation.d.ts +9 -0
- package/dist/review/linked-issue-label-propagation.js +114 -0
- package/dist/review/pre-merge-checks.d.ts +28 -0
- package/dist/review/pre-merge-checks.js +66 -0
- package/dist/review/review-thread-findings.d.ts +1 -0
- package/dist/review/review-thread-findings.js +1 -0
- package/dist/review/safe-url.d.ts +5 -0
- package/dist/review/safe-url.js +137 -0
- package/dist/review/screenshot-table-gate.d.ts +87 -0
- package/dist/review/screenshot-table-gate.js +340 -0
- package/dist/review/unlinked-issue-guardrail-config.d.ts +9 -0
- package/dist/review/unlinked-issue-guardrail-config.js +44 -0
- package/dist/reviewer-consensus-calibration.d.ts +113 -0
- package/dist/reviewer-consensus-calibration.js +493 -0
- package/dist/reward-risk.d.ts +223 -0
- package/dist/reward-risk.js +618 -0
- package/dist/scoring/label-match.d.ts +4 -0
- package/dist/scoring/label-match.js +145 -0
- package/dist/scoring/model.d.ts +31 -0
- package/dist/scoring/model.js +157 -0
- package/dist/scoring/pending-pr-scenarios.d.ts +44 -0
- package/dist/scoring/pending-pr-scenarios.js +145 -0
- package/dist/scoring/preview.d.ts +246 -0
- package/dist/scoring/preview.js +1108 -0
- package/dist/scoring/types.d.ts +131 -0
- package/dist/scoring/types.js +7 -0
- package/dist/settings/auto-close-exempt.d.ts +10 -0
- package/dist/settings/auto-close-exempt.js +58 -0
- package/dist/settings/autonomy.d.ts +33 -0
- package/dist/settings/autonomy.js +80 -0
- package/dist/settings/command-authorization.d.ts +49 -0
- package/dist/settings/command-authorization.js +222 -0
- package/dist/settings/contributor-blacklist.d.ts +17 -0
- package/dist/settings/contributor-blacklist.js +86 -0
- package/dist/settings/moderation-rules.d.ts +51 -0
- package/dist/settings/moderation-rules.js +104 -0
- package/dist/settings/pr-type-label.d.ts +70 -0
- package/dist/settings/pr-type-label.js +149 -0
- package/dist/signals/change-guardrail.d.ts +49 -0
- package/dist/signals/change-guardrail.js +164 -0
- package/dist/signals/check-summary.d.ts +9 -0
- package/dist/signals/check-summary.js +12 -0
- package/dist/signals/contributor-calibration.d.ts +23 -0
- package/dist/signals/contributor-calibration.js +47 -0
- package/dist/signals/duplicate-winner.d.ts +9 -0
- package/dist/signals/duplicate-winner.js +9 -0
- package/dist/signals/path-matchers.d.ts +36 -0
- package/dist/signals/path-matchers.js +371 -0
- package/dist/signals/predicted-gate-engine.d.ts +62 -0
- package/dist/signals/predicted-gate-engine.js +895 -0
- package/dist/signals/preflight-limits.d.ts +14 -0
- package/dist/signals/preflight-limits.js +14 -0
- package/dist/signals/slop.d.ts +56 -0
- package/dist/signals/slop.js +324 -0
- package/dist/signals/test-evidence.d.ts +25 -0
- package/dist/signals/test-evidence.js +108 -0
- package/dist/subprocess-env.d.ts +23 -0
- package/dist/subprocess-env.js +74 -0
- package/dist/track-record-summary.d.ts +92 -0
- package/dist/track-record-summary.js +301 -0
- package/dist/types/manifest-deps-types.d.ts +514 -0
- package/dist/types/manifest-deps-types.js +4 -0
- package/dist/types/predicted-gate-types.d.ts +338 -0
- package/dist/types/predicted-gate-types.js +3 -0
- package/dist/types/reward-risk-types.d.ts +319 -0
- package/dist/types/reward-risk-types.js +11 -0
- package/dist/utils/json.d.ts +1 -0
- package/dist/utils/json.js +3 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +80 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import { buildScorePreview } from "./scoring/preview.js";
|
|
2
|
+
import { isFailingCheckSummary } from "./signals/check-summary.js";
|
|
3
|
+
import { nowIso } from "./utils/json.js";
|
|
4
|
+
const ACTION_RANK = {
|
|
5
|
+
cleanup_existing_prs: 0,
|
|
6
|
+
land_existing_prs: 1,
|
|
7
|
+
close_or_withdraw_low_fit_prs: 2,
|
|
8
|
+
open_new_direct_pr: 3,
|
|
9
|
+
file_issue_discovery: 4,
|
|
10
|
+
maintainer_lane_improve_repo: 5,
|
|
11
|
+
maintainer_cut_readiness: 6,
|
|
12
|
+
};
|
|
13
|
+
export function buildRepoRewardRisk(args, deps) {
|
|
14
|
+
const roleContext = deps.buildRoleContext({
|
|
15
|
+
login: args.login,
|
|
16
|
+
repo: args.repo,
|
|
17
|
+
repoFullName: args.repoFullName,
|
|
18
|
+
pullRequests: args.pullRequests,
|
|
19
|
+
issues: args.issues,
|
|
20
|
+
profile: args.profile,
|
|
21
|
+
});
|
|
22
|
+
const lane = deps.buildLaneAdvice(args.repo, args.repoFullName);
|
|
23
|
+
const repoOutcome = args.outcomeHistory.repoOutcomes.find((outcome) => sameRepo(outcome.repoFullName, args.repoFullName));
|
|
24
|
+
const collisions = deps.buildCollisionReport(args.repoFullName, args.issues, args.pullRequests, args.recentMergedPullRequests ?? []);
|
|
25
|
+
const queueHealth = deps.buildQueueHealth(args.repo, args.issues, args.pullRequests, collisions);
|
|
26
|
+
const recommendation = deps.buildRepoFitRecommendation({
|
|
27
|
+
login: args.login,
|
|
28
|
+
repo: args.repo,
|
|
29
|
+
repoFullName: args.repoFullName,
|
|
30
|
+
profile: args.profile,
|
|
31
|
+
outcomeHistory: args.outcomeHistory,
|
|
32
|
+
issues: args.issues,
|
|
33
|
+
pullRequests: args.pullRequests,
|
|
34
|
+
}).recommendation;
|
|
35
|
+
const labels = bestFitLabels(args.repo);
|
|
36
|
+
const competitionFactor = opportunityCompetitionFactor(collisions.summary.highRiskCount, queueHealth.signals.openPullRequests);
|
|
37
|
+
const freshnessFactor = opportunityFreshnessFactor(args.issues);
|
|
38
|
+
const currentOpenPrCount = nonNegative(args.outcomeHistory.totals.openPullRequests);
|
|
39
|
+
const currentOpenIssueCount = nonNegative(repoOutcome?.openIssues ?? args.outcomeHistory.totals.openIssues);
|
|
40
|
+
/* v8 ignore next -- Credibility fallback order protects sparse private snapshots; behavior is covered through scoring profile tests. */
|
|
41
|
+
const credibility = repoOutcome?.credibility && repoOutcome.credibility > 0 ? repoOutcome.credibility : args.scoringProfile?.evidence.credibilityAssumption ?? args.outcomeHistory.totals.credibility ?? 0.8;
|
|
42
|
+
const commonPreviewInput = {
|
|
43
|
+
repoFullName: args.repoFullName,
|
|
44
|
+
targetType: "planned_pr",
|
|
45
|
+
targetKey: `${args.login}:${args.repoFullName}:reward-risk`,
|
|
46
|
+
contributorLogin: args.login,
|
|
47
|
+
labels,
|
|
48
|
+
linkedIssueMode: lane.lane === "issue_discovery" ? "none" : "standard",
|
|
49
|
+
sourceTokenScore: estimatedSourceTokenScore(repoOutcome),
|
|
50
|
+
totalTokenScore: estimatedTotalTokenScore(repoOutcome),
|
|
51
|
+
sourceLines: estimatedSourceLines(repoOutcome),
|
|
52
|
+
existingContributorTokenScore: 0,
|
|
53
|
+
credibility,
|
|
54
|
+
metadataOnly: true,
|
|
55
|
+
duplicateRiskCount: collisions.summary.highRiskCount,
|
|
56
|
+
openIssueCount: currentOpenIssueCount,
|
|
57
|
+
};
|
|
58
|
+
const currentPreview = buildScorePreview({
|
|
59
|
+
input: { ...commonPreviewInput, openPrCount: currentOpenPrCount },
|
|
60
|
+
repo: args.repo,
|
|
61
|
+
snapshot: args.scoringSnapshot,
|
|
62
|
+
});
|
|
63
|
+
const cleanupOpenPrCount = Math.min(currentOpenPrCount, currentPreview.gates.openPrThreshold);
|
|
64
|
+
const afterCleanupPreview = buildScorePreview({
|
|
65
|
+
input: { ...commonPreviewInput, openPrCount: cleanupOpenPrCount },
|
|
66
|
+
repo: args.repo,
|
|
67
|
+
snapshot: args.scoringSnapshot,
|
|
68
|
+
});
|
|
69
|
+
const relevantLane = relevantLaneFor(lane, roleContext);
|
|
70
|
+
const laneValueScore = laneValue(lane, currentPreview, relevantLane);
|
|
71
|
+
const personalFitScore = personalFit(repoOutcome, args.scoringProfile, roleContext, args.profile, args.repoLanguage ?? null);
|
|
72
|
+
const riskPenalty = riskScore(repoOutcome, queueHealth, collisions.summary.clusterCount, collisions.summary.highRiskCount, currentOpenPrCount, currentPreview.gates.openPrThreshold);
|
|
73
|
+
const maintainerFrictionPenalty = maintainerFriction(queueHealth, collisions.summary.clusterCount, args.pullRequests);
|
|
74
|
+
const scoreBlockers = scoreBlockersFor({
|
|
75
|
+
lane,
|
|
76
|
+
roleContext,
|
|
77
|
+
currentPreview,
|
|
78
|
+
repo: args.repo,
|
|
79
|
+
repoOutcome,
|
|
80
|
+
currentOpenPrCount,
|
|
81
|
+
});
|
|
82
|
+
const scoreabilityScore = scoreBlockers.length > 0 ? 0 : clamp((currentPreview.scoreEstimate.estimatedMergedScore / 50) * 100, 0, 100);
|
|
83
|
+
const actionLeverageScore = cleanupOpenPrCount < currentOpenPrCount ? clamp((currentOpenPrCount - cleanupOpenPrCount) * 18, 30, 100) : 0;
|
|
84
|
+
const baseActionInput = {
|
|
85
|
+
repoFullName: args.repoFullName,
|
|
86
|
+
laneValueScore,
|
|
87
|
+
scoreabilityScore,
|
|
88
|
+
personalFitScore,
|
|
89
|
+
riskPenalty,
|
|
90
|
+
maintainerFrictionPenalty,
|
|
91
|
+
actionLeverageScore,
|
|
92
|
+
};
|
|
93
|
+
const cleanupNeeded = Math.max(0, currentOpenPrCount - currentPreview.gates.openPrThreshold);
|
|
94
|
+
const actions = buildActions({
|
|
95
|
+
...baseActionInput,
|
|
96
|
+
lane,
|
|
97
|
+
roleContext,
|
|
98
|
+
repoOutcome,
|
|
99
|
+
currentPreview,
|
|
100
|
+
afterCleanupPreview,
|
|
101
|
+
cleanupNeeded,
|
|
102
|
+
scoreBlockers,
|
|
103
|
+
queueHealth,
|
|
104
|
+
collisionsHighRiskCount: collisions.summary.highRiskCount,
|
|
105
|
+
});
|
|
106
|
+
const actionImpact = {
|
|
107
|
+
currentOpenPrCount,
|
|
108
|
+
openPrThreshold: currentPreview.gates.openPrThreshold,
|
|
109
|
+
openPrMultiplierDelta: `${currentPreview.scoreEstimate.openPrMultiplier} -> ${afterCleanupPreview.scoreEstimate.openPrMultiplier}`,
|
|
110
|
+
estimatedScoreDelta: `${currentPreview.scoreEstimate.estimatedMergedScore} -> ${afterCleanupPreview.scoreEstimate.estimatedMergedScore}`,
|
|
111
|
+
cleanupNeeded,
|
|
112
|
+
explanation: cleanupNeeded > 0
|
|
113
|
+
? `Landing, closing, or withdrawing ${cleanupNeeded} open PR(s) moves the current open-PR gate from blocked toward scoreable future work.`
|
|
114
|
+
: "Open PR pressure is not the primary scoreability blocker for this repo right now.",
|
|
115
|
+
};
|
|
116
|
+
const whyThisHelps = whyThisHelpsFor({
|
|
117
|
+
repoFullName: args.repoFullName,
|
|
118
|
+
lane,
|
|
119
|
+
roleContext,
|
|
120
|
+
repoOutcome,
|
|
121
|
+
currentPreview,
|
|
122
|
+
afterCleanupPreview,
|
|
123
|
+
cleanupNeeded,
|
|
124
|
+
scoreBlockers,
|
|
125
|
+
queueHealth,
|
|
126
|
+
collisionsHighRiskCount: collisions.summary.highRiskCount,
|
|
127
|
+
});
|
|
128
|
+
const nextActions = [...new Set(actions.flatMap((action) => action.nextActions))].slice(0, 8);
|
|
129
|
+
return {
|
|
130
|
+
login: args.login,
|
|
131
|
+
repoFullName: args.repoFullName,
|
|
132
|
+
generatedAt: nowIso(),
|
|
133
|
+
roleContext,
|
|
134
|
+
lane,
|
|
135
|
+
recommendation,
|
|
136
|
+
rewardUpside: {
|
|
137
|
+
relevantLane,
|
|
138
|
+
repoSlice: currentPreview.laneMath.repoSlice,
|
|
139
|
+
directPrSlice: currentPreview.laneMath.directPrSlice,
|
|
140
|
+
issueDiscoverySlice: currentPreview.laneMath.issueDiscoverySlice,
|
|
141
|
+
maintainerCutSlice: round((args.repo?.registryConfig?.maintainerCut ?? 0) * currentPreview.laneMath.repoSlice),
|
|
142
|
+
labelMultiplier: currentPreview.scoreEstimate.labelMultiplier,
|
|
143
|
+
issueMultiplier: currentPreview.scoreEstimate.issueMultiplier,
|
|
144
|
+
estimatedScoreIfClean: afterCleanupPreview.scoreEstimate.estimatedMergedScore,
|
|
145
|
+
currentEstimatedScore: currentPreview.scoreEstimate.estimatedMergedScore,
|
|
146
|
+
opportunityFactors: { competitionFactor, freshnessFactor },
|
|
147
|
+
},
|
|
148
|
+
scoreBlockers,
|
|
149
|
+
riskBreakdown: {
|
|
150
|
+
queueBurden: queueHealth.level,
|
|
151
|
+
queueBurdenScore: queueHealth.burdenScore,
|
|
152
|
+
duplicateClusters: collisions.summary.clusterCount,
|
|
153
|
+
highRiskDuplicateClusters: collisions.summary.highRiskCount,
|
|
154
|
+
closedPullRequestRate: repoOutcome?.closedPullRequestRate ?? args.outcomeHistory.totals.closedPullRequestRate,
|
|
155
|
+
openPullRequests: currentOpenPrCount,
|
|
156
|
+
credibility,
|
|
157
|
+
reviewChurnRisk: reviewChurnRisk(repoOutcome, queueHealth, collisions.summary.highRiskCount),
|
|
158
|
+
},
|
|
159
|
+
actionImpact,
|
|
160
|
+
currentPreview,
|
|
161
|
+
afterCleanupPreview,
|
|
162
|
+
actions,
|
|
163
|
+
whyThisHelps,
|
|
164
|
+
nextActions: nextActions.length > 0 ? nextActions : ["Gather fresher repo and contributor evidence before acting."],
|
|
165
|
+
summary: `${args.repoFullName}: ${scoreBlockers.length > 0 ? "blocked or cautionary" : "scoreable"} private reward/risk context; top action ${actions[0]?.actionKind ?? "none"}.`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export function buildContributorRewardRiskStrategy(args, deps) {
|
|
169
|
+
const registeredRepoNames = new Map(args.repositories.filter((repo) => repo.isRegistered).map((repo) => [repo.fullName.toLowerCase(), repo.fullName]));
|
|
170
|
+
const candidateRepoNames = uniqueRegisteredRepoNames([
|
|
171
|
+
...args.fit.opportunities.map((opportunity) => opportunity.repoFullName),
|
|
172
|
+
...args.outcomeHistory.repoOutcomes.filter((outcome) => registeredRepoNames.has(outcome.repoFullName.toLowerCase())).map((outcome) => outcome.repoFullName),
|
|
173
|
+
...args.repositories.filter((repo) => repo.isRegistered).map((repo) => repo.fullName),
|
|
174
|
+
], registeredRepoNames);
|
|
175
|
+
const issuesByRepo = groupByRepo(args.allIssues);
|
|
176
|
+
const pullRequestsByRepo = groupByRepo(args.allPullRequests);
|
|
177
|
+
const recentMergedPullRequestsByRepo = groupByRepo(args.recentMergedPullRequests ?? []);
|
|
178
|
+
const repoAnalyses = candidateRepoNames
|
|
179
|
+
.map((repoFullName) => {
|
|
180
|
+
/* v8 ignore next -- Strategy inputs usually originate from repository records; null protects stale fit snapshots. */
|
|
181
|
+
const repo = args.repositories.find((candidate) => sameRepo(candidate.fullName, repoFullName)) ?? null;
|
|
182
|
+
const repoKey = repoFullName.toLowerCase();
|
|
183
|
+
return buildRepoRewardRisk({
|
|
184
|
+
login: args.login,
|
|
185
|
+
repo,
|
|
186
|
+
repoFullName,
|
|
187
|
+
profile: args.fit.profile,
|
|
188
|
+
outcomeHistory: args.outcomeHistory,
|
|
189
|
+
scoringSnapshot: args.scoringSnapshot,
|
|
190
|
+
scoringProfile: args.scoringProfile,
|
|
191
|
+
issues: issuesByRepo.get(repoKey) ?? [],
|
|
192
|
+
pullRequests: pullRequestsByRepo.get(repoKey) ?? [],
|
|
193
|
+
recentMergedPullRequests: recentMergedPullRequestsByRepo.get(repoKey) ?? [],
|
|
194
|
+
repoLanguage: args.fit.languageFit.find((entry) => sameRepo(entry.repoFullName, repoFullName))?.language ?? null,
|
|
195
|
+
}, deps);
|
|
196
|
+
})
|
|
197
|
+
/* v8 ignore next -- Locale tie ordering is deterministic presentation fallback after ranked analysis scores. */
|
|
198
|
+
.sort((left, right) => analysisRank(right) - analysisRank(left) || left.repoFullName.localeCompare(right.repoFullName))
|
|
199
|
+
.slice(0, 20);
|
|
200
|
+
const topActions = repoAnalyses
|
|
201
|
+
.flatMap((analysis) => analysis.actions)
|
|
202
|
+
/* v8 ignore next -- Secondary sort keys make ties deterministic; priority ordering is covered by strategy tests. */
|
|
203
|
+
.sort((left, right) => right.priorityScore - left.priorityScore || ACTION_RANK[left.actionKind] - ACTION_RANK[right.actionKind] || left.repoFullName.localeCompare(right.repoFullName))
|
|
204
|
+
.slice(0, 12);
|
|
205
|
+
const reasoning = [
|
|
206
|
+
...topActions.slice(0, 5).flatMap((action) => action.whyThisHelps.map((reason) => `${action.repoFullName}: ${reason}`)),
|
|
207
|
+
...repoAnalyses
|
|
208
|
+
.filter((analysis) => analysis.roleContext.maintainerLane)
|
|
209
|
+
.slice(0, 4)
|
|
210
|
+
.map((analysis) => `${analysis.repoFullName}: maintainer-lane economics are separate from normal contributor rewards.`),
|
|
211
|
+
];
|
|
212
|
+
const actionImpact = repoAnalyses
|
|
213
|
+
.filter((analysis) => analysis.actionImpact.cleanupNeeded > 0 || analysis.currentPreview.scoreEstimate.estimatedMergedScore !== analysis.afterCleanupPreview.scoreEstimate.estimatedMergedScore)
|
|
214
|
+
.slice(0, 8)
|
|
215
|
+
.map((analysis) => `${analysis.repoFullName}: ${analysis.actionImpact.explanation} Score preview ${analysis.actionImpact.estimatedScoreDelta}; openPrMultiplier ${analysis.actionImpact.openPrMultiplierDelta}.`);
|
|
216
|
+
const nextActions = [...new Set(topActions.flatMap((action) => action.nextActions))].slice(0, 10);
|
|
217
|
+
const eligibilityGap = buildEligibilityGap(repoAnalyses);
|
|
218
|
+
return {
|
|
219
|
+
login: args.login,
|
|
220
|
+
generatedAt: nowIso(),
|
|
221
|
+
scoringModelSnapshotId: args.scoringSnapshot.id,
|
|
222
|
+
summary: `${args.login} has ${topActions.length} ranked reward/risk action(s) from ${repoAnalyses.length} repo analysis record(s).`,
|
|
223
|
+
topActions,
|
|
224
|
+
repoAnalyses,
|
|
225
|
+
reasoning: [...new Set(reasoning)],
|
|
226
|
+
actionImpact,
|
|
227
|
+
nextActions: nextActions.length > 0 ? nextActions : ["Refresh official Gittensor and GitHub backfill data, then rerun strategy."],
|
|
228
|
+
eligibilityGap,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
export function buildMaintainerNoiseReport(repo, issues, pullRequests, recentMergedPullRequests, fullName, deps) {
|
|
232
|
+
const collisions = deps.buildCollisionReport(fullName, issues, pullRequests, recentMergedPullRequests);
|
|
233
|
+
const queueHealth = deps.buildQueueHealth(repo, issues, pullRequests, collisions);
|
|
234
|
+
const intake = deps.buildContributorIntakeHealth(repo, issues, pullRequests, fullName, collisions);
|
|
235
|
+
const unlinked = pullRequests.filter((pr) => pr.state === "open" && pr.linkedIssues.length === 0).length;
|
|
236
|
+
// Only OPEN PRs are live maintainer-queue noise. Without the state guard (which the sibling `unlinked`
|
|
237
|
+
// count above already applies), already-merged/closed PRs with common churn titles ("refactor", "cleanup",
|
|
238
|
+
// "various", …) are miscounted as active noise, inflating noiseSources and depressing the score.
|
|
239
|
+
const broadDiffSignals = pullRequests.filter((pr) => pr.state === "open" && (pr.title.length > 120 || /refactor|cleanup|misc|various/i.test(pr.title))).length;
|
|
240
|
+
const noiseSources = [
|
|
241
|
+
...(unlinked > 0 ? [`${unlinked} open PR(s) lack linked issue context.`] : []),
|
|
242
|
+
...(collisions.summary.highRiskCount > 0 ? [`${collisions.summary.highRiskCount} high-risk duplicate/WIP cluster(s).`] : []),
|
|
243
|
+
...(queueHealth.signals.stalePullRequests > 0 ? [`${queueHealth.signals.stalePullRequests} stale PR(s) add queue drag.`] : []),
|
|
244
|
+
...(broadDiffSignals > 0 ? [`${broadDiffSignals} PR(s) look broad or hard to triage from title metadata.`] : []),
|
|
245
|
+
...(intake.level === "strained" || intake.level === "blocked" ? [`Contributor intake is ${intake.level}.`] : []),
|
|
246
|
+
];
|
|
247
|
+
const score = clamp(100 - queueHealth.burdenScore * 0.55 - collisions.summary.highRiskCount * 12 - unlinked * 6 - broadDiffSignals * 4, 0, 100);
|
|
248
|
+
const level = score < 25 ? "critical" : score < 50 ? "high" : score < 75 ? "medium" : "low";
|
|
249
|
+
const maintainerActions = [
|
|
250
|
+
...(collisions.summary.highRiskCount > 0 ? ["likely_duplicate"] : []),
|
|
251
|
+
...(unlinked > 0 || queueHealth.signals.stalePullRequests > 0 ? ["needs_author"] : []),
|
|
252
|
+
...(queueHealth.signals.likelyReviewablePullRequests > 0 ? ["review_now"] : []),
|
|
253
|
+
...(noiseSources.length === 0 ? ["watch"] : []),
|
|
254
|
+
];
|
|
255
|
+
return {
|
|
256
|
+
repoFullName: fullName,
|
|
257
|
+
generatedAt: nowIso(),
|
|
258
|
+
score: round(score),
|
|
259
|
+
level,
|
|
260
|
+
noiseSources: noiseSources.length > 0 ? noiseSources : ["No major maintainer-noise source detected in cached metadata."],
|
|
261
|
+
maintainerActions: [...new Set(maintainerActions)],
|
|
262
|
+
queueHealth,
|
|
263
|
+
summary: `${fullName} maintainer noise is ${level}; queue ${queueHealth.level}, ${collisions.summary.highRiskCount} high-risk collision cluster(s), ${unlinked} unlinked open PR(s).`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
export function buildPullRequestReviewability(args, deps) {
|
|
267
|
+
const intelligence = deps.buildPullRequestReviewIntelligence(args);
|
|
268
|
+
const pr = args.pullRequest;
|
|
269
|
+
const failingChecks = args.checks.filter(isFailingCheckSummary).length;
|
|
270
|
+
const broadDiff = intelligence.changeSummary.fileCount >= 12 || intelligence.changeSummary.additions + intelligence.changeSummary.deletions >= 800;
|
|
271
|
+
const noiseSources = [
|
|
272
|
+
...(pr?.state && pr.state !== "open" ? [`PR is ${pr.state}.`] : []),
|
|
273
|
+
...(intelligence.reviewSignals.linkedIssues.length === 0 ? ["Missing linked issue or no-issue rationale."] : []),
|
|
274
|
+
...(intelligence.reviewSignals.collisionClusters > 0 ? [`${intelligence.reviewSignals.collisionClusters} duplicate/WIP collision cluster(s).`] : []),
|
|
275
|
+
...(intelligence.changeSummary.codeFileCount > 0 && intelligence.changeSummary.testFileCount === 0 ? ["Code changes do not include cached test files."] : []),
|
|
276
|
+
...(failingChecks > 0 ? [`${failingChecks} failing or cancelled check(s).`] : []),
|
|
277
|
+
...(broadDiff ? ["Diff is broad enough to create avoidable review friction."] : []),
|
|
278
|
+
...(intelligence.outcomeContext && !intelligence.roleContext.maintainerLane && intelligence.outcomeContext.closedPullRequestRate >= 0.35
|
|
279
|
+
? [`Contributor repo-specific closed PR rate is ${percent(intelligence.outcomeContext.closedPullRequestRate)}.`]
|
|
280
|
+
: []),
|
|
281
|
+
];
|
|
282
|
+
const score = clamp(100 -
|
|
283
|
+
noiseSources.length * 14 -
|
|
284
|
+
intelligence.reviewSignals.collisionClusters * 12 -
|
|
285
|
+
failingChecks * 18 -
|
|
286
|
+
(broadDiff ? 18 : 0) +
|
|
287
|
+
(intelligence.reviewSignals.approvalCount > 0 ? 12 : 0), 0, 100);
|
|
288
|
+
const action = intelligence.roleContext.maintainerLane
|
|
289
|
+
? "maintainer_lane"
|
|
290
|
+
: pr?.state && pr.state !== "open"
|
|
291
|
+
? "close_or_redirect"
|
|
292
|
+
: intelligence.reviewSignals.collisionClusters > 0
|
|
293
|
+
? "likely_duplicate"
|
|
294
|
+
: score >= 75
|
|
295
|
+
? "review_now"
|
|
296
|
+
: score >= 45
|
|
297
|
+
? "needs_author"
|
|
298
|
+
: "watch";
|
|
299
|
+
const whyThisHelps = [
|
|
300
|
+
...(action === "review_now" ? ["Reviewing now is efficient because cached signals show linked context and manageable friction."] : []),
|
|
301
|
+
...(action === "needs_author" ? ["Asking for author cleanup first reduces maintainer review time before deep technical review."] : []),
|
|
302
|
+
...(action === "likely_duplicate" ? ["Checking overlap first prevents maintainers from reviewing duplicate or soon-obsolete work."] : []),
|
|
303
|
+
...(action === "maintainer_lane" ? ["Maintainer-authored work should be reviewed as repo stewardship, not outside-contributor triage."] : []),
|
|
304
|
+
...(action === "close_or_redirect" ? ["Closed or non-open PRs should be redirected before consuming review time."] : []),
|
|
305
|
+
...(action === "watch" ? ["Watching is lower-cost until checks, tests, issue links, or overlap signals improve."] : []),
|
|
306
|
+
];
|
|
307
|
+
return {
|
|
308
|
+
repoFullName: args.repoFullName,
|
|
309
|
+
pullNumber: args.pullNumber,
|
|
310
|
+
generatedAt: nowIso(),
|
|
311
|
+
score: round(score),
|
|
312
|
+
action,
|
|
313
|
+
noiseSources: noiseSources.length > 0 ? noiseSources : ["No major reviewability blocker detected in cached metadata."],
|
|
314
|
+
whyThisHelps,
|
|
315
|
+
maintainerNextSteps: maintainerNextStepsFor(action, noiseSources),
|
|
316
|
+
privateSummary: `Reviewability ${round(score)}/100; action ${action}; ${noiseSources.length} noise source(s) from cached metadata.`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function buildActions(args) {
|
|
320
|
+
const actions = [];
|
|
321
|
+
const openRepoPrs = args.repoOutcome?.openPullRequests ?? 0;
|
|
322
|
+
const hasBlockers = args.scoreBlockers.length > 0;
|
|
323
|
+
if (args.roleContext.maintainerLane) {
|
|
324
|
+
actions.push(action("maintainer_lane_improve_repo", args, 55 + (100 - args.maintainerFrictionPenalty) * 0.25, [
|
|
325
|
+
"Improves the repo's contributor intake, label/config quality, and review flow instead of treating owner work as normal contributor evidence.",
|
|
326
|
+
], "info"), action("maintainer_cut_readiness", args, 45 + (args.queueHealth.level === "low" ? 20 : 0), [
|
|
327
|
+
"Checks whether maintainer-lane economics are configured clearly enough for repo owners without inflating outside-contributor history.",
|
|
328
|
+
], "info"));
|
|
329
|
+
}
|
|
330
|
+
if (!args.roleContext.maintainerLane && openRepoPrs > 0) {
|
|
331
|
+
actions.push(action("cleanup_existing_prs", args, 30 + args.actionLeverageScore * 0.55 + args.personalFitScore * 0.22 + args.laneValueScore * 0.12 - args.maintainerFrictionPenalty * 0.04, [
|
|
332
|
+
args.cleanupNeeded > 0
|
|
333
|
+
? `Reduces open PR pressure; current openPrMultiplier ${args.currentPreview.scoreEstimate.openPrMultiplier} can move toward ${args.afterCleanupPreview.scoreEstimate.openPrMultiplier}.`
|
|
334
|
+
: "Keeps repo-specific queue pressure lower before adding more work.",
|
|
335
|
+
], args.cleanupNeeded > 0 ? "warning" : "info"));
|
|
336
|
+
if (args.lane.lane !== "issue_discovery") {
|
|
337
|
+
actions.push(action("land_existing_prs", args, 25 + args.personalFitScore * 0.28 + args.laneValueScore * 0.18 + args.actionLeverageScore * 0.35 - args.riskPenalty * 0.08, [
|
|
338
|
+
"Landing already-open work preserves successful repo-specific evidence and avoids adding new maintainer load.",
|
|
339
|
+
], "tip"));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!args.roleContext.maintainerLane && openRepoPrs > 0 && (hasBlockers || args.riskPenalty >= 55)) {
|
|
343
|
+
actions.push(action("close_or_withdraw_low_fit_prs", args, 20 + args.actionLeverageScore * 0.35 + args.riskPenalty * 0.08, [
|
|
344
|
+
"Withdrawing stale or low-fit work can reduce collateral pressure faster than opening new submissions.",
|
|
345
|
+
], "warning"));
|
|
346
|
+
}
|
|
347
|
+
if (!args.roleContext.maintainerLane && (args.lane.lane === "direct_pr" || args.lane.lane === "split")) {
|
|
348
|
+
actions.push(action("open_new_direct_pr", args, 18 + args.laneValueScore * 0.22 + args.scoreabilityScore * 0.3 + args.personalFitScore * 0.25 - args.riskPenalty * 0.18 - args.maintainerFrictionPenalty * 0.08, hasBlockers
|
|
349
|
+
? ["New PR expected value is low until hard scoreability blockers and maintainer-friction signals are cleared."]
|
|
350
|
+
: ["A tightly scoped, linked, tested direct PR has scoreability and maintainer-fit upside in this lane."], hasBlockers ? "critical" : "tip"));
|
|
351
|
+
}
|
|
352
|
+
if (!args.roleContext.maintainerLane && (args.lane.lane === "issue_discovery" || args.lane.lane === "split")) {
|
|
353
|
+
actions.push(action("file_issue_discovery", args, 18 + args.laneValueScore * 0.28 + (args.lane.lane === "issue_discovery" ? 20 : 0) - args.riskPenalty * 0.16, [
|
|
354
|
+
args.lane.lane === "issue_discovery"
|
|
355
|
+
? "This repo routes value through issue discovery; direct PR-side work has little or no lane value under current config."
|
|
356
|
+
: "Issue discovery can be viable only for high-proof reports that someone else can solve.",
|
|
357
|
+
], "tip"));
|
|
358
|
+
}
|
|
359
|
+
const ranked = actions.map((candidate) => ({ ...candidate, priorityScore: round(clamp(candidate.priorityScore, 0, 100)) }));
|
|
360
|
+
/* v8 ignore start -- secondary action rank is a deterministic presentation tie-break */
|
|
361
|
+
return ranked.sort((left, right) => right.priorityScore - left.priorityScore || ACTION_RANK[left.actionKind] - ACTION_RANK[right.actionKind]);
|
|
362
|
+
/* v8 ignore stop */
|
|
363
|
+
}
|
|
364
|
+
function action(kind, args, priorityScore, whyThisHelps, severity) {
|
|
365
|
+
return {
|
|
366
|
+
actionKind: kind,
|
|
367
|
+
repoFullName: args.repoFullName,
|
|
368
|
+
severity,
|
|
369
|
+
priorityScore,
|
|
370
|
+
laneValueScore: round(args.laneValueScore),
|
|
371
|
+
scoreabilityScore: round(args.scoreabilityScore),
|
|
372
|
+
personalFitScore: round(args.personalFitScore),
|
|
373
|
+
riskPenalty: round(args.riskPenalty),
|
|
374
|
+
maintainerFrictionPenalty: round(args.maintainerFrictionPenalty),
|
|
375
|
+
actionLeverageScore: round(args.actionLeverageScore),
|
|
376
|
+
whyThisHelps,
|
|
377
|
+
nextActions: nextActionsFor(kind),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function scoreBlockersFor(args) {
|
|
381
|
+
return [
|
|
382
|
+
...(!args.repo?.isRegistered ? ["Repository is not registered in the local snapshot."] : []),
|
|
383
|
+
...(args.lane.lane === "inactive" ? ["Repository allocation is inactive."] : []),
|
|
384
|
+
...(args.lane.lane === "unknown" ? ["Repository lane is unknown."] : []),
|
|
385
|
+
...(args.roleContext.maintainerLane ? ["Maintainer-lane work is not normal outside-contributor reward evidence."] : []),
|
|
386
|
+
...(args.currentPreview.laneMath.directPrSlice <= 0 && args.lane.lane === "issue_discovery" ? ["Direct PR-side lane value is disabled for this repo."] : []),
|
|
387
|
+
...(args.currentOpenPrCount > args.currentPreview.gates.openPrThreshold ? ["Open PR count exceeds the current threshold assumption."] : []),
|
|
388
|
+
...(args.currentPreview.gates.credibilityObserved < args.currentPreview.gates.credibilityFloor ? ["Credibility assumption is below the current floor."] : []),
|
|
389
|
+
...((args.repoOutcome?.closedPullRequestRate ?? 0) >= 0.35 ? ["Repo-specific closed PR rate is high enough to create credibility risk."] : []),
|
|
390
|
+
];
|
|
391
|
+
}
|
|
392
|
+
function whyThisHelpsFor(args) {
|
|
393
|
+
return [
|
|
394
|
+
...(args.cleanupNeeded > 0
|
|
395
|
+
? [`Cleanup is high leverage because it changes openPrMultiplier ${args.currentPreview.scoreEstimate.openPrMultiplier} -> ${args.afterCleanupPreview.scoreEstimate.openPrMultiplier} and estimated score ${args.currentPreview.scoreEstimate.estimatedMergedScore} -> ${args.afterCleanupPreview.scoreEstimate.estimatedMergedScore}.`]
|
|
396
|
+
: []),
|
|
397
|
+
...(args.repoOutcome && args.repoOutcome.mergedPullRequests > 0
|
|
398
|
+
? [`Protects repo-specific credibility where ${args.repoOutcome.mergedPullRequests} merged PR(s) already show fit.`]
|
|
399
|
+
: []),
|
|
400
|
+
...(args.roleContext.maintainerLane
|
|
401
|
+
? [`${args.repoFullName} is maintainer lane for this user, so repo-health and maintainer_cut readiness matter more than normal contributor submissions.`]
|
|
402
|
+
: []),
|
|
403
|
+
...(args.lane.lane === "issue_discovery" ? ["Direct PRs have no PR-side lane value here; issue-discovery quality and closure risk dominate."] : []),
|
|
404
|
+
...(args.scoreBlockers.length > 0 ? [`Hard blockers: ${args.scoreBlockers.join(" ")}`] : []),
|
|
405
|
+
...(args.queueHealth.level === "high" || args.queueHealth.level === "critical" ? [`Maintainer queue is ${args.queueHealth.level}; review friction lowers risk-adjusted priority.`] : []),
|
|
406
|
+
...(args.collisionsHighRiskCount > 0 ? [`${args.collisionsHighRiskCount} high-risk collision cluster(s) must be cleared before new work has good expected value.`] : []),
|
|
407
|
+
];
|
|
408
|
+
}
|
|
409
|
+
function relevantLaneFor(lane, roleContext) {
|
|
410
|
+
if (roleContext.maintainerLane)
|
|
411
|
+
return "maintainer_lane";
|
|
412
|
+
if (lane.lane === "direct_pr")
|
|
413
|
+
return "direct_pr";
|
|
414
|
+
if (lane.lane === "issue_discovery")
|
|
415
|
+
return "issue_discovery";
|
|
416
|
+
if (lane.lane === "split")
|
|
417
|
+
return "direct_pr";
|
|
418
|
+
return "none";
|
|
419
|
+
}
|
|
420
|
+
function laneValue(lane, preview, relevantLane) {
|
|
421
|
+
if (lane.lane === "inactive" || lane.lane === "unknown" || relevantLane === "none")
|
|
422
|
+
return 0;
|
|
423
|
+
if (relevantLane === "issue_discovery")
|
|
424
|
+
return clamp(preview.laneMath.issueDiscoverySlice * 1000, 0, 100);
|
|
425
|
+
if (relevantLane === "maintainer_lane")
|
|
426
|
+
return clamp(preview.laneMath.repoSlice * 800, 0, 100);
|
|
427
|
+
return clamp(preview.laneMath.directPrSlice * 1000, 0, 100);
|
|
428
|
+
}
|
|
429
|
+
function personalFit(outcome, scoringProfile, roleContext, profile, repoLanguage) {
|
|
430
|
+
if (roleContext.maintainerLane)
|
|
431
|
+
return 80;
|
|
432
|
+
// Award the language-fit bonus only when the repo's primary language (sourced
|
|
433
|
+
// from ContributorFit.languageFit, as decision-pack.ts does) is one the
|
|
434
|
+
// contributor actually works in. Previously this granted +10 to any repo
|
|
435
|
+
// whenever the contributor had *any* top language, never comparing the two —
|
|
436
|
+
// so an off-language repo (e.g. a Rust repo for a Python-only contributor) was
|
|
437
|
+
// scored as a language match, inflating personalFit and the action
|
|
438
|
+
// priorityScores derived from it.
|
|
439
|
+
const contributorLanguages = new Set(profile.github.topLanguages.map((language) => language.toLowerCase()));
|
|
440
|
+
const languageMatch = repoLanguage && contributorLanguages.has(repoLanguage.toLowerCase()) ? 10 : 0;
|
|
441
|
+
return clamp((outcome?.mergedPullRequests ?? 0) * 2.2 +
|
|
442
|
+
/* v8 ignore next -- Credibility fallback order protects sparse private snapshots; scoring behavior is covered at public entry points. */
|
|
443
|
+
(outcome?.credibility ?? scoringProfile?.evidence.credibilityAssumption ?? 0.8) * 35 +
|
|
444
|
+
(outcome?.validSolvedIssues ?? 0) * 3 +
|
|
445
|
+
languageMatch -
|
|
446
|
+
(outcome?.closedPullRequestRate ?? 0) * 45 -
|
|
447
|
+
Math.max(0, (outcome?.openPullRequests ?? 0) - 2) * 4, 0, 100);
|
|
448
|
+
}
|
|
449
|
+
function riskScore(outcome, queueHealth, duplicateClusters, highRiskDuplicateClusters, openPrCount, openPrThreshold) {
|
|
450
|
+
const queuePenalty = queueHealth.level === "critical" ? 35 : queueHealth.level === "high" ? 24 : queueHealth.level === "medium" ? 12 : 0;
|
|
451
|
+
return clamp(queuePenalty +
|
|
452
|
+
duplicateClusters * 4 +
|
|
453
|
+
highRiskDuplicateClusters * 14 +
|
|
454
|
+
Math.max(0, openPrCount - openPrThreshold) * 12 +
|
|
455
|
+
(outcome?.closedPullRequestRate ?? 0) * 55 +
|
|
456
|
+
Math.max(0, (outcome?.openPullRequests ?? 0) - 2) * 5, 0, 100);
|
|
457
|
+
}
|
|
458
|
+
function maintainerFriction(queueHealth, duplicateClusters, pullRequests) {
|
|
459
|
+
const unlinked = pullRequests.filter((pr) => pr.state === "open" && pr.linkedIssues.length === 0).length;
|
|
460
|
+
return clamp(queueHealth.burdenScore * 0.55 + duplicateClusters * 8 + unlinked * 5, 0, 100);
|
|
461
|
+
}
|
|
462
|
+
function reviewChurnRisk(outcome, queueHealth, highRiskDuplicateClusters) {
|
|
463
|
+
const risk = (outcome?.closedPullRequestRate ?? 0) * 100 + highRiskDuplicateClusters * 18 + (queueHealth.level === "critical" ? 25 : queueHealth.level === "high" ? 15 : 0);
|
|
464
|
+
return risk >= 45 ? "high" : risk >= 20 ? "medium" : "low";
|
|
465
|
+
}
|
|
466
|
+
function analysisRank(analysis) {
|
|
467
|
+
return (analysis.actions[0]?.priorityScore ?? 0) + analysis.rewardUpside.directPrSlice * 100 + analysis.rewardUpside.issueDiscoverySlice * 100;
|
|
468
|
+
}
|
|
469
|
+
function bestFitLabels(repo) {
|
|
470
|
+
const multipliers = repo?.registryConfig?.labelMultipliers ?? {};
|
|
471
|
+
const labels = Object.entries(multipliers)
|
|
472
|
+
// Exclude meta labels only at a keyword boundary (a real separator or end-of-string after the keyword),
|
|
473
|
+
// not mid-word — mirroring the anchored `suspiciousConfiguredLabels` matcher in engine.ts. The old
|
|
474
|
+
// unanchored regex over-matched substrings (e.g. "opensource" via "source", "risky-refactor" via "risk"),
|
|
475
|
+
// wrongly dropping a legitimate high-multiplier label from the best-fit suggestion.
|
|
476
|
+
.filter(([label]) => !/^(status|source|contributor|verified|risk|codex)([:/-]|$)/i.test(label))
|
|
477
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
478
|
+
.map(([label]) => label);
|
|
479
|
+
return labels.slice(0, 1);
|
|
480
|
+
}
|
|
481
|
+
function estimatedSourceTokenScore(outcome) {
|
|
482
|
+
return clamp(42 + (outcome?.mergedPullRequests ?? 0) * 2, 30, 120);
|
|
483
|
+
}
|
|
484
|
+
function estimatedTotalTokenScore(outcome) {
|
|
485
|
+
return clamp(70 + (outcome?.mergedPullRequests ?? 0) * 4, 60, 220);
|
|
486
|
+
}
|
|
487
|
+
function estimatedSourceLines(outcome) {
|
|
488
|
+
return Math.max(12, estimatedSourceTokenScore(outcome));
|
|
489
|
+
}
|
|
490
|
+
function nextActionsFor(kind) {
|
|
491
|
+
switch (kind) {
|
|
492
|
+
case "cleanup_existing_prs":
|
|
493
|
+
return ["Land, close, or withdraw stale open PRs before opening additional direct-PR work.", "Prioritize the repo where existing successful evidence is strongest."];
|
|
494
|
+
case "land_existing_prs":
|
|
495
|
+
return ["Tighten validation, update PR bodies, and resolve review/check blockers on already-open work."];
|
|
496
|
+
case "close_or_withdraw_low_fit_prs":
|
|
497
|
+
return ["Withdraw stale or low-fit PRs that are unlikely to merge cleanly and are adding open PR pressure."];
|
|
498
|
+
case "open_new_direct_pr":
|
|
499
|
+
return ["Only open a new PR after duplicate checks, local score preview, tests, and linked/no-issue rationale are clean."];
|
|
500
|
+
case "file_issue_discovery":
|
|
501
|
+
return ["File only high-proof issues that someone else can solve and that are unlikely to be closed as duplicate or unclear."];
|
|
502
|
+
case "maintainer_lane_improve_repo":
|
|
503
|
+
return ["Improve labels, contribution docs, queue hygiene, and contributor intake for the maintained repo."];
|
|
504
|
+
case "maintainer_cut_readiness":
|
|
505
|
+
return ["Check config quality and maintainer_cut readiness before expecting maintainer-lane economics to work cleanly."];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function maintainerNextStepsFor(action, noiseSources) {
|
|
509
|
+
if (action === "review_now")
|
|
510
|
+
return ["Review the technical diff now; cached hygiene signals look clean enough."];
|
|
511
|
+
if (action === "maintainer_lane")
|
|
512
|
+
return ["Treat as maintainer stewardship and verify repo-health impact separately."];
|
|
513
|
+
if (action === "likely_duplicate")
|
|
514
|
+
return ["Compare against linked issues, active PRs, and recent merges before detailed review."];
|
|
515
|
+
if (action === "close_or_redirect")
|
|
516
|
+
return ["Redirect or close non-open/stale context before spending review time."];
|
|
517
|
+
if (action === "needs_author")
|
|
518
|
+
return ["Ask the author to address the concrete missing context before deep review.", ...noiseSources.slice(0, 3)];
|
|
519
|
+
return ["Watch for tests, checks, linked context, or duplicate-risk changes before prioritizing review."];
|
|
520
|
+
}
|
|
521
|
+
function buildEligibilityGap(analyses) {
|
|
522
|
+
return analyses
|
|
523
|
+
.filter((a) => !a.roleContext.maintainerLane && a.actionImpact.cleanupNeeded > 0 && a.actionImpact.cleanupNeeded <= 5)
|
|
524
|
+
.sort((left, right) => left.actionImpact.cleanupNeeded - right.actionImpact.cleanupNeeded)
|
|
525
|
+
.slice(0, 5)
|
|
526
|
+
.map((a) => ({
|
|
527
|
+
repoFullName: a.repoFullName,
|
|
528
|
+
prsToUnlock: a.actionImpact.cleanupNeeded,
|
|
529
|
+
estimatedScoreAtThreshold: a.afterCleanupPreview.scoreEstimate.estimatedMergedScore,
|
|
530
|
+
recommendation: a.actionImpact.explanation,
|
|
531
|
+
}));
|
|
532
|
+
}
|
|
533
|
+
function opportunityCompetitionFactor(highRiskDuplicateClusters, openPullRequests) {
|
|
534
|
+
return round(clamp(highRiskDuplicateClusters / Math.max(1, openPullRequests), 0, 1));
|
|
535
|
+
}
|
|
536
|
+
function opportunityFreshnessFactor(issues) {
|
|
537
|
+
const openIssues = issues.filter((issue) => issue.state === "open");
|
|
538
|
+
if (openIssues.length === 0)
|
|
539
|
+
return 0;
|
|
540
|
+
let mostRecentAgeDays = Number.POSITIVE_INFINITY;
|
|
541
|
+
for (const issue of openIssues) {
|
|
542
|
+
const ageDays = issueAgeDays(pickIssueTimestamp(issue));
|
|
543
|
+
if (ageDays < mostRecentAgeDays)
|
|
544
|
+
mostRecentAgeDays = ageDays;
|
|
545
|
+
}
|
|
546
|
+
// Freshness decays exponentially: ~1.0 at 0 days, ~0.6 at 7 days, ~0.2 at 30 days, ~0.05 at 90 days.
|
|
547
|
+
return round(clamp(Math.exp(-mostRecentAgeDays / 20), 0.05, 1));
|
|
548
|
+
}
|
|
549
|
+
function isParseableIssueTimestamp(value) {
|
|
550
|
+
return Number.isFinite(Date.parse(value));
|
|
551
|
+
}
|
|
552
|
+
function pickIssueTimestamp(issue) {
|
|
553
|
+
const updated = typeof issue.updatedAt === "string" ? issue.updatedAt.trim() : "";
|
|
554
|
+
if (updated && isParseableIssueTimestamp(updated))
|
|
555
|
+
return updated;
|
|
556
|
+
const created = typeof issue.createdAt === "string" ? issue.createdAt.trim() : "";
|
|
557
|
+
if (created && isParseableIssueTimestamp(created))
|
|
558
|
+
return created;
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
/** Unknown/unparseable timestamps floor freshness (parity with gittensory-engine opportunity-freshness.ts). */
|
|
562
|
+
function issueAgeDays(value) {
|
|
563
|
+
if (!value)
|
|
564
|
+
return Number.POSITIVE_INFINITY;
|
|
565
|
+
const parsed = Date.parse(value);
|
|
566
|
+
if (!Number.isFinite(parsed))
|
|
567
|
+
return Number.POSITIVE_INFINITY;
|
|
568
|
+
return Math.floor((Date.now() - parsed) / 86_400_000);
|
|
569
|
+
}
|
|
570
|
+
function sameRepo(left, right) {
|
|
571
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
572
|
+
}
|
|
573
|
+
/** Bucket records by their `repoFullName` (case-insensitive) for O(1) per-repo lookups (#2112). */
|
|
574
|
+
function groupByRepo(records) {
|
|
575
|
+
const buckets = new Map();
|
|
576
|
+
for (const record of records) {
|
|
577
|
+
const key = record.repoFullName.toLowerCase();
|
|
578
|
+
const bucket = buckets.get(key);
|
|
579
|
+
if (bucket)
|
|
580
|
+
bucket.push(record);
|
|
581
|
+
else
|
|
582
|
+
buckets.set(key, [record]);
|
|
583
|
+
}
|
|
584
|
+
return buckets;
|
|
585
|
+
}
|
|
586
|
+
function uniqueRegisteredRepoNames(repoFullNames, registeredRepoNames) {
|
|
587
|
+
const seen = new Set();
|
|
588
|
+
const unique = [];
|
|
589
|
+
for (const repoFullName of repoFullNames) {
|
|
590
|
+
const key = repoFullName.toLowerCase();
|
|
591
|
+
const canonical = registeredRepoNames.get(key);
|
|
592
|
+
if (!canonical || seen.has(key))
|
|
593
|
+
continue;
|
|
594
|
+
seen.add(key);
|
|
595
|
+
unique.push(canonical);
|
|
596
|
+
}
|
|
597
|
+
return unique;
|
|
598
|
+
}
|
|
599
|
+
function nonNegative(value) {
|
|
600
|
+
/* v8 ignore next -- Sparse contributor totals normalize to zero before scoring; aggregate scoring tests cover the behavior. */
|
|
601
|
+
return Number.isFinite(value) ? Math.max(0, value ?? 0) : 0;
|
|
602
|
+
}
|
|
603
|
+
function percent(value) {
|
|
604
|
+
return `${Math.round(value * 100)}%`;
|
|
605
|
+
}
|
|
606
|
+
function round(value) {
|
|
607
|
+
return Math.round(value * 10000) / 10000;
|
|
608
|
+
}
|
|
609
|
+
function clamp(value, min, max) {
|
|
610
|
+
return Math.max(min, Math.min(max, value));
|
|
611
|
+
}
|
|
612
|
+
/* v8 ignore start -- Test-only export surface for branch coverage. */
|
|
613
|
+
export const rewardRiskFreshnessInternals = {
|
|
614
|
+
pickIssueTimestamp,
|
|
615
|
+
issueAgeDays,
|
|
616
|
+
bestFitLabels,
|
|
617
|
+
};
|
|
618
|
+
/* v8 ignore stop */
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function labelMatchesPattern(label: string, pattern: string): boolean;
|
|
2
|
+
export declare const LABEL_PATTERN_REGEXP_CACHE_MAX_ENTRIES = 256;
|
|
3
|
+
export declare function clearLabelPatternRegExpCacheForTest(): void;
|
|
4
|
+
export declare function labelPatternRegExpCacheKeysForTest(): string[];
|