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 d81b50e5fa..c06304ebae 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,8 @@ import { QueueHealthCard, type MaintainerQueueHealth, } from "@/components/site/app-panels/queue-health-card"; +import { SlopDuplicateTrendCard } from "@/components/site/app-panels/slop-duplicate-trend-card"; +import type { MaintainerSlopDuplicateTrend } from "@/components/site/app-panels/slop-duplicate-trend-card-model"; 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 +94,7 @@ type MaintainerDashboard = { topContributors: MaintainerTopContributor[]; gateOutcomeBreakdown: GateOutcomeCardData; queueHealth?: MaintainerQueueHealth; + slopDuplicateTrend?: MaintainerSlopDuplicateTrend; }; }; @@ -388,6 +391,10 @@ function MaintainerDashboardView({ + {data.qualityDashboard.slopDuplicateTrend ? ( + + ) : null} + diff --git a/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-trend-card-model.ts b/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-trend-card-model.ts new file mode 100644 index 0000000000..a4bc515a89 --- /dev/null +++ b/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-trend-card-model.ts @@ -0,0 +1,63 @@ +// Slop + duplicate trend card model (#2202). UI-side mirror of MaintainerSlopDuplicateTrend from +// src/services/maintainer-slop-duplicate-trend.ts — plus pure helpers for chart series mapping. + +export type SlopBandLabel = "clean" | "low" | "elevated" | "high"; + +export type SlopDuplicateTrendWeek = { + weekStart: string; + slopFlagRatePct: number | null; + slopBandLabel: SlopBandLabel | null; + duplicateFlagRatePct: number | null; +}; + +export type MaintainerSlopDuplicateTrend = { + generatedAt: string; + stale: boolean; + weeks: SlopDuplicateTrendWeek[]; + summary: string; +}; + +export function formatTrendRatePct(value: number | null | undefined): string { + if (value == null) return "—"; + return `${value}%`; +} + +export function chartValuesForSeries( + weeks: SlopDuplicateTrendWeek[], + series: "slop" | "duplicate", +): number[] { + return weeks.map((week) => { + const value = series === "slop" ? week.slopFlagRatePct : week.duplicateFlagRatePct; + return value ?? 0; + }); +} + +export function seriesHasSignal( + weeks: SlopDuplicateTrendWeek[], + series: "slop" | "duplicate", +): boolean { + return weeks.some((week) => + series === "slop" ? week.slopFlagRatePct !== null : week.duplicateFlagRatePct !== null, + ); +} + +export function trendHasAnySignal(weeks: SlopDuplicateTrendWeek[]): boolean { + return seriesHasSignal(weeks, "slop") || seriesHasSignal(weeks, "duplicate"); +} + +export function latestWeekWithSignal( + weeks: SlopDuplicateTrendWeek[], +): SlopDuplicateTrendWeek | null { + for (let index = weeks.length - 1; index >= 0; index -= 1) { + const week = weeks[index]; + if (!week) continue; + if (week.slopFlagRatePct !== null || week.duplicateFlagRatePct !== null) return week; + } + return null; +} + +export function formatGeneratedAt(iso: string): string { + const parsed = Date.parse(iso); + if (!Number.isFinite(parsed)) return iso; + return new Date(parsed).toUTCString().slice(5, 22); +} diff --git a/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-trend-card.test.tsx b/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-trend-card.test.tsx new file mode 100644 index 0000000000..91619e9de6 --- /dev/null +++ b/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-trend-card.test.tsx @@ -0,0 +1,86 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { SlopDuplicateTrendCard } from "@/components/site/app-panels/slop-duplicate-trend-card"; +import type { MaintainerSlopDuplicateTrend } from "@/components/site/app-panels/slop-duplicate-trend-card-model"; + +function trend( + overrides: Partial = {}, +): MaintainerSlopDuplicateTrend { + return { + generatedAt: "2026-06-14T12:00:00.000Z", + stale: false, + summary: "8-week slop + duplicate flag rates across 1 shaped repo(s).", + weeks: Array.from({ length: 8 }, (_, index) => ({ + weekStart: `2026-04-${String(21 + index).padStart(2, "0")}`, + slopFlagRatePct: 12.5, + slopBandLabel: "low" as const, + duplicateFlagRatePct: 25, + })), + ...overrides, + }; +} + +describe("SlopDuplicateTrendCard", () => { + it("renders both trend series, shared legend, and freshness metadata", () => { + render(); + expect(screen.getByText("Slop + duplicate trend")).toBeTruthy(); + expect(screen.getAllByText("Slop flag rate").length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText("Duplicate flag rate").length).toBeGreaterThanOrEqual(1); + expect(screen.getByText(/latest band: low/i)).toBeTruthy(); + expect(screen.getByText(/latest: 25%/i)).toBeTruthy(); + expect(screen.getByText(/fresh snapshot/i)).toBeTruthy(); + expect(screen.getByText(/generated/i)).toBeTruthy(); + expect(screen.getAllByLabelText("Trend chart")).toHaveLength(2); + }); + + it("shows a one-series-empty branch when only duplicate samples exist", () => { + render( + , + ); + expect(screen.getByText("No slop-flag samples in the snapshot window yet.")).toBeTruthy(); + expect(screen.getByLabelText("Trend chart")).toBeTruthy(); + expect(screen.getByText(/latest: 50%/i)).toBeTruthy(); + }); + + it("shows the no-data branch when every weekly bucket is empty", () => { + render( + , + ); + expect( + screen.getByText( + /Queue-health snapshot history will appear here after signal snapshot jobs run/i, + ), + ).toBeTruthy(); + expect(screen.queryByLabelText("Trend chart")).toBeNull(); + }); + + it("surfaces the stale snapshot pill when data is old", () => { + render(); + expect(screen.getByText(/stale snapshot/i)).toBeTruthy(); + }); +}); diff --git a/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-trend-card.tsx b/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-trend-card.tsx new file mode 100644 index 0000000000..a381f23997 --- /dev/null +++ b/apps/gittensory-ui/src/components/site/app-panels/slop-duplicate-trend-card.tsx @@ -0,0 +1,157 @@ +import { StatusPill } from "@/components/site/control-primitives"; +import { TrendChart } from "@/components/site/trend-chart"; +import { + chartValuesForSeries, + formatGeneratedAt, + formatTrendRatePct, + latestWeekWithSignal, + seriesHasSignal, + trendHasAnySignal, + type MaintainerSlopDuplicateTrend, + type SlopBandLabel, +} from "@/components/site/app-panels/slop-duplicate-trend-card-model"; +import { cn } from "@/lib/utils"; + +const SLOP_BAND_TONE: Record = { + clean: "text-success", + low: "text-mint", + elevated: "text-warning", + high: "text-danger", +}; + +/** Maintainer quality dashboard card (#2202): weekly slop-flag and duplicate-flag rates from queue-health + * snapshots. Band labels only — never raw slop-risk or credibility numbers. */ +export function SlopDuplicateTrendCard({ trend }: { trend: MaintainerSlopDuplicateTrend }) { + const hasSignal = trendHasAnySignal(trend.weeks); + const hasSlop = seriesHasSignal(trend.weeks, "slop"); + const hasDuplicate = seriesHasSignal(trend.weeks, "duplicate"); + const latest = latestWeekWithSignal(trend.weeks); + + return ( +
+
+
+

Slop + duplicate trend

+

+ Weekly slop-flag and duplicate-flag rates from queue-health snapshots. Band labels only. +

+
+
+ + {trend.stale ? "stale snapshot" : "fresh snapshot"} + + + generated {formatGeneratedAt(trend.generatedAt)} + +
+
+ + {hasSignal ? ( + <> +
+ + +
+ +
+ + +
+ +

{trend.summary}

+ + ) : ( +

+ Queue-health snapshot history will appear here after signal snapshot jobs run for your + scoped repositories. +

+ )} +
+ ); +} + +function TrendPanel({ + title, + emptyMessage, + hasSignal, + values, + stroke, + fill, +}: { + title: string; + emptyMessage: string; + hasSignal: boolean; + values: number[]; + stroke: string; + fill: string; +}) { + return ( +
+
{title}
+ {hasSignal ? ( +
+ +
+ ) : ( +

{emptyMessage}

+ )} +
+ ); +} + +function LegendItem({ + color, + label, + detail, + bandLabel, +}: { + color: string; + label: string; + detail: string; + bandLabel?: SlopBandLabel | null; +}) { + return ( +
+ + {label} + + {detail} + +
+ ); +} diff --git a/packages/gittensory-engine/src/signals/predicted-gate-engine.ts b/packages/gittensory-engine/src/signals/predicted-gate-engine.ts index fc086bcafd..eeca6499ca 100644 --- a/packages/gittensory-engine/src/signals/predicted-gate-engine.ts +++ b/packages/gittensory-engine/src/signals/predicted-gate-engine.ts @@ -247,6 +247,23 @@ export function buildQueueHealth( const stalePullRequests = openPullRequests.filter((pr) => daysSince(pr.updatedAt ?? pr.createdAt) >= 14); const draftPullRequests = openPullRequests.filter((pr) => pr.isDraft); const maintainerAuthoredPullRequests = openPullRequests.filter((pr) => isMaintainerAssociation(pr.authorAssociation)); + const slopFlaggedPullRequests = openPullRequests.filter( + (pr) => pr.slopBand === "elevated" || pr.slopBand === "high", + ).length; + const highRiskDuplicatePrNumbers = new Set( + collisions.clusters + .filter( + (cluster) => + cluster.risk === "high" && + cluster.items.filter((item) => item.type === "pull_request").length >= 2, + ) + .flatMap((cluster) => + cluster.items.filter((item) => item.type === "pull_request").map((item) => item.number), + ), + ); + const duplicateFlaggedPullRequests = openPullRequests.filter((pr) => + highRiskDuplicatePrNumbers.has(pr.number), + ).length; const cachedLikelyReviewablePullRequests = openPullRequests.filter((pr) => pr.linkedIssues.length > 0 && daysSince(pr.updatedAt ?? pr.createdAt) < 30).length; const likelyReviewablePullRequests = Math.min(openPullRequestCount, Math.max(cachedLikelyReviewablePullRequests, countOverrides.likelyReviewablePullRequests ?? 0)); const ageBuckets = { @@ -323,6 +340,8 @@ export function buildQueueHealth( draftPullRequests: draftPullRequests.length, maintainerAuthoredPullRequests: maintainerAuthoredPullRequests.length, collisionClusters: collisions.summary.clusterCount, + slopFlaggedPullRequests, + duplicateFlaggedPullRequests, ageBuckets, likelyReviewablePullRequests, cachedOpenPullRequests: openPullRequests.length, diff --git a/packages/gittensory-engine/src/types/predicted-gate-types.ts b/packages/gittensory-engine/src/types/predicted-gate-types.ts index a3091ff056..64578d2b5b 100644 --- a/packages/gittensory-engine/src/types/predicted-gate-types.ts +++ b/packages/gittensory-engine/src/types/predicted-gate-types.ts @@ -197,6 +197,10 @@ export type QueueHealth = { draftPullRequests: number; maintainerAuthoredPullRequests: number; collisionClusters: number; + /** Open PRs with slop band elevated or high (public-safe flag count for trend snapshots). */ + slopFlaggedPullRequests: number; + /** Open PRs in a high-risk duplicate cluster with 2+ pull requests (public-safe flag count). */ + duplicateFlaggedPullRequests: number; ageBuckets: { under7Days: number; days7To30: number; diff --git a/src/api/routes.ts b/src/api/routes.ts index dbc436c534..48331c779a 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -84,6 +84,7 @@ import { listRepoSyncStates, summarizeRepoSyncOpenPullRequests, listSignalSnapshots, + listRecentSignalSnapshotsForTargets, listPullRequests, listRepositories, getLatestUpstreamRulesetSnapshot, @@ -267,6 +268,7 @@ import { loadPublicAccuracyTrend } from "../services/public-accuracy-trend"; import { loadPublicReuseRateTrend } from "../services/public-reuse-rate-trend"; import { loadPublicReviewVolumeTrend } from "../services/public-review-volume-trend"; import { buildMaintainerQualityDashboard, isMaintainerQualityDataStale } from "../services/maintainer-quality-dashboard"; +import { buildMaintainerSlopDuplicateTrend, SLOP_DUPLICATE_TREND_SNAPSHOT_LIMIT } from "../services/maintainer-slop-duplicate-trend"; import { buildGateOutcomeBreakdown, GATE_OUTCOME_BREAKDOWN_WINDOW_DAYS } from "../services/gate-outcome-breakdown"; import { MAX_LOCAL_SCORER_WARNING_CHARS, MAX_LOCAL_SCORER_WARNING_COUNT } from "../signals/local-scorer-diagnostics"; import { compileFocusManifestPolicy, MAX_FOCUS_MANIFEST_BYTES, normalizeReadinessGateMode } from "../signals/focus-manifest"; @@ -1397,9 +1399,38 @@ export function createApp() { ]); const qualityRepoNames = new Set(qualityRepos.map((repo) => repo.fullName.toLowerCase())); const scopedSyncCompletions = allSyncStates.filter((state) => qualityRepoNames.has(state.repoFullName.toLowerCase())).map((state) => state.lastCompletedAt); - const qualityStale = isMaintainerQualityDataStale({ lastCompletedAts: scopedSyncCompletions, repoCount: qualityRepos.length, nowMs: Date.parse(nowIso()) }); - const qualityDashboard = buildMaintainerQualityDashboard({ repos: qualityRepoInputs, generatedAt: nowIso(), stale: qualityStale, repoTotal: repositories.length }); - const gateOutcomeSinceIso = new Date(Date.parse(nowIso()) - GATE_OUTCOME_BREAKDOWN_WINDOW_DAYS * 24 * 60 * 60 * 1000).toISOString(); + const generatedAt = nowIso(); + const qualityStale = isMaintainerQualityDataStale({ lastCompletedAts: scopedSyncCompletions, repoCount: qualityRepos.length, nowMs: Date.parse(generatedAt) }); + const queueHealthHistoriesByRepo = await listRecentSignalSnapshotsForTargets( + c.env, + "queue-health", + qualityRepos.map((repo) => repo.fullName), + SLOP_DUPLICATE_TREND_SNAPSHOT_LIMIT, + ); + const slopDuplicateTrend = buildMaintainerSlopDuplicateTrend({ + repos: qualityRepoInputs.map((input) => { + const collisions = buildCollisionReport(input.repo.fullName, input.issues, input.pullRequests); + const currentQueueHealth = buildQueueHealth(input.repo, input.issues, input.pullRequests, collisions); + return { + repoFullName: input.repo.fullName, + queueHealthSnapshots: queueHealthHistoriesByRepo.get(input.repo.fullName) ?? [], + currentQueueHealth, + }; + }), + generatedAt, + stale: qualityStale, + nowMs: Date.parse(generatedAt), + }); + const qualityDashboard = { + ...buildMaintainerQualityDashboard({ + repos: qualityRepoInputs, + generatedAt, + stale: qualityStale, + repoTotal: repositories.length, + }), + slopDuplicateTrend, + }; + const gateOutcomeSinceIso = new Date(Date.parse(generatedAt) - GATE_OUTCOME_BREAKDOWN_WINDOW_DAYS * 24 * 60 * 60 * 1000).toISOString(); const gateOutcomeRollups = await listGateOutcomeAuditEventRollups(c.env, { repoFullNames: repositories.map((repo) => repo.fullName), sinceIso: gateOutcomeSinceIso, @@ -1407,10 +1438,10 @@ export function createApp() { const gateOutcomeBreakdown = buildGateOutcomeBreakdown({ rollups: gateOutcomeRollups, windowDays: GATE_OUTCOME_BREAKDOWN_WINDOW_DAYS, - generatedAt: nowIso(), + generatedAt, }); return c.json({ - generatedAt: nowIso(), + generatedAt, installations, health: health.map(enrichInstallationHealth), metrics: [ diff --git a/src/db/repositories.ts b/src/db/repositories.ts index b70d8f474f..2b9770e5e9 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -5025,6 +5025,65 @@ export async function listLatestSignalSnapshotsForTargets( return result; } +/** Bulk variant of `listSignalSnapshots` for callers that need the most recent N snapshots per target key + * across many repos in bounded round trips (#2202 maintainer-dashboard trend card; mirrors + * listLatestSignalSnapshotsForTargets' batching). */ +export async function listRecentSignalSnapshotsForTargets( + env: Env, + signalType: string, + targetKeys: readonly string[], + maxPerTarget = 16, +): Promise> { + const result = new Map(); + if (targetKeys.length === 0) return result; + const perTargetLimit = Math.max(1, Math.min(maxPerTarget, 100)); + for (let i = 0; i < targetKeys.length; i += SIGNAL_SNAPSHOT_TARGET_KEY_SQL_BATCH) { + const batch = targetKeys.slice(i, i + SIGNAL_SNAPSHOT_TARGET_KEY_SQL_BATCH); + const placeholders = batch.map(() => "?").join(", "); + const { results } = await env.DB.prepare( + ` + SELECT id, signal_type, target_key, repo_full_name, generated_at, payload_json + FROM ( + SELECT + id, + signal_type, + target_key, + repo_full_name, + generated_at, + payload_json, + row_number() OVER (PARTITION BY target_key ORDER BY generated_at DESC, rowid DESC) AS snapshot_rank + FROM signal_snapshots + WHERE signal_type = ? AND target_key IN (${placeholders}) + ) + WHERE snapshot_rank <= ? + ORDER BY target_key, generated_at DESC + `, + ) + .bind(signalType, ...batch, perTargetLimit) + .all<{ + id: string; + signal_type: string; + target_key: string; + repo_full_name: string | null; + generated_at: string; + payload_json: string; + }>(); + for (const row of results) { + const snapshots = result.get(row.target_key) ?? []; + snapshots.push({ + id: row.id, + signalType: row.signal_type, + targetKey: row.target_key, + repoFullName: row.repo_full_name, + payload: parseJson>(row.payload_json, {}), + generatedAt: row.generated_at, + }); + result.set(row.target_key, snapshots); + } + } + return result; +} + export async function listLatestSignalSnapshotsByTarget( env: Env, options: { limit?: number; generatedAfter?: string; maxTargetKeyChars?: number } = {}, diff --git a/src/services/maintainer-quality-dashboard.ts b/src/services/maintainer-quality-dashboard.ts index bfc171ca1e..6a1f10aaca 100644 --- a/src/services/maintainer-quality-dashboard.ts +++ b/src/services/maintainer-quality-dashboard.ts @@ -1,4 +1,5 @@ import { buildCollisionReport, buildQueueHealth, type QueueHealth } from "../signals/engine"; +import type { MaintainerSlopDuplicateTrend } from "./maintainer-slop-duplicate-trend"; import type { IssueRecord, PullRequestRecord, RepositoryRecord } from "../types"; // ─── Maintainer quality dashboard (#557) ───────────────────────────────────────────────────────── @@ -46,6 +47,8 @@ export type MaintainerQualityDashboard = { topContributors: MaintainerTopContributor[]; /** Aggregate counts across the SHAPED repos' open PRs — observable facts, not private scores. */ qualitySignals: { openPrs: number; duplicatePrRisk: number; missingLinkedIssue: number }; + /** Weekly slop-flag + duplicate-flag rates from queue-health snapshots (#2202). Attached at API compose time. */ + slopDuplicateTrend?: MaintainerSlopDuplicateTrend; /** Aggregate PR-queue-health across the SHAPED repos (#2201): summed open/stale/draft/unlinked PR counts, * collision clusters, an age-bucket distribution, and how many repos fall in each burden band. Observable * counts + bands only, never raw scores — folds the per-repo QueueHealth signals the shaping already computes. */ diff --git a/src/services/maintainer-slop-duplicate-trend.ts b/src/services/maintainer-slop-duplicate-trend.ts new file mode 100644 index 0000000000..f007330240 --- /dev/null +++ b/src/services/maintainer-slop-duplicate-trend.ts @@ -0,0 +1,194 @@ +import type { JsonValue, SignalSnapshotRecord } from "../types"; +import type { QueueHealth } from "../signals/engine"; +import { isoWeekStart } from "./public-quality-metrics"; + +// Maintainer slop + duplicate flag-rate trend (#2202). Shapes queue-health signal snapshots (and optional +// live queue-health points) into weekly slop-flag and duplicate-flag RATES for the maintainer dashboard card. +// Public-safe: band labels and observable rates only — never raw slop-risk or credibility numbers. + +export const SLOP_DUPLICATE_TREND_WEEKS = 8; +/** Max queue-health snapshots read per repo when shaping the maintainer trend card — two per week of history. */ +export const SLOP_DUPLICATE_TREND_SNAPSHOT_LIMIT = SLOP_DUPLICATE_TREND_WEEKS * 2; +const MS_PER_WEEK = 7 * 86_400_000; +const MIN_OPEN_PRS_FOR_RATE = 1; +const SLOP_BAND_LOW_MAX_PCT = 25; +const SLOP_BAND_ELEVATED_MAX_PCT = 60; + +export type SlopBandLabel = "clean" | "low" | "elevated" | "high"; + +export type SlopDuplicateTrendWeek = { + /** UTC Monday (YYYY-MM-DD) that starts the bucket. */ + weekStart: string; + /** Share of open PRs flagged elevated/high slop; null when no open PR sample. */ + slopFlagRatePct: number | null; + /** Dominant slop band label for the week (from the aggregate flag rate, not raw risk scores). */ + slopBandLabel: SlopBandLabel | null; + /** Share of open PRs in a high-risk duplicate cluster; null when no open PR sample. */ + duplicateFlagRatePct: number | null; +}; + +export type MaintainerSlopDuplicateTrend = { + generatedAt: string; + stale: boolean; + weeks: SlopDuplicateTrendWeek[]; + summary: string; +}; + +export type MaintainerSlopDuplicateTrendRepoInput = { + repoFullName: string; + queueHealthSnapshots?: SignalSnapshotRecord[] | undefined; + currentQueueHealth?: QueueHealth | undefined; +}; + +type TrendPoint = { + generatedAt: string; + repoFullName: string; + openPullRequests: number; + slopFlaggedPullRequests: number; + duplicateFlaggedPullRequests: number; +}; + +export function buildMaintainerSlopDuplicateTrend(args: { + repos: MaintainerSlopDuplicateTrendRepoInput[]; + generatedAt: string; + stale?: boolean; + nowMs?: number; + weeks?: number; +}): MaintainerSlopDuplicateTrend { + const weeks = args.weeks ?? SLOP_DUPLICATE_TREND_WEEKS; + const nowMs = args.nowMs ?? Date.parse(args.generatedAt); + const currentStartMs = Date.parse(isoWeekStart(nowMs)); + const oldestStartMs = currentStartMs - (weeks - 1) * MS_PER_WEEK; + const points = collectTrendPoints(args.repos); + const trendWeeks = Array.from({ length: weeks }, (_, offset) => { + const weekStart = isoWeekStart(oldestStartMs + offset * MS_PER_WEEK); + const weekEndMs = oldestStartMs + (offset + 1) * MS_PER_WEEK; + const weekStartMs = oldestStartMs + offset * MS_PER_WEEK; + const totals = aggregateWeek(points, weekStartMs, weekEndMs); + const slopFlagRatePct = ratePct(totals.slopFlaggedPullRequests, totals.openPullRequests); + const duplicateFlagRatePct = ratePct(totals.duplicateFlaggedPullRequests, totals.openPullRequests); + return { + weekStart, + slopFlagRatePct, + slopBandLabel: slopBandLabelFromRate(slopFlagRatePct), + duplicateFlagRatePct, + }; + }); + const shapedRepos = args.repos.length; + const hasSignal = trendWeeks.some( + (week) => week.slopFlagRatePct !== null || week.duplicateFlagRatePct !== null, + ); + return { + generatedAt: args.generatedAt, + stale: args.stale ?? false, + weeks: trendWeeks, + summary: hasSignal + ? `${weeks}-week slop + duplicate flag rates across ${shapedRepos} shaped repo(s).` + : `No queue-health snapshot history yet for slop + duplicate trends across ${shapedRepos} shaped repo(s).`, + }; +} + +/** Map an aggregate slop flag rate to a public band label (never a raw credibility score). */ +export function slopBandLabelFromRate(ratePct: number | null): SlopBandLabel | null { + if (ratePct == null) return null; + if (ratePct <= 0) return "clean"; + if (ratePct < SLOP_BAND_LOW_MAX_PCT) return "low"; + if (ratePct < SLOP_BAND_ELEVATED_MAX_PCT) return "elevated"; + return "high"; +} + +export function trendPointFromQueueHealth(queueHealth: QueueHealth): Omit { + return { + generatedAt: queueHealth.generatedAt, + openPullRequests: queueHealth.signals.openPullRequests, + slopFlaggedPullRequests: queueHealth.signals.slopFlaggedPullRequests, + duplicateFlaggedPullRequests: queueHealth.signals.duplicateFlaggedPullRequests, + }; +} + +function collectTrendPoints(repos: MaintainerSlopDuplicateTrendRepoInput[]): TrendPoint[] { + const points: TrendPoint[] = []; + for (const repo of repos) { + for (const snapshot of repo.queueHealthSnapshots ?? []) { + const extracted = trendPointFromSignalSnapshot(snapshot); + if (extracted) points.push({ repoFullName: repo.repoFullName, ...extracted }); + } + if (repo.currentQueueHealth) { + points.push({ repoFullName: repo.repoFullName, ...trendPointFromQueueHealth(repo.currentQueueHealth) }); + } + } + return points.filter((point) => Number.isFinite(Date.parse(point.generatedAt))); +} + +function aggregateWeek( + points: TrendPoint[], + weekStartMs: number, + weekEndMs: number, +): { openPullRequests: number; slopFlaggedPullRequests: number; duplicateFlaggedPullRequests: number } { + const latestByRepo = new Map(); + for (const point of points) { + const ms = Date.parse(point.generatedAt); + if (ms < weekStartMs || ms >= weekEndMs) continue; + const existing = latestByRepo.get(point.repoFullName); + if (!existing || Date.parse(existing.generatedAt) < ms) latestByRepo.set(point.repoFullName, point); + } + let openPullRequests = 0; + let slopFlaggedPullRequests = 0; + let duplicateFlaggedPullRequests = 0; + for (const point of latestByRepo.values()) { + openPullRequests += point.openPullRequests; + slopFlaggedPullRequests += point.slopFlaggedPullRequests; + duplicateFlaggedPullRequests += point.duplicateFlaggedPullRequests; + } + return { openPullRequests, slopFlaggedPullRequests, duplicateFlaggedPullRequests }; +} + +function trendPointFromSignalSnapshot( + snapshot: SignalSnapshotRecord, +): Omit | null { + if (!snapshot.generatedAt) return null; + const signals = readQueueHealthSignals(snapshot.payload); + return signals ? { generatedAt: snapshot.generatedAt, ...signals } : null; +} + +function readQueueHealthSignals( + payload: Record, +): Omit | null { + const signals = isRecord(payload.signals) ? payload.signals : null; + if (!signals) return null; + const openPullRequests = numberValue(signals.openPullRequests); + const collisionClusters = numberValue(signals.collisionClusters); + const slopFlaggedPullRequests = numberValue(signals.slopFlaggedPullRequests); + const duplicateFlaggedPullRequests = + signals.duplicateFlaggedPullRequests !== undefined + ? numberValue(signals.duplicateFlaggedPullRequests) + : legacyDuplicateFlagged(openPullRequests, collisionClusters); + return { + openPullRequests, + slopFlaggedPullRequests, + duplicateFlaggedPullRequests, + }; +} + +/** Pre-#2202 queue-health snapshots only stored collision cluster counts — approximate flagged PRs. */ +function legacyDuplicateFlagged(openPullRequests: number, collisionClusters: number): number { + if (openPullRequests <= 0 || collisionClusters <= 0) return 0; + return Math.min(openPullRequests, collisionClusters * 2); +} + +function ratePct(flagged: number, openPullRequests: number): number | null { + if (openPullRequests < MIN_OPEN_PRS_FOR_RATE) return null; + return roundPct((flagged / openPullRequests) * 100); +} + +function roundPct(value: number): number { + return Math.round(value * 10) / 10; +} + +function numberValue(value: JsonValue | undefined): number { + return typeof value === "number" && Number.isFinite(value) ? value : 0; +} + +function isRecord(value: JsonValue | undefined): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 2d10d26ba8..145dfc94bc 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -99,6 +99,10 @@ export type QueueHealth = { draftPullRequests: number; maintainerAuthoredPullRequests: number; collisionClusters: number; + /** Open PRs with slop band elevated or high (public-safe flag count for trend snapshots). */ + slopFlaggedPullRequests: number; + /** Open PRs in a high-risk duplicate cluster with 2+ pull requests (public-safe flag count). */ + duplicateFlaggedPullRequests: number; ageBuckets: { under7Days: number; days7To30: number; @@ -957,6 +961,23 @@ export function buildQueueHealth( const stalePullRequests = openPullRequests.filter((pr) => daysSince(pr.updatedAt ?? pr.createdAt) >= 14); const draftPullRequests = openPullRequests.filter((pr) => pr.isDraft); const maintainerAuthoredPullRequests = openPullRequests.filter((pr) => isMaintainerAssociation(pr.authorAssociation)); + const slopFlaggedPullRequests = openPullRequests.filter( + (pr) => pr.slopBand === "elevated" || pr.slopBand === "high", + ).length; + const highRiskDuplicatePrNumbers = new Set( + collisions.clusters + .filter( + (cluster) => + cluster.risk === "high" && + cluster.items.filter((item) => item.type === "pull_request").length >= 2, + ) + .flatMap((cluster) => + cluster.items.filter((item) => item.type === "pull_request").map((item) => item.number), + ), + ); + const duplicateFlaggedPullRequests = openPullRequests.filter((pr) => + highRiskDuplicatePrNumbers.has(pr.number), + ).length; const cachedLikelyReviewablePullRequests = openPullRequests.filter((pr) => pr.linkedIssues.length > 0 && daysSince(pr.updatedAt ?? pr.createdAt) < 30).length; const likelyReviewablePullRequests = Math.min(openPullRequestCount, Math.max(cachedLikelyReviewablePullRequests, countOverrides.likelyReviewablePullRequests ?? 0)); const ageBuckets = { @@ -1030,6 +1051,8 @@ export function buildQueueHealth( draftPullRequests: draftPullRequests.length, maintainerAuthoredPullRequests: maintainerAuthoredPullRequests.length, collisionClusters: collisions.summary.clusterCount, + slopFlaggedPullRequests, + duplicateFlaggedPullRequests, ageBuckets, likelyReviewablePullRequests, cachedOpenPullRequests: openPullRequests.length, diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index 86c8c3c337..d4baf1062f 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -2227,6 +2227,7 @@ describe("api routes", () => { repoQuality: Array<{ repoFullName: string; queueBand: string }>; topContributors: Array<{ login: string; band: string }>; qualitySignals: { openPrs: number }; + slopDuplicateTrend: { generatedAt: string; stale: boolean; weeks: Array<{ weekStart: string }>; summary: string }; summary: string; gateOutcomeBreakdown: { windowDays: number; @@ -2244,6 +2245,8 @@ describe("api routes", () => { expect(body.qualityDashboard.repoQuality.every((entry) => ["low", "medium", "high", "critical"].includes(entry.queueBand))).toBe(true); expect(body.qualityDashboard.topContributors.every((entry) => ["strong", "developing", "early"].includes(entry.band))).toBe(true); expect(body.qualityDashboard.qualitySignals.openPrs).toBeGreaterThanOrEqual(0); + expect(body.qualityDashboard.slopDuplicateTrend.generatedAt).toEqual(expect.any(String)); + expect(body.qualityDashboard.slopDuplicateTrend.weeks.length).toBeGreaterThan(0); expect(body.qualityDashboard.summary).toContain("open PR(s)"); expect(body.qualityDashboard.gateOutcomeBreakdown.windowDays).toBeGreaterThan(0); expect(body.qualityDashboard.gateOutcomeBreakdown.total).toBeGreaterThanOrEqual(0); diff --git a/test/unit/contributor-issue-draft.test.ts b/test/unit/contributor-issue-draft.test.ts index 81173d3742..617ce3ef71 100644 --- a/test/unit/contributor-issue-draft.test.ts +++ b/test/unit/contributor-issue-draft.test.ts @@ -106,7 +106,7 @@ describe("contributor issue drafts", () => { const repo = { fullName: repoFullName, isRegistered: true } as never; const issues: IssueRecord[] = []; const pullRequests: never[] = []; - const collisions = { duplicatePairs: 0, openIssueCollisions: 0, summary: "" } as never; + const collisions = buildCollisionReport(repoFullName, issues, pullRequests); const queueCounts = { openIssues: 0, openPullRequests: 0 }; const lane = buildLaneAdvice(repo, repoFullName); const queueHealth = buildQueueHealth(repo, issues, pullRequests, collisions, queueCounts); diff --git a/test/unit/data-spine.test.ts b/test/unit/data-spine.test.ts index b10c22cd37..13df62f1eb 100644 --- a/test/unit/data-spine.test.ts +++ b/test/unit/data-spine.test.ts @@ -21,6 +21,7 @@ import { listRecentMergedPullRequests, listRepoLabels, listLatestSignalSnapshotsForTargets, + listRecentSignalSnapshotsForTargets, listRepoSyncStates, listSignalSnapshots, countOpenIssues, @@ -571,3 +572,41 @@ describe("listLatestSignalSnapshotsForTargets (#3202 — bulk latest-per-target expect(boundCounts).toEqual([91, 6]); }); }); + +describe("listRecentSignalSnapshotsForTargets (#2202 — bulk recent-per-target history)", () => { + it("returns an empty map without querying the DB for an empty target-key list", async () => { + const env = createTestEnv(); + expect(await listRecentSignalSnapshotsForTargets(env, "queue-health", [])).toEqual(new Map()); + }); + + it("returns the most recent snapshots per target key up to maxPerTarget, with payloads", async () => { + const env = createTestEnv(); + for (const [id, generatedAt] of [ + ["qh-1", "2026-01-01T00:00:00.000Z"], + ["qh-2", "2026-02-01T00:00:00.000Z"], + ["qh-3", "2026-03-01T00:00:00.000Z"], + ] as const) { + await persistSignalSnapshot(env, { + id, + signalType: "queue-health", + targetKey: "owner/a", + payload: { signals: { openPullRequests: Number(id.split("-")[1]) } }, + generatedAt, + }); + } + await persistSignalSnapshot(env, { + id: "qh-b", + signalType: "queue-health", + targetKey: "owner/b", + payload: { signals: { openPullRequests: 9 } }, + generatedAt: "2026-04-01T00:00:00.000Z", + }); + + const result = await listRecentSignalSnapshotsForTargets(env, "queue-health", ["owner/a", "owner/b", "owner/c"], 2); + + expect(result.get("owner/a")?.map((row) => row.id)).toEqual(["qh-3", "qh-2"]); + expect(result.get("owner/a")?.[0]?.payload).toMatchObject({ signals: { openPullRequests: 3 } }); + expect(result.get("owner/b")?.map((row) => row.id)).toEqual(["qh-b"]); + expect(result.has("owner/c")).toBe(false); + }); +}); diff --git a/test/unit/maintainer-slop-duplicate-trend.test.ts b/test/unit/maintainer-slop-duplicate-trend.test.ts new file mode 100644 index 0000000000..5e4e0cbc53 --- /dev/null +++ b/test/unit/maintainer-slop-duplicate-trend.test.ts @@ -0,0 +1,329 @@ +import { describe, expect, it } from "vitest"; + +import { buildQueueHealth, buildCollisionReport } from "../../src/signals/engine"; +import { + buildMaintainerSlopDuplicateTrend, + slopBandLabelFromRate, + SLOP_DUPLICATE_TREND_WEEKS, + trendPointFromQueueHealth, +} from "../../src/services/maintainer-slop-duplicate-trend"; +import type { IssueRecord, PullRequestRecord, RepositoryRecord } from "../../src/types"; + +const FORBIDDEN_PUBLIC_TERMS = + /wallet|hotkey|coldkey|mnemonic|reward|payout|farming|raw trust|trust score|scoreability|credibility|private ranking|slopRisk/i; + +function repo(fullName: string): RepositoryRecord { + return { + fullName, + owner: fullName.split("/")[0]!, + name: fullName.split("/")[1]!, + isInstalled: true, + isRegistered: true, + isPrivate: false, + registryConfig: { + repo: fullName, + emissionShare: 0.02, + issueDiscoveryShare: 0, + labelMultipliers: {}, + maintainerCut: 0, + raw: {}, + }, + }; +} + +function pr(number: number, over: Partial = {}): PullRequestRecord { + return { + repoFullName: "octo/demo", + number, + title: `PR ${number}`, + state: "open", + authorLogin: "alice", + authorAssociation: "NONE", + headSha: `sha${number}`, + labels: [], + linkedIssues: [number + 100], + ...over, + }; +} + +function issue(number: number): IssueRecord { + return { + repoFullName: "octo/demo", + number, + title: `Issue ${number}`, + state: "open", + authorLogin: "maintainer", + authorAssociation: "OWNER", + labels: [], + linkedPrs: [], + }; +} + +function queueHealthSnapshot(generatedAt: string, signals: Record) { + return { + id: crypto.randomUUID(), + signalType: "queue-health", + targetKey: "octo/demo", + repoFullName: "octo/demo", + payload: { signals }, + generatedAt, + }; +} + +describe("buildMaintainerSlopDuplicateTrend", () => { + it("builds both weekly series with band labels and no forbidden public terms", () => { + const trend = buildMaintainerSlopDuplicateTrend({ + generatedAt: "2026-06-14T12:00:00.000Z", + stale: false, + nowMs: Date.parse("2026-06-14T12:00:00.000Z"), + repos: [ + { + repoFullName: "octo/demo", + queueHealthSnapshots: [ + queueHealthSnapshot("2026-06-02T00:00:00.000Z", { + openPullRequests: 10, + collisionClusters: 1, + slopFlaggedPullRequests: 1, + duplicateFlaggedPullRequests: 2, + }), + queueHealthSnapshot("2026-06-09T00:00:00.000Z", { + openPullRequests: 8, + collisionClusters: 2, + slopFlaggedPullRequests: 4, + duplicateFlaggedPullRequests: 3, + }), + ], + }, + ], + }); + expect(trend.generatedAt).toBe("2026-06-14T12:00:00.000Z"); + expect(trend.stale).toBe(false); + expect(trend.weeks).toHaveLength(SLOP_DUPLICATE_TREND_WEEKS); + const populated = trend.weeks.filter( + (week) => week.slopFlagRatePct !== null || week.duplicateFlagRatePct !== null, + ); + expect(populated.length).toBeGreaterThan(0); + expect(populated.some((week) => week.slopFlagRatePct !== null)).toBe(true); + expect(populated.some((week) => week.duplicateFlagRatePct !== null)).toBe(true); + for (const week of populated) { + if (week.slopBandLabel) { + expect(["clean", "low", "elevated", "high"]).toContain(week.slopBandLabel); + } + } + expect(JSON.stringify(trend)).not.toMatch(FORBIDDEN_PUBLIC_TERMS); + }); + + it("returns null slop series when snapshots lack slop counts but still shapes duplicate series", () => { + const trend = buildMaintainerSlopDuplicateTrend({ + generatedAt: "2026-06-14T12:00:00.000Z", + nowMs: Date.parse("2026-06-14T12:00:00.000Z"), + repos: [ + { + repoFullName: "octo/demo", + queueHealthSnapshots: [ + queueHealthSnapshot("2026-06-09T00:00:00.000Z", { + openPullRequests: 4, + collisionClusters: 1, + }), + ], + }, + ], + }); + const week = trend.weeks.find((entry) => entry.duplicateFlagRatePct !== null); + expect(week?.duplicateFlagRatePct).toBeGreaterThan(0); + expect(week?.slopFlagRatePct).toBe(0); + expect(week?.slopBandLabel).toBe("clean"); + }); + + it("returns an all-null series when there is no snapshot history", () => { + const trend = buildMaintainerSlopDuplicateTrend({ + generatedAt: "2026-06-14T12:00:00.000Z", + repos: [{ repoFullName: "octo/demo", queueHealthSnapshots: [] }], + }); + expect(trend.weeks.every((week) => week.slopFlagRatePct === null && week.duplicateFlagRatePct === null)).toBe( + true, + ); + expect(trend.summary).toContain("No queue-health snapshot history"); + expect(trend.stale).toBe(false); + }); + + it("uses live queue-health for the current week when provided", () => { + const issues = [issue(101), issue(102)]; + const pullRequests = [ + pr(1, { linkedIssues: [101], slopBand: "high", slopRisk: 80 }), + pr(2, { linkedIssues: [102], slopBand: "clean", slopRisk: 0 }), + pr(3, { linkedIssues: [101], slopBand: "elevated", slopRisk: 40 }), + ]; + const collisions = buildCollisionReport("octo/demo", issues, pullRequests); + const queueHealth = buildQueueHealth(repo("octo/demo"), issues, pullRequests, collisions); + const trend = buildMaintainerSlopDuplicateTrend({ + generatedAt: queueHealth.generatedAt, + nowMs: Date.parse(queueHealth.generatedAt), + repos: [{ repoFullName: "octo/demo", currentQueueHealth: queueHealth }], + }); + const latest = trend.weeks.at(-1); + expect(latest?.slopFlagRatePct).toBeGreaterThan(0); + expect(latest?.slopBandLabel).not.toBe("clean"); + expect(JSON.stringify(trend)).not.toMatch(/"slopRisk"/); + expect(queueHealth.signals.slopFlaggedPullRequests).toBe(2); + }); + + it("counts duplicate-flagged PRs only in high-risk clusters with 2+ pull requests", () => { + const issues = [issue(101)]; + const pullRequests = [ + pr(1, { linkedIssues: [101] }), + pr(2, { linkedIssues: [101] }), + pr(3, { linkedIssues: [101] }), + ]; + const collisions = buildCollisionReport("octo/demo", issues, pullRequests); + const queueHealth = buildQueueHealth(repo("octo/demo"), issues, pullRequests, collisions); + expect(queueHealth.signals.duplicateFlaggedPullRequests).toBe(3); + expect(trendPointFromQueueHealth(queueHealth).duplicateFlaggedPullRequests).toBe(3); + }); + + it("ignores snapshots without generatedAt or signals payload", () => { + const trend = buildMaintainerSlopDuplicateTrend({ + generatedAt: "2026-06-14T12:00:00.000Z", + repos: [ + { + repoFullName: "octo/demo", + queueHealthSnapshots: [ + { + id: "bad", + signalType: "queue-health", + targetKey: "octo/demo", + repoFullName: "octo/demo", + payload: {}, + generatedAt: "", + }, + { + id: "malformed", + signalType: "queue-health", + targetKey: "octo/demo", + repoFullName: "octo/demo", + payload: { signals: { openPullRequests: "nope" } }, + generatedAt: "2026-06-09T00:00:00.000Z", + }, + ], + }, + ], + }); + expect(trend.weeks.every((week) => week.slopFlagRatePct === null && week.duplicateFlagRatePct === null)).toBe( + true, + ); + }); + + it("aggregates the latest snapshot per repo within each week bucket", () => { + const trend = buildMaintainerSlopDuplicateTrend({ + generatedAt: "2026-06-14T12:00:00.000Z", + nowMs: Date.parse("2026-06-14T12:00:00.000Z"), + repos: [ + { + repoFullName: "octo/alpha", + queueHealthSnapshots: [ + queueHealthSnapshot("2026-06-09T00:00:00.000Z", { + openPullRequests: 4, + slopFlaggedPullRequests: 1, + duplicateFlaggedPullRequests: 1, + }), + ], + }, + { + repoFullName: "octo/beta", + queueHealthSnapshots: [ + queueHealthSnapshot("2026-06-09T12:00:00.000Z", { + openPullRequests: 6, + slopFlaggedPullRequests: 3, + duplicateFlaggedPullRequests: 2, + }), + ], + }, + ], + }); + const week = trend.weeks.find((entry) => entry.slopFlagRatePct === 40); + expect(week?.duplicateFlagRatePct).toBe(30); + }); + + it("keeps the newest snapshot per repo when an older point is seen later in the week", () => { + const trend = buildMaintainerSlopDuplicateTrend({ + generatedAt: "2026-06-14T12:00:00.000Z", + nowMs: Date.parse("2026-06-14T12:00:00.000Z"), + repos: [ + { + repoFullName: "octo/demo", + queueHealthSnapshots: [ + queueHealthSnapshot("2026-06-09T12:00:00.000Z", { + openPullRequests: 10, + slopFlaggedPullRequests: 5, + duplicateFlaggedPullRequests: 2, + }), + queueHealthSnapshot("2026-06-09T06:00:00.000Z", { + openPullRequests: 2, + slopFlaggedPullRequests: 0, + duplicateFlaggedPullRequests: 0, + }), + ], + }, + ], + }); + const week = trend.weeks.find((entry) => entry.slopFlagRatePct === 50); + expect(week?.duplicateFlagRatePct).toBe(20); + }); + + it("drops snapshots whose payload lacks a parseable signals object", () => { + const trend = buildMaintainerSlopDuplicateTrend({ + generatedAt: "2026-06-14T12:00:00.000Z", + nowMs: Date.parse("2026-06-14T12:00:00.000Z"), + repos: [ + { + repoFullName: "octo/demo", + queueHealthSnapshots: [ + { + id: "missing-signals", + signalType: "queue-health", + targetKey: "octo/demo", + repoFullName: "octo/demo", + payload: {}, + generatedAt: "2026-06-09T00:00:00.000Z", + }, + { + id: "array-signals", + signalType: "queue-health", + targetKey: "octo/demo", + repoFullName: "octo/demo", + payload: { signals: [1, 2, 3] }, + generatedAt: "2026-06-09T00:00:00.000Z", + }, + ], + }, + ], + }); + expect(trend.weeks.every((week) => week.slopFlagRatePct === null && week.duplicateFlagRatePct === null)).toBe( + true, + ); + expect(trend.summary).toContain("No queue-health snapshot history"); + }); + + it("honours explicit stale and custom week windows", () => { + const trend = buildMaintainerSlopDuplicateTrend({ + generatedAt: "2026-06-14T12:00:00.000Z", + stale: true, + weeks: 4, + repos: [{ repoFullName: "octo/demo", queueHealthSnapshots: [] }], + }); + expect(trend.stale).toBe(true); + expect(trend.weeks).toHaveLength(4); + }); +}); + +describe("slopBandLabelFromRate", () => { + it("maps aggregate flag rates to public band labels", () => { + expect(slopBandLabelFromRate(null)).toBeNull(); + expect(slopBandLabelFromRate(0)).toBe("clean"); + expect(slopBandLabelFromRate(10)).toBe("low"); + expect(slopBandLabelFromRate(25)).toBe("elevated"); + expect(slopBandLabelFromRate(40)).toBe("elevated"); + expect(slopBandLabelFromRate(60)).toBe("high"); + expect(slopBandLabelFromRate(75)).toBe("high"); + }); +}); diff --git a/test/unit/open-pr-pressure-scenarios.test.ts b/test/unit/open-pr-pressure-scenarios.test.ts index 49243b156a..2aafc4ff08 100644 --- a/test/unit/open-pr-pressure-scenarios.test.ts +++ b/test/unit/open-pr-pressure-scenarios.test.ts @@ -21,6 +21,8 @@ function queueHealth(level: QueueHealth["level"], overrides: Partial { burdenScore: 0, level: "low", summary: "s", - signals: { openIssues: 0, openPullRequests: 0, unlinkedPullRequests: 0, stalePullRequests: 0, draftPullRequests: 0, maintainerAuthoredPullRequests: 0, collisionClusters: 0, ageBuckets: { under7Days: 0, days7To30: 0, over30Days: 0 }, likelyReviewablePullRequests: 0, cachedOpenPullRequests: 0, likelyReviewablePullRequestsSource: "cache" }, + signals: { openIssues: 0, openPullRequests: 0, unlinkedPullRequests: 0, stalePullRequests: 0, draftPullRequests: 0, maintainerAuthoredPullRequests: 0, collisionClusters: 0, slopFlaggedPullRequests: 0, duplicateFlaggedPullRequests: 0, ageBuckets: { under7Days: 0, days7To30: 0, over30Days: 0 }, likelyReviewablePullRequests: 0, cachedOpenPullRequests: 0, likelyReviewablePullRequestsSource: "cache" }, findings: [], }); expect(emptyQueue.evidence).toContain("0 likely reviewable"); @@ -372,6 +372,8 @@ describe("predicted-gate engine branch coverage (#2283)", () => { draftPullRequests: 0, maintainerAuthoredPullRequests: 0, collisionClusters: 0, + slopFlaggedPullRequests: 0, + duplicateFlaggedPullRequests: 0, ageBuckets: { under7Days: 2, days7To30: 1, over30Days: 0 }, likelyReviewablePullRequests: 2, likelyReviewablePullRequestsSource: undefined, @@ -395,6 +397,8 @@ describe("predicted-gate engine branch coverage (#2283)", () => { draftPullRequests: 0, maintainerAuthoredPullRequests: 0, collisionClusters: 0, + slopFlaggedPullRequests: 0, + duplicateFlaggedPullRequests: 0, ageBuckets: { under7Days: 0, days7To30: 0, over30Days: 0 }, likelyReviewablePullRequests: 0, likelyReviewablePullRequestsSource: "sampled_cache", diff --git a/test/unit/queue-trends.test.ts b/test/unit/queue-trends.test.ts index b6e0c27b03..b48e8d9092 100644 --- a/test/unit/queue-trends.test.ts +++ b/test/unit/queue-trends.test.ts @@ -34,6 +34,8 @@ describe("queue trend windows", () => { draftPullRequests: 0, maintainerAuthoredPullRequests: 1, collisionClusters: 4, + slopFlaggedPullRequests: 0, + duplicateFlaggedPullRequests: 0, ageBuckets: { under7Days: 2, days7To30: 6, over30Days: 4 }, likelyReviewablePullRequests: 3, }, diff --git a/test/unit/repo-policy-readiness.test.ts b/test/unit/repo-policy-readiness.test.ts index c26aeeb76d..0f71d537b5 100644 --- a/test/unit/repo-policy-readiness.test.ts +++ b/test/unit/repo-policy-readiness.test.ts @@ -73,6 +73,8 @@ function queue(overrides: Partial = {}): QueueHealth { draftPullRequests: 0, maintainerAuthoredPullRequests: 0, collisionClusters: 0, + slopFlaggedPullRequests: 0, + duplicateFlaggedPullRequests: 0, ageBuckets: { under7Days: 1, days7To30: 0, over30Days: 0 }, likelyReviewablePullRequests: 1, }, diff --git a/test/unit/scenario-summary.test.ts b/test/unit/scenario-summary.test.ts index db6696a787..89293507be 100644 --- a/test/unit/scenario-summary.test.ts +++ b/test/unit/scenario-summary.test.ts @@ -66,6 +66,8 @@ function queueHealth(level: QueueHealth["level"]): QueueHealth { draftPullRequests: 0, maintainerAuthoredPullRequests: 0, collisionClusters: 0, + slopFlaggedPullRequests: 0, + duplicateFlaggedPullRequests: 0, ageBuckets: { under7Days: 1, days7To30: 0, over30Days: 0 }, likelyReviewablePullRequests: 1, }, diff --git a/test/unit/self-dogfood-registration-pack.test.ts b/test/unit/self-dogfood-registration-pack.test.ts index 9515ff46b6..b73de19fe2 100644 --- a/test/unit/self-dogfood-registration-pack.test.ts +++ b/test/unit/self-dogfood-registration-pack.test.ts @@ -152,6 +152,8 @@ function readinessFixture(overrides: Partial = {}): draftPullRequests: 0, maintainerAuthoredPullRequests: 0, collisionClusters: 0, + slopFlaggedPullRequests: 0, + duplicateFlaggedPullRequests: 0, ageBuckets: { under7Days: 2, days7To30: 3, over30Days: 1 }, likelyReviewablePullRequests: 0, }, diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 09dc5e1e47..b07700822a 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -2530,6 +2530,8 @@ function queueHealthFixture(repoFullName: string, level: QueueHealth["level"]): draftPullRequests: 0, maintainerAuthoredPullRequests: 0, collisionClusters: 0, + slopFlaggedPullRequests: 0, + duplicateFlaggedPullRequests: 0, ageBuckets: { under7Days: 0, days7To30: 0, over30Days: 0 }, likelyReviewablePullRequests: level === "low" ? 1 : level === "medium" ? 3 : level === "high" ? 6 : 10, },