diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index 5697e59cf3..5b6a40875a 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -4,8 +4,10 @@ import type { GittensorContributorSnapshot } from "../gittensor/api"; import type { BountyRecord, CheckSummaryRecord, IssueRecord, PullRequestRecord, RecentMergedPullRequestRecord, RepositoryRecord, ScoringModelSnapshotRecord } from "../types"; import { nowIso } from "../utils/json"; import { + buildCollisionReport, buildLaneAdvice, buildLocalDiffPreflightResult, + buildQueueHealth, buildRepoFitRecommendation, buildRoleContext, type ContributorOutcomeHistory, @@ -23,6 +25,7 @@ import { isPublicSafeText } from "./redaction"; import { deriveEligibilityPlan } from "../services/eligibility-plan"; import { scenarioInputFromLocalBranchMetadata } from "../scenarios/input-model"; import { renderPublicScenarioSummary, type PublicScenarioSummary, type ScenarioSummaryInput } from "../scenarios/scenario-summary"; +import { simulateOpenPrPressure } from "../services/open-pr-pressure-scenarios"; export type LocalBranchChangedFile = { path: string; @@ -366,6 +369,18 @@ export function buildLocalBranchAnalysis(args: { classified: [], } : undefined; + // Open-PR pressure strategy options (#348): the scenario summary renderer fills its strategy + // `options` (open new work / wait / clean up first) and headline from this simulation. Without + // passing it, scenarioSummary.options was always empty and the guidance never reached the miner. + const queuePressureSimulation = simulateOpenPrPressure({ + repoFullName: args.input.repoFullName, + generatedAt: nowIso(), + queueHealth: buildQueueHealth(args.repo, args.issues, args.pullRequests, buildCollisionReport(args.input.repoFullName, args.issues, args.pullRequests)), + roleContext, + contributorOpenPrCount: (args.contributorPullRequests ?? args.pullRequests).filter( + (pr) => pr.state === "open" && (pr.authorLogin ?? "").toLowerCase() === args.input.login.toLowerCase(), + ).length, + }); const scenarioSummary = renderPublicScenarioSummary({ repoFullName: args.input.repoFullName, generatedAt: nowIso(), @@ -373,6 +388,7 @@ export function buildLocalBranchAnalysis(args: { publicBlockers: scorePreview.blockedBy, scenarioInput: branchScenarioInput, pendingDetection: pendingDetectionForSummary, + pressureSimulation: queuePressureSimulation, }); return { login: args.input.login, diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index f3370301d6..9d0e4a7a94 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -1604,6 +1604,43 @@ describe("local MCP git metadata collection", () => { expect(analysis.scenarioSummary.dataClassification.facts).toEqual(expect.arrayContaining(["Contributor", "Repository", "Branch"])); }); + it("wires open-PR pressure strategy options into scenarioSummary.options (#348)", () => { + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + changedFiles: [{ path: "src/util.ts", additions: 30, deletions: 2, status: "modified" }], + localScorer: { mode: "external_command", sourceTokenScore: 40, totalTokenScore: 60, sourceLines: 38 }, + }, + repo, + issues: [{ repoFullName: repo.fullName, number: 9, title: "Improve util", state: "open", labels: [], linkedPrs: [] }], + pullRequests: [ + { repoFullName: repo.fullName, number: 4, title: "WIP util", state: "open", authorLogin: "oktofeesh1", labels: [], linkedIssues: [] }, + ], + // contributorPullRequests is preferred when present; the authorless PR exercises the null-author + // guard in the own-open-PR count and must not be miscounted as this contributor's work. + contributorPullRequests: [ + { repoFullName: repo.fullName, number: 4, title: "WIP util", state: "open", authorLogin: "oktofeesh1", labels: [], linkedIssues: [] }, + { repoFullName: repo.fullName, number: 5, title: "Authorless", state: "open", authorLogin: null, labels: [], linkedIssues: [] }, + ], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + // Before this fix the renderer never received the pressure simulation, so options was always []. + const options = analysis.scenarioSummary.options; + expect(options.length).toBe(3); + expect(options.map((option) => option.rank)).toEqual([1, 2, 3]); + expect(options.filter((option) => option.recommended)).toHaveLength(1); + expect(options[0]?.recommended).toBe(true); + for (const option of options) { + expect(option.label.length).toBeGreaterThan(0); + expect(option.nextStep.length).toBeGreaterThan(0); + } + }); + it("populates scenarioSummary.dataClassification with contributor and repo facts from branch metadata", () => { const analysis = buildLocalBranchAnalysis({ input: {