Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Reversal-rate + auto-action health analytics card model (#2193). UI-only display slice: the card consumes an
// agent-health shape assumed present on the operator-dashboard payload (backend computation is computeAgentHealth
// in src/review/ops.ts). Types + the pure rate/band helpers live here (not in the .tsx) so the component file
// exports only components (react-refresh/only-export-components).

import type { Status } from "@/components/site/control-primitives";

/** A bot auto-action a human overrode (a revert of a bot-merge / a reopen of a bot-close). Mirrors
* src/review/ops.ts ReversedTarget. Public-safe: PR number + repo + status, no scores/rewards. */
export interface ReversedTarget {
number: number;
repo: string;
status: string;
eventType: string;
}

/** The agent-health slice delivered on the operator-dashboard payload: how often humans reopened/reverted a bot
* auto-action over a rolling window. Public-safe counts only (mirrors the reversal fields of
* src/review/ops.ts AgentHealth). */
export interface ReversalHealthReport {
/** Bot auto-actions a human overrode in the window. */
reversals: number;
/** reversals / recentAutoActions; 0 when no auto-actions were taken. */
reversalRate: number;
/** Share of terminal targets that took the manual (human) path rather than an auto-action. */
manualRate: number;
/** Auto-actions taken in the window — the reversal-rate denominator. */
recentAutoActions: number;
/** The specific overridden targets, for the detail list. */
reversedTargets: ReversedTarget[];
/** Rolling measurement window, in days. */
windowDays: number;
}

/** The card's derived view: the raw counts plus display percentages (a null rate when nothing was auto-actioned). */
export interface ReversalHealthSummary {
reversals: number;
/** reversalRate as a percentage; null when the denominator (recentAutoActions) is 0 — nothing to be a rate of. */
reversalRatePct: number | null;
manualRatePct: number;
recentAutoActions: number;
reversedCount: number;
}

/** Pure fold: derive the display summary from the raw health counts. An empty denominator yields a null rate. */
export function summarizeReversalHealth(report: ReversalHealthReport): ReversalHealthSummary {
return {
reversals: report.reversals,
reversalRatePct: report.recentAutoActions > 0 ? report.reversalRate * 100 : null,
manualRatePct: report.manualRate * 100,
recentAutoActions: report.recentAutoActions,
reversedCount: report.reversedTargets.length,
};
}

/** StatusPill quality band for reversal health: no auto-actions yet is informational (no signal); zero reversals
* over real auto-actions reads healthy; a reversal rate at or under 10% warns; above that blocks (humans are
* frequently overriding the bot). Mirrors the Status vocabulary in control-primitives.ts. */
export function bandForReversalHealth(report: ReversalHealthReport): Status {
if (report.recentAutoActions === 0) return "info";
if (report.reversals === 0) return "ready";
return report.reversalRate <= 0.1 ? "warn" : "blocked";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";

import { ReversalHealthCard } from "@/components/site/app-panels/reversal-health-card";
import {
bandForReversalHealth,
summarizeReversalHealth,
} from "@/components/site/app-panels/reversal-health-card-model";

const base = {
reversals: 0,
reversalRate: 0,
manualRate: 0.2,
recentAutoActions: 10,
reversedTargets: [],
windowDays: 30,
};

describe("summarizeReversalHealth", () => {
it("derives the percentages and the reversed-target count", () => {
expect(
summarizeReversalHealth({
...base,
reversals: 2,
reversalRate: 0.2,
reversedTargets: [{ number: 1, repo: "o/r", status: "reverted", eventType: "auto-merge" }],
}),
).toEqual({
reversals: 2,
reversalRatePct: 20,
manualRatePct: 20,
recentAutoActions: 10,
reversedCount: 1,
});
});

it("returns a null rate for an empty denominator (no auto-actions)", () => {
expect(summarizeReversalHealth({ ...base, recentAutoActions: 0 }).reversalRatePct).toBeNull();
});
});

describe("bandForReversalHealth", () => {
it("bands: no auto-actions info, zero reversals ready, <=10% warn, above blocked", () => {
expect(bandForReversalHealth({ ...base, recentAutoActions: 0 })).toBe("info");
expect(bandForReversalHealth({ ...base, reversals: 0, recentAutoActions: 10 })).toBe("ready");
expect(
bandForReversalHealth({ ...base, reversals: 1, reversalRate: 0.1, recentAutoActions: 10 }),
).toBe("warn");
expect(
bandForReversalHealth({ ...base, reversals: 5, reversalRate: 0.5, recentAutoActions: 10 }),
).toBe("blocked");
});
});

describe("ReversalHealthCard", () => {
it("renders the reversal rate, counts, and the reversed-target list when present", () => {
render(
<ReversalHealthCard
health={{
...base,
reversals: 2,
reversalRate: 0.2,
manualRate: 0.35, // distinct from the 20% reversal rate so each percentage is unambiguous
reversedTargets: [
{ number: 7, repo: "o/r", status: "reverted", eventType: "auto-merge" },
],
}}
/>,
);
expect(screen.getByText("Auto-action reversal health")).toBeTruthy();
expect(screen.getByText("20%")).toBeTruthy();
expect(screen.getByText("35%")).toBeTruthy();
expect(screen.getByText("30-day window")).toBeTruthy();
expect(screen.getByText("o/r#7 — auto-merge (reverted)")).toBeTruthy();
});

it("renders '—' for the rate and an empty list state when there are no auto-actions", () => {
render(<ReversalHealthCard health={{ ...base, recentAutoActions: 0 }} />);
expect(screen.getByText("—")).toBeTruthy();
expect(screen.getByText("No reversed auto-actions")).toBeTruthy();
});

it("renders a graceful EmptyState when the health field is absent", () => {
render(<ReversalHealthCard health={undefined} />);
expect(screen.getByText("Auto-action health not yet available")).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Stat, StatusPill } from "@/components/site/control-primitives";
import { EmptyState } from "@/components/site/state-views";

import {
bandForReversalHealth,
summarizeReversalHealth,
type ReversalHealthReport,
} from "./reversal-health-card-model";

/** Self-host analytics card (#2193): auto-action reversal health — how often a human reopened or reverted a bot
* auto-action (computeAgentHealth, src/review/ops.ts). Reversal rate as a percentage plus reversals / manual-rate
* counts and the specific overridden targets. UI-only display slice; the health shape is assumed present on the
* operator-dashboard payload (backend computation is #1967), so absence renders a graceful "not yet available"
* EmptyState. */
export function ReversalHealthCard({ health }: { health?: ReversalHealthReport }) {
if (!health) {
return (
<EmptyState
title="Auto-action health not yet available"
description="This appears once agent-health data is present on the dashboard payload."
/>
);
}
const summary = summarizeReversalHealth(health);
return (
<section className="rounded-token border border-border bg-transparent p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="font-display text-token-lg font-semibold">Auto-action reversal health</h2>
<p className="mt-1 text-token-xs text-muted-foreground">
How often a human reopened or reverted a bot auto-action. Public-safe counts only.
</p>
</div>
<StatusPill
status={bandForReversalHealth(health)}
>{`${health.windowDays}-day window`}</StatusPill>
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-3">
<Stat
label="Reversal rate"
value={summary.reversalRatePct !== null ? `${Math.round(summary.reversalRatePct)}%` : "—"}
hint={<span className="text-muted-foreground">reversals / auto-actions</span>}
/>
<Stat
label="Reversals"
value={String(summary.reversals)}
hint={<span className="text-muted-foreground">auto-actions a human overrode</span>}
/>
<Stat
label="Manual rate"
value={`${Math.round(summary.manualRatePct)}%`}
hint={<span className="text-muted-foreground">terminal targets taken manually</span>}
/>
</div>
{summary.reversedCount > 0 ? (
<ul className="mt-4 space-y-1">
{health.reversedTargets.map((target) => (
<li
key={`${target.repo}#${target.number}`}
className="text-token-xs text-muted-foreground"
>
{`${target.repo}#${target.number} — ${target.eventType} (${target.status})`}
</li>
))}
</ul>
) : (
<div className="mt-4">
<EmptyState
title="No reversed auto-actions"
description="No bot auto-action was reopened or reverted in this window."
/>
</div>
)}
</section>
);
}
5 changes: 5 additions & 0 deletions apps/gittensory-ui/src/routes/app.analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import type { GateEvalReport } from "@/components/site/app-panels/gate-precision-card-model";
import { CycleTimeCard } from "@/components/site/app-panels/cycle-time-card";
import type { CycleTimeAggregate } from "@/components/site/app-panels/cycle-time-card-model";
import { ReversalHealthCard } from "@/components/site/app-panels/reversal-health-card";
import type { ReversalHealthReport } from "@/components/site/app-panels/reversal-health-card-model";
import { useApiResource } from "@/lib/api/use-api-resource";

export const Route = createFileRoute("/app/analytics")({
Expand Down Expand Up @@ -104,9 +106,10 @@
upstreamDrift?: { status?: string; openReportCount?: number } | null;
gateEval?: GateEvalReport;
cycleTime?: CycleTimeAggregate;
agentHealth?: ReversalHealthReport;
};

function ProductAnalytics() {

Check warning on line 112 in apps/gittensory-ui/src/routes/app.analytics.tsx

View workflow job for this annotation

GitHub Actions / validate-code

Fast refresh only works when a file only exports components. Move your component(s) to a separate file. If all exports are HOCs, add them to the `extraHOCs` option
const dashboard = useApiResource<OperatorDashboard>(
"/v1/app/operator-dashboard",
"Product analytics",
Expand Down Expand Up @@ -188,6 +191,8 @@

{data.cycleTime ? <CycleTimeCard cycleTime={data.cycleTime} /> : null}

{data.agentHealth ? <ReversalHealthCard health={data.agentHealth} /> : null}

{data.usageSummary ? (
<ProductUsageBreakdownPanel
byEvent={data.usageSummary.byEvent}
Expand Down
Loading