Public: anyone can read this; the crew can change it.

A pure read lists the pairs of open pulls that touch the same files, most files shared first #6557

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6549-a-pure-read-lists-the-pairs-of-open-pull → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6549-a-pure-read-lists-the-pairs-of-open-pull was merged into main as 5dec3e8.

Review

Sign in to review this change.
2 files changed +83 −0
src/flow/conflict_pairs.ts added +39
@@ -0,0 +1,39 @@
1 /**
2 * Conflict pairs (#6549): pairs of open pulls that touch the same files, most
3 * files shared first, then lowest numbers, at most 20. A pull touching more
4 * than 60 files is left out, as the hot-file limit does for wide pulls.
5 * Pure: plain data in, plain data out.
6 */
7
8 export interface PullFiles {
9 readonly number: number;
10 readonly files: readonly string[];
11 }
12
13 export interface ConflictPair {
14 readonly a: number;
15 readonly b: number;
16 readonly shared: number;
17 }
18
19 const WIDE = 60;
20 const LIMIT = 20;
21
22 export function conflictPairs(pulls: readonly PullFiles[]): ConflictPair[] {
23 const narrow = pulls
24 .filter((p) => new Set(p.files).size <= WIDE)
25 .map((p) => ({ number: p.number, files: new Set(p.files) }));

Set is built twice per pull (filter, then map). Map to sets first, then filter on .size.

26 const out: ConflictPair[] = [];
27 for (let i = 0; i < narrow.length; i++) {
28 for (let j = i + 1; j < narrow.length; j++) {
29 const x = narrow[i]!;
30 const y = narrow[j]!;
31 if (x.number === y.number) continue;
32 let shared = 0;
33 for (const f of x.files) if (y.files.has(f)) shared++;
34 if (shared > 0) out.push({ a: Math.min(x.number, y.number), b: Math.max(x.number, y.number), shared });
35 }
36 }
37 out.sort((p, q) => q.shared - p.shared || p.a - q.a || p.b - q.b);
38 return out.slice(0, LIMIT);
39 }
test/flow-conflict-pairs.test.ts added +44
@@ -0,0 +1,44 @@
1 import { expect, test } from "bun:test";
2 import { conflictPairs } from "../src/flow/conflict_pairs.ts";
3
4 const many = (n: number, p = "f") => Array.from({ length: n }, (_, i) => `${p}${i}`);
5
6 test("no overlap gives no pairs", () => {
7 expect(conflictPairs([{ number: 1, files: ["a"] }, { number: 2, files: ["b"] }])).toEqual([]);
8 expect(conflictPairs([])).toEqual([]);
9 });
10
11 test("one pair, a < b whatever the input order", () => {
12 expect(conflictPairs([{ number: 9, files: ["a", "b"] }, { number: 3, files: ["b", "c"] }])).toEqual([
13 { a: 3, b: 9, shared: 1 },
14 ]);
15 });
16
17 test("most shared first, then lowest numbers", () => {
18 const r = conflictPairs([
19 { number: 5, files: ["x"] },
20 { number: 4, files: ["x", "y"] },
21 { number: 3, files: ["x", "y"] },
22 { number: 2, files: ["x"] },
23 ]);
24 expect(r.map((p) => [p.a, p.b, p.shared])).toEqual([
25 [3, 4, 2],
26 [2, 3, 1],
27 [2, 4, 1],
28 [2, 5, 1],
29 [3, 5, 1],
30 [4, 5, 1],
31 ]);
32 });
33
34 test("pulls touching more than 60 files are left out, 60 stays", () => {
35 expect(conflictPairs([{ number: 1, files: many(61) }, { number: 2, files: ["f0"] }])).toEqual([]);
36 expect(conflictPairs([{ number: 1, files: many(60) }, { number: 2, files: ["f0"] }])).toHaveLength(1);
37 });
38
39 test("at most 20 pairs", () => {
40 const pulls = Array.from({ length: 10 }, (_, i) => ({ number: i + 1, files: ["x"] }));
41 const r = conflictPairs(pulls);
42 expect(r).toHaveLength(20);
43 expect(r[0]).toEqual({ a: 1, b: 2, shared: 1 });
44 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs