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,52 @@
// Slop-band calibration analytics card model (#2196). UI-only display slice: the card consumes a slop-band
// calibration shape assumed present on the operator-dashboard payload (predicted slop band vs. realized
// merge/close outcome, from the stats feed — src/review/stats.ts). Types + the pure ordering/format helpers live
// here (not in the .tsx) so the component file exports only components (react-refresh/only-export-components).
// Public/private boundary: bands only — never a raw credibility score.

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

/** The predicted slop bands, cleanest → highest, in display order. */
export type SlopBand = "clean" | "low" | "elevated" | "high";

/** One band's calibration row: how many PRs the gate predicted in this band and how they actually resolved. */
export interface SlopBandCalibrationRow {
band: SlopBand;
/** PRs the gate predicted in this band over the window. */
predictedCount: number;
/** Realized merge rate (0..1): share of this band's PRs that ended up merged — the calibration signal. */
realizedMergeRate: number;
/** Per-bucket outcome counts for the MiniSparkbar. */
distribution: number[];
}

/** The slop-band calibration slice delivered on the operator-dashboard payload. Public-safe bands only. */
export interface SlopBandCalibrationReport {
rows: SlopBandCalibrationRow[];
/** Rolling measurement window, in days. */
windowDays: number;
}

const BAND_ORDER: readonly SlopBand[] = ["clean", "low", "elevated", "high"];

const BAND_TONE: Record<SlopBand, Status> = {
clean: "ok",
low: "info",
elevated: "warn",
high: "blocked",
};

/** Order rows cleanest → highest band, ignoring the payload's arrival order. Pure (returns a new array). */
export function orderSlopBands(rows: readonly SlopBandCalibrationRow[]): SlopBandCalibrationRow[] {
return [...rows].sort((a, b) => BAND_ORDER.indexOf(a.band) - BAND_ORDER.indexOf(b.band));
}

/** StatusPill tone for a band — mirrors the slop-band tone vocabulary used across the maintainer dashboard. */
export function toneForSlopBand(band: SlopBand): Status {
return BAND_TONE[band];
}

/** Format a 0..1 merge rate as a whole-number percentage. */
export function formatMergeRate(rate: number): string {
return `${Math.round(rate * 100)}%`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";

import { SlopBandCalibrationCard } from "@/components/site/app-panels/slop-band-calibration-card";
import {
formatMergeRate,
orderSlopBands,
toneForSlopBand,
type SlopBandCalibrationRow,
} from "@/components/site/app-panels/slop-band-calibration-card-model";

const rows: SlopBandCalibrationRow[] = [
{ band: "high", predictedCount: 4, realizedMergeRate: 0.1, distribution: [1, 1, 2] },
{ band: "clean", predictedCount: 40, realizedMergeRate: 0.95, distribution: [10, 12, 18] },
{ band: "elevated", predictedCount: 9, realizedMergeRate: 0.45, distribution: [3, 3, 3] },
{ band: "low", predictedCount: 22, realizedMergeRate: 0.8, distribution: [7, 8, 7] },
];

describe("slop-band model", () => {
it("orders bands cleanest → highest regardless of the payload's arrival order", () => {
expect(orderSlopBands(rows).map((r) => r.band)).toEqual(["clean", "low", "elevated", "high"]);
});

it("maps bands to tones and formats the merge rate as a percentage", () => {
expect(toneForSlopBand("clean")).toBe("ok");
expect(toneForSlopBand("high")).toBe("blocked");
expect(formatMergeRate(0.95)).toBe("95%");
});
});

describe("SlopBandCalibrationCard", () => {
it("renders a row per band with predicted counts and realized rates (all-bands-present arm)", () => {
render(<SlopBandCalibrationCard report={{ rows, windowDays: 30 }} />);
expect(screen.getByText("Slop-band calibration")).toBeTruthy();
expect(screen.getByText("30-day window")).toBeTruthy();
expect(screen.getByText("clean")).toBeTruthy();
expect(screen.getByText("high")).toBeTruthy();
expect(screen.getByText("95%")).toBeTruthy();
expect(screen.getByText("40")).toBeTruthy();
});

it("renders a band with zero predicted PRs (one-empty-band case)", () => {
render(
<SlopBandCalibrationCard
report={{
rows: [{ band: "high", predictedCount: 0, realizedMergeRate: 0, distribution: [] }],
windowDays: 7,
}}
/>,
);
expect(screen.getByText("high")).toBeTruthy();
expect(screen.getByText("0")).toBeTruthy();
expect(screen.getByText("7-day window")).toBeTruthy();
});

it("renders a graceful EmptyState when there is no data (no-data arm)", () => {
render(<SlopBandCalibrationCard report={{ rows: [], windowDays: 30 }} />);
expect(screen.getByText("Slop-band calibration not yet available")).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { MiniSparkbar, Stat, StatusPill } from "@/components/site/control-primitives";
import { EmptyState } from "@/components/site/state-views";

import {
formatMergeRate,
orderSlopBands,
toneForSlopBand,
type SlopBandCalibrationReport,
} from "./slop-band-calibration-card-model";

/** Self-host analytics card (#2196): slop-band calibration — for each predicted slop band, how many PRs the gate
* put there and how they actually resolved (realized merge rate), so a maintainer can see whether the slop
* predictor is well-calibrated (cleaner bands should merge more, higher bands less). Public-safe bands only — no
* raw credibility scores. UI-only display slice; the calibration shape is assumed present on the operator-
* dashboard payload, so absence renders a graceful "not yet available" EmptyState. */
export function SlopBandCalibrationCard({ report }: { report?: SlopBandCalibrationReport }) {
if (!report || report.rows.length === 0) {
return (
<EmptyState
title="Slop-band calibration not yet available"
description="This appears once slop-band calibration data is present on the dashboard payload."
/>
);
}
const rows = orderSlopBands(report.rows);
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">Slop-band calibration</h2>
<p className="mt-1 text-token-xs text-muted-foreground">
Predicted slop band vs. realized merge outcome. Public-safe bands only — no raw scores.
</p>
</div>
<StatusPill status="info">{`${report.windowDays}-day window`}</StatusPill>
</div>
<div className="mt-4 space-y-3">
{rows.map((row) => (
<div
key={row.band}
className="flex flex-wrap items-center justify-between gap-3 rounded-token border border-border/60 p-3"
>
<StatusPill status={toneForSlopBand(row.band)}>{row.band}</StatusPill>
<Stat
label="Predicted"
value={String(row.predictedCount)}
hint={<span className="text-muted-foreground">PRs in band</span>}
/>
<Stat
label="Realized merge rate"
value={formatMergeRate(row.realizedMergeRate)}
hint={<span className="text-muted-foreground">of this band merged</span>}
/>
<MiniSparkbar values={row.distribution} />
</div>
))}
</div>
</section>
);
}
7 changes: 7 additions & 0 deletions apps/gittensory-ui/src/routes/app.analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
FindingsBreakdownCard,
type FindingsBreakdown,
} from "@/components/site/app-panels/findings-breakdown-card";
import { SlopBandCalibrationCard } from "@/components/site/app-panels/slop-band-calibration-card";
import type { SlopBandCalibrationReport } from "@/components/site/app-panels/slop-band-calibration-card-model";
import { useApiResource } from "@/lib/api/use-api-resource";
import { exportOperatorDashboardCsv } from "@/lib/csv-export";

Expand Down Expand Up @@ -124,6 +126,7 @@ type OperatorDashboard = {
agentHealth?: ReversalHealth;
acceptance?: FindingAcceptance;
findingsBreakdown?: FindingsBreakdown;
slopBandCalibration?: SlopBandCalibrationReport;
};

function ProductAnalytics() {
Expand Down Expand Up @@ -232,6 +235,10 @@ function ProductAnalytics() {

<FindingsBreakdownCard findings={data.findingsBreakdown} />

{data.slopBandCalibration ? (
<SlopBandCalibrationCard report={data.slopBandCalibration} />
) : null}

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