Skip to content

Commit 28ccdd5

Browse files
committed
feat(signals): add queue burden score breakdown with weighted contributors and top lever
1 parent 9e2f533 commit 28ccdd5

2 files changed

Lines changed: 467 additions & 0 deletions

File tree

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import { sanitizePublicComment } from "../github/commands";
2+
import type { QueueHealth } from "../signals/engine";
3+
4+
// ─── Queue burden breakdown (explanation family) ─────────────────────────────────────────────────
5+
// A pure projection over a computed {@link QueueHealth} that decomposes the otherwise-opaque
6+
// `burdenScore` into its weighted, observable contributors and names the single highest-leverage lever
7+
// a maintainer can pull to bring queue pressure down fastest. Sibling of `score-breakdown.ts`,
8+
// `miner-dashboard-recommendations.ts`, and `agent-action-explanation-card.ts`: deterministic, no I/O,
9+
// no GitHub fetch. Public-safe by construction — it reports observable counts, relative shares, and
10+
// bands only, and routes every rendered string through `sanitizePublicComment`.
11+
12+
export type QueueBurdenBand = "credit" | "none" | "low" | "moderate" | "high";
13+
14+
export type QueueBurdenComponent = {
15+
/** The QueueHealth signal this contribution is derived from. */
16+
component: string;
17+
/** Observable signal count (open PRs, unlinked PRs, collision clusters, …). */
18+
count: number;
19+
/** Signed per-unit weight this signal carries in the burden formula (the reviewable credit is negative). */
20+
weightPerUnit: number;
21+
/** `count * weightPerUnit` — positive for a penalty, negative for the reviewable credit. */
22+
contribution: number;
23+
/** Share of total positive penalty (0–100). For the credit it is the percentage of penalty it offsets. */
24+
sharePercent: number;
25+
band: QueueBurdenBand;
26+
summary: string;
27+
lever: string;
28+
/** 0–100 ranking weight used to pick the single highest-leverage improvement lever. */
29+
leverageScore: number;
30+
};
31+
32+
export type QueueBurdenBreakdown = {
33+
repoFullName: string;
34+
generatedAt: string;
35+
/** The authoritative (already clamped) burden score carried on the QueueHealth. */
36+
burdenScore: number;
37+
level: QueueHealth["level"];
38+
/** Pre-clamp signed sum of every contribution (penalties minus the reviewable credit). */
39+
rawBurden: number;
40+
/** True when the raw sum fell outside the 0–100 band and the engine clamped it. */
41+
clamped: boolean;
42+
/** Sum of the positive penalty contributions. */
43+
totalPenalty: number;
44+
/** Absolute size of the reviewable credit that offsets the penalties. */
45+
totalCredit: number;
46+
components: QueueBurdenComponent[];
47+
highestLeverageLever: { component: string; lever: string; reason: string };
48+
summary: string;
49+
};
50+
51+
// These per-unit weights MIRROR buildQueueHealth() in src/signals/engine.ts. A drift-guard test rebuilds a
52+
// QueueHealth through buildQueueHealth and asserts this module recomposes the same burdenScore, so any change
53+
// to the engine weights fails the suite instead of silently producing a wrong breakdown.
54+
const PENALTY_DESCRIPTORS: ReadonlyArray<{
55+
component: string;
56+
weightPerUnit: number;
57+
count: (health: QueueHealth) => number;
58+
describe: (count: number) => { summary: string; lever: string };
59+
}> = [
60+
{
61+
component: "unlinkedPullRequests",
62+
weightPerUnit: 8,
63+
count: (health) => health.signals.unlinkedPullRequests,
64+
describe: (count) =>
65+
count > 0
66+
? {
67+
summary: `${count} open pull request(s) lack a linked issue, the heaviest per-PR burden factor.`,
68+
lever: "Ask contributors to link a closing issue or state explicit no-issue intent so unlinked PRs stop driving burden.",
69+
}
70+
: {
71+
summary: "Every open pull request carries linked-issue context, so this factor adds no burden.",
72+
lever: "Keep requiring a linked issue or a clear no-issue rationale so this factor stays at zero.",
73+
},
74+
},
75+
{
76+
component: "collisionClusters",
77+
weightPerUnit: 10,
78+
count: (health) => health.signals.collisionClusters,
79+
describe: (count) =>
80+
count > 0
81+
? {
82+
summary: `${count} duplicate or overlapping work cluster(s) carry the highest per-unit burden weight.`,
83+
lever: "Resolve overlapping submissions before spending detailed review time to cut collision burden fastest.",
84+
}
85+
: {
86+
summary: "No duplicate or overlapping work clusters were detected, so this factor adds no burden.",
87+
lever: "Keep deduplicating incoming work early so collision burden stays at zero.",
88+
},
89+
},
90+
{
91+
component: "openPullRequests",
92+
weightPerUnit: 6,
93+
count: (health) => health.signals.openPullRequests,
94+
describe: (count) =>
95+
count > 0
96+
? {
97+
summary: `${count} open pull request(s) contribute baseline review load.`,
98+
lever: "Land or close open pull requests to reduce the baseline queue load.",
99+
}
100+
: {
101+
summary: "There are no open pull requests adding baseline load.",
102+
lever: "No action needed; baseline pull-request load is already at zero.",
103+
},
104+
},
105+
{
106+
component: "stalePullRequests",
107+
weightPerUnit: 6,
108+
count: (health) => health.signals.stalePullRequests,
109+
describe: (count) =>
110+
count > 0
111+
? {
112+
summary: `${count} open pull request(s) have stalled without an update for at least 14 days.`,
113+
lever: "Review, nudge, or close stale pull requests so they stop accruing burden.",
114+
}
115+
: {
116+
summary: "No open pull requests have stalled past the 14-day staleness threshold.",
117+
lever: "Keep pull requests moving so none cross the staleness threshold.",
118+
},
119+
},
120+
{
121+
component: "over30DayPullRequests",
122+
weightPerUnit: 4,
123+
count: (health) => health.signals.ageBuckets.over30Days,
124+
describe: (count) =>
125+
count > 0
126+
? {
127+
summary: `${count} open pull request(s) have aged past 30 days.`,
128+
lever: "Resolve the long-aged pull requests to clear the oldest backlog in the queue.",
129+
}
130+
: {
131+
summary: "No open pull requests have aged past 30 days.",
132+
lever: "Keep clearing aged work so none crosses the 30-day mark.",
133+
},
134+
},
135+
{
136+
component: "openIssues",
137+
weightPerUnit: 1,
138+
count: (health) => health.signals.openIssues,
139+
describe: (count) =>
140+
count > 0
141+
? {
142+
summary: `${count} open issue(s) add minor triage load.`,
143+
lever: "Triage or close resolved open issues to trim residual queue load.",
144+
}
145+
: {
146+
summary: "There are no open issues adding triage load.",
147+
lever: "No action needed; open-issue triage load is already at zero.",
148+
},
149+
},
150+
];
151+
152+
const REVIEWABLE_CREDIT_PER_UNIT = -2;
153+
154+
function penaltyBand(count: number, sharePercent: number): QueueBurdenBand {
155+
if (count <= 0) return "none";
156+
if (sharePercent >= 40) return "high";
157+
if (sharePercent >= 15) return "moderate";
158+
return "low";
159+
}
160+
161+
function shareOf(contribution: number, totalPenalty: number): number {
162+
if (totalPenalty <= 0) return 0;
163+
return Math.round((Math.abs(contribution) / totalPenalty) * 100);
164+
}
165+
166+
function creditComponent(health: QueueHealth, totalPenalty: number): QueueBurdenComponent {
167+
const count = health.signals.likelyReviewablePullRequests;
168+
const contribution = count * REVIEWABLE_CREDIT_PER_UNIT;
169+
const sharePercent = shareOf(contribution, totalPenalty);
170+
return {
171+
component: "likelyReviewablePullRequests",
172+
count,
173+
weightPerUnit: REVIEWABLE_CREDIT_PER_UNIT,
174+
contribution,
175+
sharePercent,
176+
band: "credit",
177+
summary:
178+
count > 0
179+
? `${count} open pull request(s) look readily reviewable and reduce net queue burden.`
180+
: "No open pull requests are currently counted as readily reviewable, so nothing is offsetting burden.",
181+
lever:
182+
count > 0
183+
? "Keep pull requests linked and fresh so they stay readily reviewable and keep offsetting burden."
184+
: "Help open pull requests become linked and fresh so they start offsetting queue burden.",
185+
// The credit is already helping; it is never the lever a maintainer pulls to REDUCE burden, so it stays
186+
// out of the highest-leverage ranking.
187+
leverageScore: 0,
188+
};
189+
}
190+
191+
function pickHighestLeverage(components: QueueBurdenComponent[]): QueueBurdenBreakdown["highestLeverageLever"] {
192+
// Rank by share of burden, then break ties toward the heavier per-unit weight (reducing one high-weight item
193+
// removes more burden per action, so it is the better lever), and finally by name purely for determinism.
194+
const ranked = [...components].sort(
195+
(left, right) =>
196+
right.leverageScore - left.leverageScore ||
197+
right.weightPerUnit - left.weightPerUnit ||
198+
left.component.localeCompare(right.component),
199+
);
200+
const top = ranked[0]!;
201+
// When no penalty is active (every leverageScore is 0), the sort tie-break would otherwise surface an
202+
// arbitrary alphabetically-first component as "the lever" — which is misleading because there is nothing to
203+
// reduce. Return an explicit no-op lever instead so the breakdown stays honest for a healthy queue.
204+
if (top.leverageScore <= 0) {
205+
return {
206+
component: "none",
207+
lever: sanitizePublicComment("No queue-burden lever needs attention; there are no active contributors to reduce."),
208+
reason: sanitizePublicComment("Queue burden has no active penalty contributors right now, so there is no pressing lever to pull."),
209+
};
210+
}
211+
const reason =
212+
top.band === "high"
213+
? `${top.component} is the dominant queue-burden contributor right now.`
214+
: `${top.component} is the largest remaining queue-burden contributor.`;
215+
return {
216+
component: top.component,
217+
lever: top.lever,
218+
reason: sanitizePublicComment(reason),
219+
};
220+
}
221+
222+
/**
223+
* Pure projection over a {@link QueueHealth} that explains how the queue `burdenScore` breaks down into its
224+
* weighted, observable contributors and names the single highest-leverage lever to reduce queue pressure.
225+
*/
226+
export function explainQueueBurden(health: QueueHealth): QueueBurdenBreakdown {
227+
const penalties = PENALTY_DESCRIPTORS.map((descriptor) => {
228+
const count = descriptor.count(health);
229+
const contribution = count * descriptor.weightPerUnit;
230+
return { descriptor, count, contribution };
231+
});
232+
const totalPenalty = penalties.reduce((sum, entry) => sum + entry.contribution, 0);
233+
234+
const penaltyComponents: QueueBurdenComponent[] = penalties.map(({ descriptor, count, contribution }) => {
235+
const sharePercent = shareOf(contribution, totalPenalty);
236+
const copy = descriptor.describe(count);
237+
return {
238+
component: descriptor.component,
239+
count,
240+
weightPerUnit: descriptor.weightPerUnit,
241+
contribution,
242+
sharePercent,
243+
band: penaltyBand(count, sharePercent),
244+
summary: copy.summary,
245+
lever: copy.lever,
246+
// A penalty's leverage is exactly its share of the burden — the biggest contributor is the best lever.
247+
leverageScore: sharePercent,
248+
};
249+
});
250+
251+
const credit = creditComponent(health, totalPenalty);
252+
const totalCredit = Math.abs(credit.contribution);
253+
const rawBurden = totalPenalty + credit.contribution;
254+
// The engine clamps burden to 0–100. The lower bound is unreachable in practice: every open PR adds 6 to the
255+
// penalty while its reviewable credit only subtracts 2 (and reviewable PRs never exceed open PRs), so the
256+
// penalties always dominate the credit. Only the upper clamp is observable here.
257+
const clamped = rawBurden > 100;
258+
259+
const components = [...penaltyComponents, credit].map((entry) => ({
260+
...entry,
261+
summary: sanitizePublicComment(entry.summary),
262+
lever: sanitizePublicComment(entry.lever),
263+
}));
264+
265+
const highestLeverageLever = pickHighestLeverage(components);
266+
const summary =
267+
highestLeverageLever.component === "none"
268+
? `Queue burden is ${health.level} with no active contributors to address.`
269+
: `Queue burden is ${health.level}; ${highestLeverageLever.component} is the leading factor to address.`;
270+
271+
return {
272+
repoFullName: health.repoFullName,
273+
generatedAt: health.generatedAt,
274+
burdenScore: health.burdenScore,
275+
level: health.level,
276+
rawBurden,
277+
clamped,
278+
totalPenalty,
279+
totalCredit,
280+
components,
281+
highestLeverageLever,
282+
summary: sanitizePublicComment(summary),
283+
};
284+
}

0 commit comments

Comments
 (0)