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
85 changes: 85 additions & 0 deletions src/services/maintainer-recap-gate-outcomes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Maintainer-recap GATE-OUTCOMES section (#2242, content slice of the #1963 recap digest).
//
// Pure section builder over a RecapReport projection: summarize the gate's window — how many PRs the
// gate blocked, how many maintainers OVERRODE, and the blocked-then-merged FALSE-POSITIVE count + rate —
// straight from the same GatePrecisionReport totals that services/gate-precision.ts aggregates (the source
// src/review/ops-wire.ts reads). No delivery, no scheduling, no new queries.
//
// The false-positive rate is NULLED below MIN_SAMPLE exactly as gate-precision.ts:103 — a 1-of-1 "false
// positive" is noise, not a precision signal. Own file (mirroring maintainer-recap-calibration.ts) so it
// stays decoupled from the foundation builder and sibling sections (zero shared-file conflict surface).
import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN } from "../signals/redaction";

// Mirror gate-precision.ts:22 — the rate is noise below this many blocks, so it reports as null (n/a).
const MIN_SAMPLE = 5;

/** Mirror gate-precision.ts:41 round() — three decimal places. */
function round(value: number): number {
return Math.round(value * 1000) / 1000;
}

/** Projection of RecapReport used by the gate-outcomes section (window + gate totals only). */
export type GateOutcomesRecapSource = {
windowDays: number;
totals: {
/** Total gate blocks over the window (the rate denominator). */
blocked: number;
/** Blocks that later MERGED anyway — a gate FALSE POSITIVE. */
gateFalsePositives: number;
/** Blocks a maintainer explicitly OVERRODE. */
gateOverrides: number;
};
};

/** One titled digest section: structured fields for consumers + ready-to-emit lines for the formatter. */
export type GateOutcomesRecapSection = {
title: string;
blocked: number;
overridden: number;
falsePositives: number;
/** blockedThenMerged / blocked, 3 dp — NULL below MIN_SAMPLE (gate-precision.ts:103). */
falsePositiveRate: number | null;
lines: string[];
};

/** Public-safe scrub for free text pulled into the section (defense in depth — counts are the only inputs
* today). Mirrors maintainer-recap-calibration.ts. */
function sanitizeRecapText(value: string): string {
return value.replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, "<redacted-path>").slice(0, 240);
}

/**
* Pure gate-outcomes section over a RecapReport projection.
*
* - `falsePositiveRate` = gateFalsePositives / blocked, rounded to 3 dp — but **null below MIN_SAMPLE**
* (and therefore also when nothing was blocked), exactly as gate-precision.ts nulls a low-sample rate.
* - The rate line reads "n/a" on the null arm so the digest still carries a gate-outcomes section.
*/
export function buildGateOutcomesRecapSection(report: GateOutcomesRecapSource): GateOutcomesRecapSection {
const { blocked, gateFalsePositives, gateOverrides } = report.totals;
// Null the rate below MIN_SAMPLE (gate-precision.ts:103) — a 1-of-1 "false positive" is noise. This also
// covers the divide-by-zero arm (blocked === 0 < MIN_SAMPLE), so the ratio is never evaluated at 0.
const falsePositiveRate = blocked >= MIN_SAMPLE ? round(gateFalsePositives / blocked) : null;

const rateLine =
falsePositiveRate === null
? `False-positive rate: n/a (fewer than ${MIN_SAMPLE} blocks in the last ${report.windowDays} day(s))`
: `False-positive rate: ${Math.round(falsePositiveRate * 100)}% (${gateFalsePositives} of ${blocked} blocks merged anyway)`;

const title = "Gate outcomes";
const lines = [
`Blocked: ${blocked}`,
`Maintainer overrides: ${gateOverrides}`,
`False positives (blocked then merged): ${gateFalsePositives}`,
rateLine,
].map(sanitizeRecapText);

return {
title,
blocked,
overridden: gateOverrides,
falsePositives: gateFalsePositives,
falsePositiveRate,
lines,
};
}
70 changes: 70 additions & 0 deletions test/unit/maintainer-recap-gate-outcomes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import {
buildGateOutcomesRecapSection,
type GateOutcomesRecapSource,
} from "../../src/services/maintainer-recap-gate-outcomes";

const WINDOW = 7;

function report(totals: GateOutcomesRecapSource["totals"], windowDays = WINDOW): GateOutcomesRecapSource {
return { windowDays, totals };
}

describe("buildGateOutcomesRecapSection (#2242)", () => {
it("emits counts + a numeric rate when blocks clear MIN_SAMPLE (enough-samples arm)", () => {
// 10 blocks ≥ MIN_SAMPLE(5), 3 merged anyway ⇒ rate 0.3.
const section = buildGateOutcomesRecapSection(report({ blocked: 10, gateFalsePositives: 3, gateOverrides: 2 }));
expect(section.title).toBe("Gate outcomes");
expect(section.blocked).toBe(10);
expect(section.overridden).toBe(2);
expect(section.falsePositives).toBe(3);
expect(section.falsePositiveRate).toBe(0.3);
expect(section.lines).toEqual([
"Blocked: 10",
"Maintainer overrides: 2",
"False positives (blocked then merged): 3",
"False-positive rate: 30% (3 of 10 blocks merged anyway)",
]);
});

it("nulls the rate below MIN_SAMPLE (too-few-samples arm) — a 1-of-few FP is noise, not a signal", () => {
// 4 blocks < MIN_SAMPLE(5) ⇒ rate null even though a block merged anyway.
const section = buildGateOutcomesRecapSection(report({ blocked: 4, gateFalsePositives: 1, gateOverrides: 1 }));
expect(section.falsePositiveRate).toBeNull();
expect(section.falsePositives).toBe(1);
expect(section.lines[3]).toBe("False-positive rate: n/a (fewer than 5 blocks in the last 7 day(s))");
});

it("treats exactly MIN_SAMPLE blocks as enough (boundary — blocked === 5)", () => {
const section = buildGateOutcomesRecapSection(report({ blocked: 5, gateFalsePositives: 1, gateOverrides: 0 }));
expect(section.falsePositiveRate).toBe(0.2); // 1/5, ≥ MIN_SAMPLE ⇒ numeric
expect(section.lines[3]).toBe("False-positive rate: 20% (1 of 5 blocks merged anyway)");
});

it("nulls the rate on an empty report without dividing by zero (blocked === 0 arm)", () => {
const section = buildGateOutcomesRecapSection(report({ blocked: 0, gateFalsePositives: 0, gateOverrides: 0 }));
expect(section.falsePositiveRate).toBeNull();
expect(Number.isNaN(section.falsePositiveRate as number)).toBe(false);
expect(section.lines).toEqual([
"Blocked: 0",
"Maintainer overrides: 0",
"False positives (blocked then merged): 0",
"False-positive rate: n/a (fewer than 5 blocks in the last 7 day(s))",
]);
});

it("rounds the rate to three decimals like gate-precision.ts (percent via Math.round)", () => {
// 1/6 = 0.16666… ⇒ round(*1000)/1000 = 0.167; percent line ⇒ Math.round(16.7) = 17%.
const section = buildGateOutcomesRecapSection(report({ blocked: 6, gateFalsePositives: 1, gateOverrides: 0 }));
expect(section.falsePositiveRate).toBe(0.167);
expect(section.lines[3]).toBe("False-positive rate: 17% (1 of 6 blocks merged anyway)");
});

it("scrubs a local-path leak from every emitted line (defense-in-depth — lines are count-derived today)", () => {
const section = buildGateOutcomesRecapSection(report({ blocked: 7, gateFalsePositives: 0, gateOverrides: 0 }));
for (const line of section.lines) {
expect(line).not.toMatch(/\/Users\//);
expect(line).not.toMatch(/\/tmp\//);
}
});
});