Skip to content

Commit 292e275

Browse files
authored
feat(services): wire computeFindingAcceptance into the operator dashboard (#5684)
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
1 parent 41393da commit 292e275

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

src/services/operator-dashboard.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import type {
2929
import { computeFleetAnalytics, type FleetAnalytics } from "../orb/analytics";
3030
import { computeAgentHealth, computeCalibration, type AgentHealth, type Calibration } from "../review/ops";
3131
import { computeGateEval, type GateEvalReport } from "../review/parity";
32-
import { computeCycleTimeAggregate, type CycleTimeAggregate } from "../review/stats";
32+
import { computeCycleTimeAggregate, computeFindingAcceptance, type CycleTimeAggregate } from "../review/stats";
3333
import { loadUpstreamStatus, type UpstreamStatus } from "../upstream/ruleset";
3434
import { nowIso } from "../utils/json";
3535
import { buildRecommendationQualityReport, type RecommendationQualityReport } from "./recommendation-quality-report";
@@ -48,6 +48,17 @@ export type OperatorDashboardNoiseMetric = {
4848
spark: number[];
4949
};
5050

51+
/** Finding acceptance rate (#1967), reshaped for the dashboard's `AcceptanceRateCard` (see
52+
* apps/gittensory-ui/src/components/site/app-panels/acceptance-rate-card.tsx). The card's field names
53+
* (windowDays/accepted/total/rate) intentionally differ from `FindingAcceptanceAggregate`'s
54+
* (flagged/addressed/unaddressed/acceptanceRate) — this is the UI-facing shape, mapped in buildOperatorDashboardPayload. */
55+
export type OperatorDashboardFindingAcceptance = {
56+
windowDays: number;
57+
accepted: number;
58+
total: number;
59+
rate: number | null;
60+
};
61+
5162
export type OperatorDashboardPayload = {
5263
generatedAt: string;
5364
metrics: OperatorDashboardMetric[];
@@ -76,6 +87,9 @@ export type OperatorDashboardPayload = {
7687
// Slop-band calibration (#2196): org-wide per-band merge/close rates over resolved PRs carrying a persisted
7788
// slop band — is the deterministic slop score predictive? Bands only, never raw scores. Fails safe to empty.
7889
slopCalibration: SlopOutcomeCalibration;
90+
// Finding acceptance rate (#1967): share of gate-flagged (hold|close) PRs later merged, reshaped to the
91+
// AcceptanceRateCard's field names. Fails safe to an empty aggregate (rate: null) on any read error.
92+
acceptance: OperatorDashboardFindingAcceptance;
7993
};
8094

8195
const USAGE_WINDOW_DAYS = 7;
@@ -111,6 +125,7 @@ export async function buildOperatorDashboardPayload(
111125
calibration,
112126
agentHealth,
113127
slopCalibration,
128+
findingAcceptance,
114129
] = await Promise.all([
115130
listRepositories(env),
116131
listInstallations(env),
@@ -135,6 +150,9 @@ export async function buildOperatorDashboardPayload(
135150
computeCalibration(env, operatorAgentConfig(env)),
136151
computeAgentHealth(env, operatorAgentConfig(env)),
137152
buildOrgSlopCalibration(env),
153+
// #1967: reuse the existing finding-acceptance aggregate (no new compute); fails safe to an empty
154+
// aggregate on any read error.
155+
computeFindingAcceptance(env, { days: GATE_ANALYTICS_WINDOW_DAYS, nowMs: Date.now() }),
138156
]);
139157
const weeklyValueReport = buildWeeklyValueReport({
140158
generatedAt: nowIso(),
@@ -154,6 +172,13 @@ export async function buildOperatorDashboardPayload(
154172
});
155173
const installedRepos = repositories.filter((repo: RepositoryRecord) => repo.isInstalled).length;
156174
const registeredRepos = repositories.filter((repo: RepositoryRecord) => repo.isRegistered).length;
175+
// #1967: map FindingAcceptanceAggregate's field names onto the AcceptanceRateCard's expected shape.
176+
const acceptance: OperatorDashboardFindingAcceptance = {
177+
windowDays: GATE_ANALYTICS_WINDOW_DAYS,
178+
accepted: findingAcceptance.addressed,
179+
total: findingAcceptance.flagged,
180+
rate: findingAcceptance.acceptanceRate,
181+
};
157182
return {
158183
generatedAt: nowIso(),
159184
metrics: [
@@ -239,6 +264,7 @@ export async function buildOperatorDashboardPayload(
239264
calibration,
240265
agentHealth,
241266
slopCalibration,
267+
acceptance,
242268
};
243269
}
244270

test/unit/operator-dashboard.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ describe("operator dashboard payload", () => {
6161
overallMergeRate: null,
6262
discriminates: null,
6363
});
64+
// #1967/#5213: no review_audit signal → the acceptance card's zero-flagged (null-rate) branch.
65+
expect(payload.acceptance).toEqual({ windowDays: 90, accepted: 0, total: 0, rate: null });
6466
// Empty fleet → instanceCount 0, null precision card ("—"), no-outlier delta.
6567
expect(payload.fleetMetrics.instanceCount).toBe(0);
6668
expect(payload.metrics).toEqual(
@@ -189,6 +191,21 @@ describe("operator dashboard payload", () => {
189191
expect(payload.metrics).toEqual(expect.arrayContaining([expect.objectContaining({ label: "Fleet gaming-pattern flags", value: "1", delta: "farmer" })]));
190192
});
191193

194+
it("wires computeFindingAcceptance into the dashboard's acceptance card shape (#1967/#5213)", async () => {
195+
const env = createTestEnv();
196+
await env.DB.prepare(
197+
`INSERT INTO review_audit (id, project, target_id, event_type, decision, source, created_at) VALUES
198+
('gd1', 'owner/repo', 'owner/repo#1', 'gate_decision', 'close', 'test', '2026-06-10T10:00:00Z'),
199+
('po1', 'owner/repo', 'owner/repo#1', 'pr_outcome', 'merged', 'test', '2026-06-10T12:00:00Z'),
200+
('gd2', 'owner/repo', 'owner/repo#2', 'gate_decision', 'hold', 'test', '2026-06-11T10:00:00Z'),
201+
('po2', 'owner/repo', 'owner/repo#2', 'pr_outcome', 'closed', 'test', '2026-06-11T12:00:00Z')`,
202+
).run();
203+
const payload = await buildOperatorDashboardPayload(env);
204+
// 2 flagged (hold|close), 1 addressed (merged) → mapped to the card's windowDays/accepted/total/rate shape,
205+
// not the raw aggregate's flagged/addressed/unaddressed/acceptanceRate field names.
206+
expect(payload.acceptance).toEqual({ windowDays: 90, accepted: 1, total: 2, rate: 0.5 });
207+
});
208+
192209
it("clamps unsupported window values to the default 7d lookback (#2199)", () => {
193210
expect(clampOperatorDashboardWindowDays(30)).toBe(30);
194211
expect(clampOperatorDashboardWindowDays(14)).toBe(7);

0 commit comments

Comments
 (0)