From fb73faf7e73ce62c1aa43f6c9f6dd84899902829 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:30:29 -0700 Subject: [PATCH] feat(services): wire computeFindingAcceptance into the operator dashboard AcceptanceRateCard has shown its "not yet available" empty state since #2197 landed because nothing populated the acceptance field it expects. computeFindingAcceptance (#1967) was fully implemented but only wired into computeStats/StatsPayload, which powers a route the dashboard doesn't read from. Call it from buildOperatorDashboardPayload following the existing Promise.all + fail-safe-aggregate pattern, and map its flagged/addressed/unaddressed/acceptanceRate fields onto the card's windowDays/accepted/total/rate shape. Closes #5213 --- src/services/operator-dashboard.ts | 28 +++++++++++++++++++++++++++- test/unit/operator-dashboard.test.ts | 17 +++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/services/operator-dashboard.ts b/src/services/operator-dashboard.ts index e59c832082..2db327063e 100644 --- a/src/services/operator-dashboard.ts +++ b/src/services/operator-dashboard.ts @@ -29,7 +29,7 @@ import type { import { computeFleetAnalytics, type FleetAnalytics } from "../orb/analytics"; import { computeAgentHealth, computeCalibration, type AgentHealth, type Calibration } from "../review/ops"; import { computeGateEval, type GateEvalReport } from "../review/parity"; -import { computeCycleTimeAggregate, type CycleTimeAggregate } from "../review/stats"; +import { computeCycleTimeAggregate, computeFindingAcceptance, type CycleTimeAggregate } from "../review/stats"; import { loadUpstreamStatus, type UpstreamStatus } from "../upstream/ruleset"; import { nowIso } from "../utils/json"; import { buildRecommendationQualityReport, type RecommendationQualityReport } from "./recommendation-quality-report"; @@ -48,6 +48,17 @@ export type OperatorDashboardNoiseMetric = { spark: number[]; }; +/** Finding acceptance rate (#1967), reshaped for the dashboard's `AcceptanceRateCard` (see + * apps/gittensory-ui/src/components/site/app-panels/acceptance-rate-card.tsx). The card's field names + * (windowDays/accepted/total/rate) intentionally differ from `FindingAcceptanceAggregate`'s + * (flagged/addressed/unaddressed/acceptanceRate) — this is the UI-facing shape, mapped in buildOperatorDashboardPayload. */ +export type OperatorDashboardFindingAcceptance = { + windowDays: number; + accepted: number; + total: number; + rate: number | null; +}; + export type OperatorDashboardPayload = { generatedAt: string; metrics: OperatorDashboardMetric[]; @@ -76,6 +87,9 @@ export type OperatorDashboardPayload = { // Slop-band calibration (#2196): org-wide per-band merge/close rates over resolved PRs carrying a persisted // slop band — is the deterministic slop score predictive? Bands only, never raw scores. Fails safe to empty. slopCalibration: SlopOutcomeCalibration; + // Finding acceptance rate (#1967): share of gate-flagged (hold|close) PRs later merged, reshaped to the + // AcceptanceRateCard's field names. Fails safe to an empty aggregate (rate: null) on any read error. + acceptance: OperatorDashboardFindingAcceptance; }; const USAGE_WINDOW_DAYS = 7; @@ -111,6 +125,7 @@ export async function buildOperatorDashboardPayload( calibration, agentHealth, slopCalibration, + findingAcceptance, ] = await Promise.all([ listRepositories(env), listInstallations(env), @@ -135,6 +150,9 @@ export async function buildOperatorDashboardPayload( computeCalibration(env, operatorAgentConfig(env)), computeAgentHealth(env, operatorAgentConfig(env)), buildOrgSlopCalibration(env), + // #1967: reuse the existing finding-acceptance aggregate (no new compute); fails safe to an empty + // aggregate on any read error. + computeFindingAcceptance(env, { days: GATE_ANALYTICS_WINDOW_DAYS, nowMs: Date.now() }), ]); const weeklyValueReport = buildWeeklyValueReport({ generatedAt: nowIso(), @@ -154,6 +172,13 @@ export async function buildOperatorDashboardPayload( }); const installedRepos = repositories.filter((repo: RepositoryRecord) => repo.isInstalled).length; const registeredRepos = repositories.filter((repo: RepositoryRecord) => repo.isRegistered).length; + // #1967: map FindingAcceptanceAggregate's field names onto the AcceptanceRateCard's expected shape. + const acceptance: OperatorDashboardFindingAcceptance = { + windowDays: GATE_ANALYTICS_WINDOW_DAYS, + accepted: findingAcceptance.addressed, + total: findingAcceptance.flagged, + rate: findingAcceptance.acceptanceRate, + }; return { generatedAt: nowIso(), metrics: [ @@ -239,6 +264,7 @@ export async function buildOperatorDashboardPayload( calibration, agentHealth, slopCalibration, + acceptance, }; } diff --git a/test/unit/operator-dashboard.test.ts b/test/unit/operator-dashboard.test.ts index fa7a83c0db..1f1cb94244 100644 --- a/test/unit/operator-dashboard.test.ts +++ b/test/unit/operator-dashboard.test.ts @@ -61,6 +61,8 @@ describe("operator dashboard payload", () => { overallMergeRate: null, discriminates: null, }); + // #1967/#5213: no review_audit signal → the acceptance card's zero-flagged (null-rate) branch. + expect(payload.acceptance).toEqual({ windowDays: 90, accepted: 0, total: 0, rate: null }); // Empty fleet → instanceCount 0, null precision card ("—"), no-outlier delta. expect(payload.fleetMetrics.instanceCount).toBe(0); expect(payload.metrics).toEqual( @@ -189,6 +191,21 @@ describe("operator dashboard payload", () => { expect(payload.metrics).toEqual(expect.arrayContaining([expect.objectContaining({ label: "Fleet gaming-pattern flags", value: "1", delta: "farmer" })])); }); + it("wires computeFindingAcceptance into the dashboard's acceptance card shape (#1967/#5213)", async () => { + const env = createTestEnv(); + await env.DB.prepare( + `INSERT INTO review_audit (id, project, target_id, event_type, decision, source, created_at) VALUES + ('gd1', 'owner/repo', 'owner/repo#1', 'gate_decision', 'close', 'test', '2026-06-10T10:00:00Z'), + ('po1', 'owner/repo', 'owner/repo#1', 'pr_outcome', 'merged', 'test', '2026-06-10T12:00:00Z'), + ('gd2', 'owner/repo', 'owner/repo#2', 'gate_decision', 'hold', 'test', '2026-06-11T10:00:00Z'), + ('po2', 'owner/repo', 'owner/repo#2', 'pr_outcome', 'closed', 'test', '2026-06-11T12:00:00Z')`, + ).run(); + const payload = await buildOperatorDashboardPayload(env); + // 2 flagged (hold|close), 1 addressed (merged) → mapped to the card's windowDays/accepted/total/rate shape, + // not the raw aggregate's flagged/addressed/unaddressed/acceptanceRate field names. + expect(payload.acceptance).toEqual({ windowDays: 90, accepted: 1, total: 2, rate: 0.5 }); + }); + it("clamps unsupported window values to the default 7d lookback (#2199)", () => { expect(clampOperatorDashboardWindowDays(30)).toBe(30); expect(clampOperatorDashboardWindowDays(14)).toBe(7);