feat(FN-3977): complete Step 1 — overlap fanout semantics

Fusion-Task-Id: FN-3977
Fusion-Task-Lineage: c84f2ce0-3726-49b2-ad84-6703edbfecf1
This commit is contained in:
Fusion
2026-05-14 15:33:43 -07:00
committed by gsxdsm
parent 85c6869fa6
commit 05864d9bcb
3 changed files with 77 additions and 143 deletions

View File

@@ -20,12 +20,12 @@ function createTask(id: string, column: Task["column"], overrides: Partial<Task>
}
describe("computeBlockerFanoutMap escalation", () => {
it("escalates high fan-out blockers when age crosses threshold", () => {
it("escalates high overlap fan-out blockers when age crosses threshold", () => {
const nowMs = Date.parse("2026-01-01T06:00:00.000Z");
const blocker = createTask("B", "in-progress", { columnMovedAt: "2026-01-01T00:00:00.000Z" });
const dependents = [1, 2, 3, 4, 5].map((n) => createTask(`D${n}`, "todo", { dependencies: ["B"] }));
const blockedByDependents = [1, 2, 3, 4, 5].map((n) => createTask(`D${n}`, "todo", { blockedBy: "B" }));
const entry = computeBlockerFanoutMap([blocker, ...dependents], MAX_AUTO_MERGE_RETRIES, {
const entry = computeBlockerFanoutMap([blocker, ...blockedByDependents], MAX_AUTO_MERGE_RETRIES, {
nowMs,
staleHighFanoutAgeThresholdMs: 60 * 60 * 1000,
}).get("B");
@@ -38,12 +38,24 @@ describe("computeBlockerFanoutMap escalation", () => {
});
});
it("keeps short-lived high fan-out blockers quiet", () => {
it("does not classify dependency-only fan-out as overlap bottleneck", () => {
const blocker = createTask("B", "in-progress", { columnMovedAt: "2026-01-01T00:00:00.000Z" });
const dependencyDependents = [1, 2, 3, 4, 5].map((n) => createTask(`D${n}`, "todo", { dependencies: ["B"] }));
const entry = computeBlockerFanoutMap([blocker, ...dependencyDependents], MAX_AUTO_MERGE_RETRIES).get("B");
expect(entry?.overlapBlockedTodoCount).toBe(0);
expect(entry?.activeTodoCount).toBe(5);
expect(entry?.isHighFanout).toBe(false);
expect(entry?.escalation).toBeUndefined();
});
it("keeps short-lived high overlap fan-out blockers quiet", () => {
const nowMs = Date.parse("2026-01-01T00:10:00.000Z");
const blocker = createTask("B", "in-progress", { columnMovedAt: "2026-01-01T00:00:00.000Z" });
const dependents = [1, 2, 3, 4, 5].map((n) => createTask(`D${n}`, "todo", { dependencies: ["B"] }));
const blockedByDependents = [1, 2, 3, 4, 5].map((n) => createTask(`D${n}`, "todo", { blockedBy: "B" }));
const entry = computeBlockerFanoutMap([blocker, ...dependents], MAX_AUTO_MERGE_RETRIES, {
const entry = computeBlockerFanoutMap([blocker, ...blockedByDependents], MAX_AUTO_MERGE_RETRIES, {
nowMs,
staleHighFanoutAgeThresholdMs: 60 * 60 * 1000,
}).get("B");
@@ -51,18 +63,4 @@ describe("computeBlockerFanoutMap escalation", () => {
expect(entry?.isHighFanout).toBe(true);
expect(entry?.escalation).toBeUndefined();
});
it("does not escalate sub-threshold chains", () => {
const nowMs = Date.parse("2026-01-01T10:00:00.000Z");
const blocker = createTask("B", "in-review", { columnMovedAt: "2026-01-01T00:00:00.000Z" });
const dependents = [1, 2, 3, 4].map((n) => createTask(`D${n}`, "todo", { dependencies: ["B"] }));
const entry = computeBlockerFanoutMap([blocker, ...dependents], MAX_AUTO_MERGE_RETRIES, {
nowMs,
staleHighFanoutAgeThresholdMs: 60 * 60 * 1000,
}).get("B");
expect(entry?.isHighFanout).toBe(false);
expect(entry?.escalation).toBeUndefined();
});
});

View File

@@ -15,6 +15,10 @@ export interface BlockerFanoutEntry {
totalCount: number;
activeTodoCount: number;
dependentIds: string[];
dependencyDependentIds: string[];
overlapBlockedDependentIds: string[];
overlapBlockedActiveCount: number;
overlapBlockedTodoCount: number;
staleBlockedByDependentIds: string[];
isHighFanout: boolean;
escalation?: BlockerEscalation;
@@ -32,9 +36,12 @@ const ACTIVE_COLUMNS = new Set<Task["column"]>(["triage", "todo", "in-progress",
interface MutableEntry {
dependentIds: string[];
dependencyDependentIds: string[];
blockedByDependentIds: string[];
activeCount: number;
activeTodoCount: number;
overlapBlockedActiveCount: number;
overlapBlockedTodoCount: number;
}
export function isStaleBlockedByBlocker(blocker: Task | undefined, maxAutoMergeRetries: number): boolean {
@@ -70,7 +77,15 @@ export function computeBlockerFanoutMap(
const ensureEntry = (blockerId: string): MutableEntry => {
let entry = fanout.get(blockerId);
if (!entry) {
entry = { dependentIds: [], blockedByDependentIds: [], activeCount: 0, activeTodoCount: 0 };
entry = {
dependentIds: [],
dependencyDependentIds: [],
blockedByDependentIds: [],
activeCount: 0,
activeTodoCount: 0,
overlapBlockedActiveCount: 0,
overlapBlockedTodoCount: 0,
};
fanout.set(blockerId, entry);
}
return entry;
@@ -84,6 +99,7 @@ export function computeBlockerFanoutMap(
if (!depId) continue;
const entry = ensureEntry(depId);
entry.dependentIds.push(task.id);
entry.dependencyDependentIds.push(task.id);
if (active) entry.activeCount += 1;
if (isTodo) entry.activeTodoCount += 1;
}
@@ -92,8 +108,14 @@ export function computeBlockerFanoutMap(
const entry = ensureEntry(task.blockedBy);
entry.dependentIds.push(task.id);
entry.blockedByDependentIds.push(task.id);
if (active) entry.activeCount += 1;
if (isTodo) entry.activeTodoCount += 1;
if (active) {
entry.activeCount += 1;
entry.overlapBlockedActiveCount += 1;
}
if (isTodo) {
entry.activeTodoCount += 1;
entry.overlapBlockedTodoCount += 1;
}
}
}
@@ -104,7 +126,7 @@ export function computeBlockerFanoutMap(
? [...entry.blockedByDependentIds]
: [];
const isHighFanout = entry.activeTodoCount >= highFanoutTodoThreshold;
const isHighFanout = entry.overlapBlockedTodoCount >= highFanoutTodoThreshold;
const blockingAgeMs = blocker ? getBlockingAgeMs(blocker, nowMs) : 0;
const blockerColumn = blocker?.column;
const shouldEscalate =
@@ -117,13 +139,17 @@ export function computeBlockerFanoutMap(
totalCount: entry.activeCount,
activeTodoCount: entry.activeTodoCount,
dependentIds: entry.dependentIds,
dependencyDependentIds: entry.dependencyDependentIds,
overlapBlockedDependentIds: entry.blockedByDependentIds,
overlapBlockedActiveCount: entry.overlapBlockedActiveCount,
overlapBlockedTodoCount: entry.overlapBlockedTodoCount,
staleBlockedByDependentIds,
isHighFanout,
escalation: shouldEscalate
? {
blockerId,
activeTodoCount: entry.activeTodoCount,
totalActiveCount: entry.activeCount,
activeTodoCount: entry.overlapBlockedTodoCount,
totalActiveCount: entry.overlapBlockedActiveCount,
blockingAgeMs,
}
: undefined,