diff --git a/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx b/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx
index d81b50e5f..89970c86c 100644
--- a/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx
+++ b/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx
@@ -27,6 +27,10 @@ import {
QueueHealthCard,
type MaintainerQueueHealth,
} from "@/components/site/app-panels/queue-health-card";
+import {
+ SlopDuplicateCard,
+ type SlopDuplicateSignal,
+} from "@/components/site/app-panels/slop-duplicate-card";
import { MaintainerSettings } from "@/components/site/app-panels/maintainer-settings";
import { OnboardingPreviewCard } from "@/components/site/app-panels/onboarding-preview-card";
import { CheckRunReadinessTable } from "@/components/site/check-run-readiness-table";
@@ -92,6 +96,7 @@ type MaintainerDashboard = {
topContributors: MaintainerTopContributor[];
gateOutcomeBreakdown: GateOutcomeCardData;
queueHealth?: MaintainerQueueHealth;
+ slopDuplicate?: SlopDuplicateSignal;
};
};
@@ -388,6 +393,8 @@ function MaintainerDashboardView({
+
+
diff --git a/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-card.test.tsx b/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-card.test.tsx
new file mode 100644
index 000000000..09d44c491
--- /dev/null
+++ b/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-card.test.tsx
@@ -0,0 +1,63 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { SlopDuplicateCard } from "@/components/site/app-panels/slop-duplicate-card";
+
+describe("SlopDuplicateCard", () => {
+ it("renders both signal rows with rates and flagged/total counts when populated", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("Slop-flagged")).toBeTruthy();
+ expect(screen.getByText("Duplicate-flagged")).toBeTruthy();
+ expect(screen.getByText("15%")).toBeTruthy();
+ expect(screen.getByText("40%")).toBeTruthy();
+ expect(screen.getByText(/3 of 20 open PRs/)).toBeTruthy();
+ expect(screen.getByText(/8 of 20 open PRs/)).toBeTruthy();
+ });
+
+ it("renders an em-dash when a rate is null (unassessed)", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("—")).toBeTruthy();
+ expect(screen.getByText("no data")).toBeTruthy();
+ });
+
+ it("shows the 'queue is clear' empty state when there are no open PRs", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("Queue is clear")).toBeTruthy();
+ expect(screen.queryByText("Slop-flagged")).toBeNull();
+ });
+
+ it("shows the 'not yet available' empty state when the signal is absent", () => {
+ render();
+ expect(screen.getByText("Not yet available")).toBeTruthy();
+ });
+});
diff --git a/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-card.tsx b/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-card.tsx
new file mode 100644
index 000000000..0e154ae90
--- /dev/null
+++ b/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-card.tsx
@@ -0,0 +1,109 @@
+import { AnalyticsCardShell } from "@/components/site/app-panels/analytics-card-shell";
+import { StatusPill, type Status } from "@/components/site/control-primitives";
+import { cn } from "@/lib/utils";
+
+/** Aggregate slop + duplicate signal for the maintainer quality dashboard (#2202): the share of the maintainer's
+ * open PRs carrying an elevated/high deterministic slop band and the share sitting in a high-risk duplicate
+ * cluster, each as a rate of the open-PR total. Display slice over the dashboard payload's `slopDuplicate`
+ * aggregate — observable rates + bands, never raw scores. Degrades to an empty state when the queue is clear
+ * or the field is absent. (Current-window rates; a time-series trend is a follow-up per the issue thread.) */
+export type SlopDuplicateSignal = {
+ openPullRequests: number;
+ slopFlaggedPullRequests: number;
+ duplicateFlaggedPullRequests: number;
+ slopRate: number | null;
+ duplicateRate: number | null;
+};
+
+/** Lower is healthier — a small flagged share is good; a large one is a burden signal. */
+function rateStatus(rate: number | null): Status {
+ if (rate === null) return "info";
+ if (rate < 0.1) return "ready";
+ if (rate < 0.3) return "warn";
+ return "degraded";
+}
+
+function rateBandLabel(rate: number | null): string {
+ if (rate === null) return "no data";
+ if (rate < 0.1) return "low";
+ if (rate < 0.3) return "watch";
+ return "high";
+}
+
+function SignalRow({
+ label,
+ rate,
+ flagged,
+ total,
+ bar,
+}: {
+ label: string;
+ rate: number | null;
+ flagged: number;
+ total: number;
+ bar: string;
+}) {
+ const pct = rate === null ? null : Math.round(rate * 100);
+ return (
+
+
+ {label}
+
+ {pct === null ? "—" : `${pct}%`}
+
+
+
+ {pct === null ? null :
}
+
+
+
+ {flagged} of {total} open PR{total === 1 ? "" : "s"}
+
+ {rateBandLabel(rate)}
+
+
+ );
+}
+
+export function SlopDuplicateCard({ signal }: { signal?: SlopDuplicateSignal }) {
+ if (!signal || signal.openPullRequests === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/services/maintainer-quality-dashboard.ts b/src/services/maintainer-quality-dashboard.ts
index bfc171ca1..8336f88f5 100644
--- a/src/services/maintainer-quality-dashboard.ts
+++ b/src/services/maintainer-quality-dashboard.ts
@@ -58,6 +58,16 @@ export type MaintainerQualityDashboard = {
ageBuckets: { under7Days: number; days7To30: number; over30Days: number };
bandCounts: Record;
};
+ /** Aggregate slop + duplicate signal across the SHAPED repos' open PRs (#2202): how many open PRs carry an
+ * elevated/high deterministic slop band, how many sit in a high-risk duplicate cluster, and each as a rate
+ * of the open-PR total (null when there are no open PRs). Observable counts + rates, never raw scores. */
+ slopDuplicate: {
+ openPullRequests: number;
+ slopFlaggedPullRequests: number;
+ duplicateFlaggedPullRequests: number;
+ slopRate: number | null;
+ duplicateRate: number | null;
+ };
summary: string;
};
@@ -91,6 +101,7 @@ export function buildMaintainerQualityDashboard(args: { repos: MaintainerQuality
let openPrs = 0;
let duplicatePrRisk = 0;
let missingLinkedIssue = 0;
+ let slopFlaggedPrs = 0;
const queueHealthAggregate = {
openPullRequests: 0,
stalePullRequests: 0,
@@ -141,6 +152,8 @@ export function buildMaintainerQualityDashboard(args: { repos: MaintainerQuality
const inHighRiskCluster = highRiskPrNumbers.has(pr.number);
if (pr.linkedIssues.length === 0) missingLinkedIssue += 1;
if (inHighRiskCluster) duplicatePrRisk += 1;
+ // #2202: an "elevated" or "high" deterministic slop band is the slop flag (null/absent = not assessed).
+ if (pr.slopBand === "elevated" || pr.slopBand === "high") slopFlaggedPrs += 1;
const linkedToRealIssue = pr.linkedIssues.some((number) => realIssueNumbers.has(number));
const author = pr.authorLogin ?? "unknown";
const tally = contributorTotals.get(author) ?? { open: 0, clean: 0 };
@@ -171,6 +184,13 @@ export function buildMaintainerQualityDashboard(args: { repos: MaintainerQuality
topContributors,
qualitySignals: { openPrs, duplicatePrRisk, missingLinkedIssue },
queueHealth: queueHealthAggregate,
+ slopDuplicate: {
+ openPullRequests: openPrs,
+ slopFlaggedPullRequests: slopFlaggedPrs,
+ duplicateFlaggedPullRequests: duplicatePrRisk,
+ slopRate: openPrs > 0 ? slopFlaggedPrs / openPrs : null,
+ duplicateRate: openPrs > 0 ? duplicatePrRisk / openPrs : null,
+ },
summary,
};
}
diff --git a/test/unit/maintainer-quality-dashboard.test.ts b/test/unit/maintainer-quality-dashboard.test.ts
index b9ced8cf6..36f3aff8a 100644
--- a/test/unit/maintainer-quality-dashboard.test.ts
+++ b/test/unit/maintainer-quality-dashboard.test.ts
@@ -68,6 +68,26 @@ describe("buildMaintainerQualityDashboard", () => {
expect(JSON.stringify(dashboard)).not.toMatch(/"burdenScore"/);
});
+ it("aggregates the slop + duplicate signal and its rates, failing safe to null rates with no open PRs (#2202)", () => {
+ const flagged = buildMaintainerQualityDashboard({
+ repos: [input({ pullRequests: [pr(1), pr(2, { slopBand: "high" })] })],
+ generatedAt: "2026-06-14T00:00:00.000Z",
+ });
+ expect(flagged.slopDuplicate.openPullRequests).toBe(2);
+ expect(flagged.slopDuplicate.slopFlaggedPullRequests).toBe(1);
+ expect(flagged.slopDuplicate.slopRate).toBeCloseTo(0.5);
+ expect(flagged.slopDuplicate.duplicateRate).not.toBeNull();
+
+ // No open PRs → the rates divide-guard to null rather than 0/0.
+ const empty = buildMaintainerQualityDashboard({
+ repos: [input({ pullRequests: [] })],
+ generatedAt: "2026-06-14T00:00:00.000Z",
+ });
+ expect(empty.slopDuplicate.openPullRequests).toBe(0);
+ expect(empty.slopDuplicate.slopRate).toBeNull();
+ expect(empty.slopDuplicate.duplicateRate).toBeNull();
+ });
+
it("counts PRs without a linked issue toward the missing-linked-issue signal", () => {
const dashboard = buildMaintainerQualityDashboard({
repos: [input({ pullRequests: [pr(1, { linkedIssues: [] }), pr(2, { linkedIssues: [5] })] })],