Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/services/operator-dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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[];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -111,6 +125,7 @@ export async function buildOperatorDashboardPayload(
calibration,
agentHealth,
slopCalibration,
findingAcceptance,
] = await Promise.all([
listRepositories(env),
listInstallations(env),
Expand All @@ -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(),
Expand All @@ -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: [
Expand Down Expand Up @@ -239,6 +264,7 @@ export async function buildOperatorDashboardPayload(
calibration,
agentHealth,
slopCalibration,
acceptance,
};
}

Expand Down
17 changes: 17 additions & 0 deletions test/unit/operator-dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down