feat(FN-4608): complete Step 2 — handle empty-sample reliability days
Fusion-Task-Id: FN-4608 Fusion-Task-Lineage: 6cabc509-38a1-4cb8-be6f-51c53d5753ff
This commit is contained in:
@@ -4,6 +4,7 @@ import type { ActivityLogEntry, RunAuditEvent } from "@fusion/core";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
bucketByDay,
|
bucketByDay,
|
||||||
|
dayHasSamples,
|
||||||
fileScopeInvariantFailuresPerDay,
|
fileScopeInvariantFailuresPerDay,
|
||||||
inReviewDurationMetrics,
|
inReviewDurationMetrics,
|
||||||
inReviewFailureRate7d,
|
inReviewFailureRate7d,
|
||||||
@@ -136,4 +137,52 @@ describe("reliability-metrics", () => {
|
|||||||
expect(inReviewFailureRate7d({ "2026-05-13": 10 }, { "2026-05-13": 2 }, endMs)).toEqual({ value: 0.2 });
|
expect(inReviewFailureRate7d({ "2026-05-13": 10 }, { "2026-05-13": 2 }, endMs)).toEqual({ value: 0.2 });
|
||||||
expect(inReviewFailureRate7d({}, {}, endMs)).toEqual({ value: null, reason: "no-in-review-entries" });
|
expect(inReviewFailureRate7d({}, {}, endMs)).toEqual({ value: null, reason: "no-in-review-entries" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns no-in-review-entries when all seven days are empty", () => {
|
||||||
|
const endMs = Date.parse("2026-05-13T00:00:00.000Z");
|
||||||
|
expect(inReviewFailureRate7d({ "2026-05-13": 0, "2026-05-12": 0 }, { "2026-05-13": 0 }, endMs)).toEqual({
|
||||||
|
value: null,
|
||||||
|
reason: "no-in-review-entries",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters task movement counts by start/end window", () => {
|
||||||
|
const activity: ActivityLogEntry[] = [
|
||||||
|
moved("2026-05-10T23:59:59.000Z", "FN-1", "todo", "in-review"),
|
||||||
|
moved("2026-05-11T00:00:00.000Z", "FN-2", "todo", "in-review"),
|
||||||
|
moved("2026-05-12T00:00:00.000Z", "FN-3", "in-review", "in-progress"),
|
||||||
|
];
|
||||||
|
|
||||||
|
const start = Date.parse("2026-05-11T00:00:00.000Z");
|
||||||
|
const end = Date.parse("2026-05-12T00:00:00.000Z");
|
||||||
|
|
||||||
|
expect(tasksEnteredInReviewPerDay(activity, start, end)).toEqual({ "2026-05-11": 1 });
|
||||||
|
expect(tasksBouncedToInProgressPerDay(activity, start, end)).toEqual({ "2026-05-12": 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports hasSamples semantics for per-day rows", () => {
|
||||||
|
expect(dayHasSamples({
|
||||||
|
tasksEnteredInReview: 0,
|
||||||
|
tasksBouncedToInProgress: 0,
|
||||||
|
postMergeAuditFailures: null,
|
||||||
|
fileScopeInvariantFailures: null,
|
||||||
|
recoverAlreadyMergedReviewTasksRecoveries: null,
|
||||||
|
})).toBe(false);
|
||||||
|
|
||||||
|
expect(dayHasSamples({
|
||||||
|
tasksEnteredInReview: 0,
|
||||||
|
tasksBouncedToInProgress: 1,
|
||||||
|
postMergeAuditFailures: null,
|
||||||
|
fileScopeInvariantFailures: null,
|
||||||
|
recoverAlreadyMergedReviewTasksRecoveries: null,
|
||||||
|
})).toBe(true);
|
||||||
|
|
||||||
|
expect(dayHasSamples({
|
||||||
|
tasksEnteredInReview: 0,
|
||||||
|
tasksBouncedToInProgress: 0,
|
||||||
|
postMergeAuditFailures: { block: 0, warn: 1, off: 0 },
|
||||||
|
fileScopeInvariantFailures: 0,
|
||||||
|
recoverAlreadyMergedReviewTasksRecoveries: 0,
|
||||||
|
})).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ export interface InReviewDurationMetric {
|
|||||||
reason?: NullMetricReason;
|
reason?: NullMetricReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReliabilityPerDayCounts {
|
||||||
|
tasksEnteredInReview: number;
|
||||||
|
tasksBouncedToInProgress: number;
|
||||||
|
postMergeAuditFailures: { block: number; warn: number; off: number } | null;
|
||||||
|
fileScopeInvariantFailures: number | null;
|
||||||
|
recoverAlreadyMergedReviewTasksRecoveries: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
const DAY_MS = 86_400_000;
|
const DAY_MS = 86_400_000;
|
||||||
|
|
||||||
export function bucketByDay(timestamp: string): string {
|
export function bucketByDay(timestamp: string): string {
|
||||||
@@ -183,6 +191,21 @@ export function mergeAttemptsPerMergedTask(events: RunAuditEvent[], activity: Ac
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dayHasSamples(counts: ReliabilityPerDayCounts): boolean {
|
||||||
|
if (counts.tasksEnteredInReview > 0 || counts.tasksBouncedToInProgress > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counts.postMergeAuditFailures) {
|
||||||
|
const { block, warn, off } = counts.postMergeAuditFailures;
|
||||||
|
if (block + warn + off > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (counts.fileScopeInvariantFailures ?? 0) > 0 || (counts.recoverAlreadyMergedReviewTasksRecoveries ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
export function inReviewFailureRate7d(enteredByDay: Record<string, number>, bouncedByDay: Record<string, number>, endMs: number): NullableMetric<number> {
|
export function inReviewFailureRate7d(enteredByDay: Record<string, number>, bouncedByDay: Record<string, number>, endMs: number): NullableMetric<number> {
|
||||||
let entered = 0;
|
let entered = 0;
|
||||||
let bounced = 0;
|
let bounced = 0;
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import { createAuthMiddleware, authenticateUpgradeRequest, getDaemonToken } from
|
|||||||
import { validateRemoteAuthToken } from "./remote-auth.js";
|
import { validateRemoteAuthToken } from "./remote-auth.js";
|
||||||
import { getCliPackageVersion } from "./cli-package-version.js";
|
import { getCliPackageVersion } from "./cli-package-version.js";
|
||||||
import {
|
import {
|
||||||
|
dayHasSamples,
|
||||||
fileScopeInvariantFailuresPerDay,
|
fileScopeInvariantFailuresPerDay,
|
||||||
inReviewDurationMetrics,
|
inReviewDurationMetrics,
|
||||||
inReviewFailureRate7d,
|
inReviewFailureRate7d,
|
||||||
@@ -1167,11 +1168,13 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
|||||||
const postMergeAuditFailures = postMergeByDay.value ? (postMergeByDay.value[day] ?? { block: 0, warn: 0, off: 0 }) : null;
|
const postMergeAuditFailures = postMergeByDay.value ? (postMergeByDay.value[day] ?? { block: 0, warn: 0, off: 0 }) : null;
|
||||||
const fileScopeInvariantFailures = fileScopeByDay.value ? (fileScopeByDay.value[day] ?? 0) : null;
|
const fileScopeInvariantFailures = fileScopeByDay.value ? (fileScopeByDay.value[day] ?? 0) : null;
|
||||||
const recoverAlreadyMergedReviewTasksRecoveries = recoveriesByDay.value ? (recoveriesByDay.value[day] ?? 0) : null;
|
const recoverAlreadyMergedReviewTasksRecoveries = recoveriesByDay.value ? (recoveriesByDay.value[day] ?? 0) : null;
|
||||||
const hasSamples = tasksEnteredInReview > 0
|
const hasSamples = dayHasSamples({
|
||||||
|| tasksBouncedToInProgress > 0
|
tasksEnteredInReview,
|
||||||
|| (postMergeAuditFailures ? postMergeAuditFailures.block + postMergeAuditFailures.warn + postMergeAuditFailures.off > 0 : false)
|
tasksBouncedToInProgress,
|
||||||
|| (typeof fileScopeInvariantFailures === "number" && fileScopeInvariantFailures > 0)
|
postMergeAuditFailures,
|
||||||
|| (typeof recoverAlreadyMergedReviewTasksRecoveries === "number" && recoverAlreadyMergedReviewTasksRecoveries > 0);
|
fileScopeInvariantFailures,
|
||||||
|
recoverAlreadyMergedReviewTasksRecoveries,
|
||||||
|
});
|
||||||
|
|
||||||
perDay.push({
|
perDay.push({
|
||||||
date: day,
|
date: day,
|
||||||
|
|||||||
Reference in New Issue
Block a user