Skip to content

Commit 81efc24

Browse files
committed
feat(signals): add queue burden score breakdown with weighted contributors and top lever
1 parent 2bdeb19 commit 81efc24

2 files changed

Lines changed: 427 additions & 0 deletions

File tree

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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+
const ranked = [...components].sort(
193+
(left, right) => right.leverageScore - left.leverageScore || left.component.localeCompare(right.component),
194+
);
195+
const top = ranked[0]!;
196+
const reason =
197+
top.leverageScore <= 0
198+
? "Queue burden has no active penalty contributors right now, so there is no pressing lever to pull."
199+
: top.band === "high"
200+
? `${top.component} is the dominant queue-burden contributor right now.`
201+
: `${top.component} is the largest remaining queue-burden contributor.`;
202+
return {
203+
component: top.component,
204+
lever: top.lever,
205+
reason: sanitizePublicComment(reason),
206+
};
207+
}
208+
209+
/**
210+
* Pure projection over a {@link QueueHealth} that explains how the queue `burdenScore` breaks down into its
211+
* weighted, observable contributors and names the single highest-leverage lever to reduce queue pressure.
212+
*/
213+
export function explainQueueBurden(health: QueueHealth): QueueBurdenBreakdown {
214+
const penalties = PENALTY_DESCRIPTORS.map((descriptor) => {
215+
const count = descriptor.count(health);
216+
const contribution = count * descriptor.weightPerUnit;
217+
return { descriptor, count, contribution };
218+
});
219+
const totalPenalty = penalties.reduce((sum, entry) => sum + entry.contribution, 0);
220+
221+
const penaltyComponents: QueueBurdenComponent[] = penalties.map(({ descriptor, count, contribution }) => {
222+
const sharePercent = shareOf(contribution, totalPenalty);
223+
const copy = descriptor.describe(count);
224+
return {
225+
component: descriptor.component,
226+
count,
227+
weightPerUnit: descriptor.weightPerUnit,
228+
contribution,
229+
sharePercent,
230+
band: penaltyBand(count, sharePercent),
231+
summary: copy.summary,
232+
lever: copy.lever,
233+
// A penalty's leverage is exactly its share of the burden — the biggest contributor is the best lever.
234+
leverageScore: sharePercent,
235+
};
236+
});
237+
238+
const credit = creditComponent(health, totalPenalty);
239+
const totalCredit = Math.abs(credit.contribution);
240+
const rawBurden = totalPenalty + credit.contribution;
241+
const clamped = rawBurden < 0 || rawBurden > 100;
242+
243+
const components = [...penaltyComponents, credit].map((entry) => ({
244+
...entry,
245+
summary: sanitizePublicComment(entry.summary),
246+
lever: sanitizePublicComment(entry.lever),
247+
}));
248+
249+
return {
250+
repoFullName: health.repoFullName,
251+
generatedAt: health.generatedAt,
252+
burdenScore: health.burdenScore,
253+
level: health.level,
254+
rawBurden,
255+
clamped,
256+
totalPenalty,
257+
totalCredit,
258+
components,
259+
highestLeverageLever: pickHighestLeverage(components),
260+
summary: sanitizePublicComment(
261+
`Queue burden is ${health.level}; ${pickHighestLeverage(components).component} is the leading factor to address.`,
262+
),
263+
};
264+
}

0 commit comments

Comments
 (0)