pr-report-core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +5 -2
  2. package/dist/branch-rules.d.mts +30 -0
  3. package/dist/branch-rules.d.mts.map +1 -0
  4. package/dist/branch-rules.mjs +71 -0
  5. package/dist/branch-rules.mjs.map +1 -0
  6. package/dist/checks.d.mts +61 -62
  7. package/dist/checks.d.mts.map +1 -1
  8. package/dist/checks.mjs +0 -1
  9. package/dist/checks.mjs.map +1 -1
  10. package/dist/claim.d.mts +21 -15
  11. package/dist/claim.d.mts.map +1 -1
  12. package/dist/claim.mjs +1 -1
  13. package/dist/claim.mjs.map +1 -1
  14. package/dist/closing-issues.d.mts +18 -19
  15. package/dist/closing-issues.d.mts.map +1 -1
  16. package/dist/closing-issues.mjs +0 -1
  17. package/dist/closing-issues.mjs.map +1 -1
  18. package/dist/code-owners.d.mts +33 -34
  19. package/dist/code-owners.d.mts.map +1 -1
  20. package/dist/code-owners.mjs +0 -2
  21. package/dist/code-owners.mjs.map +1 -1
  22. package/dist/index.d.mts +3 -0
  23. package/dist/index.d.mts.map +1 -1
  24. package/dist/index.mjs +3 -0
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/issue-claim.d.mts +56 -0
  27. package/dist/issue-claim.d.mts.map +1 -0
  28. package/dist/issue-claim.mjs +87 -0
  29. package/dist/issue-claim.mjs.map +1 -0
  30. package/dist/issue-priority.d.mts +44 -0
  31. package/dist/issue-priority.d.mts.map +1 -0
  32. package/dist/issue-priority.mjs +91 -0
  33. package/dist/issue-priority.mjs.map +1 -0
  34. package/dist/labels.d.mts +43 -42
  35. package/dist/labels.d.mts.map +1 -1
  36. package/dist/labels.mjs +2 -1
  37. package/dist/labels.mjs.map +1 -1
  38. package/dist/merge-after.d.mts +22 -16
  39. package/dist/merge-after.d.mts.map +1 -1
  40. package/dist/merge-after.mjs +87 -24
  41. package/dist/merge-after.mjs.map +1 -1
  42. package/dist/queue-comments.d.mts +2 -2
  43. package/dist/queue-order.d.mts +32 -32
  44. package/dist/queue-order.mjs +0 -2
  45. package/dist/queue-order.mjs.map +1 -1
  46. package/dist/queue-owner.d.mts +14 -14
  47. package/dist/queue-owner.d.mts.map +1 -1
  48. package/dist/queue-owner.mjs +18 -0
  49. package/dist/queue-owner.mjs.map +1 -1
  50. package/dist/report.d.mts +4 -4
  51. package/dist/review-hold.d.mts +14 -14
  52. package/dist/ruleset.d.mts +12 -13
  53. package/dist/ruleset.d.mts.map +1 -1
  54. package/dist/ruleset.mjs +0 -1
  55. package/dist/ruleset.mjs.map +1 -1
  56. package/dist/set-aside.d.mts +29 -28
  57. package/dist/set-aside.d.mts.map +1 -1
  58. package/dist/set-aside.mjs +2 -0
  59. package/dist/set-aside.mjs.map +1 -1
  60. package/dist/stack.d.mts +14 -15
  61. package/dist/stack.d.mts.map +1 -1
  62. package/dist/stack.mjs +0 -1
  63. package/dist/stack.mjs.map +1 -1
  64. package/dist/start-after.d.mts +20 -21
  65. package/dist/start-after.d.mts.map +1 -1
  66. package/dist/start-after.mjs +0 -1
  67. package/dist/start-after.mjs.map +1 -1
  68. package/dist/summarize.d.mts +2 -2
  69. package/dist/timestamp.d.mts +10 -10
  70. package/dist/tree.d.mts +5 -5
  71. package/dist/types.d.mts +32 -33
  72. package/dist/types.d.mts.map +1 -1
  73. package/package.json +10 -10
  74. package/src/branch-rules.mts +105 -0
  75. package/src/branch-rules.test.mts +221 -0
  76. package/src/checks.mts +64 -65
  77. package/src/checks.test.mts +30 -31
  78. package/src/claim.mts +16 -16
  79. package/src/closing-issues.mts +18 -19
  80. package/src/closing-issues.test.mts +2 -2
  81. package/src/code-owners.mts +39 -41
  82. package/src/index.mts +3 -0
  83. package/src/issue-claim.mts +138 -0
  84. package/src/issue-claim.test.mts +149 -0
  85. package/src/issue-priority.mts +116 -0
  86. package/src/issue-priority.test.mts +71 -0
  87. package/src/labels.mts +43 -42
  88. package/src/merge-after.mts +142 -49
  89. package/src/merge-after.test.mts +80 -3
  90. package/src/queue-comments.mts +2 -2
  91. package/src/queue-order.mts +60 -62
  92. package/src/queue-order.test.mts +6 -6
  93. package/src/queue-owner.mts +46 -25
  94. package/src/queue-owner.test.mts +4 -2
  95. package/src/report.mts +9 -9
  96. package/src/review-hold.mts +14 -14
  97. package/src/review-hold.test.mts +3 -3
  98. package/src/ruleset.mts +12 -13
  99. package/src/set-aside.mts +30 -28
  100. package/src/set-aside.test.mts +22 -0
  101. package/src/stack.mts +14 -15
  102. package/src/stack.test.mts +4 -4
  103. package/src/start-after.mts +20 -21
  104. package/src/summarize.mts +2 -2
  105. package/src/timestamp.mts +14 -14
  106. package/src/tree.mts +5 -5
  107. package/src/types.mts +32 -33
@@ -8,11 +8,10 @@ import { Arr, Num, Result } from 'ts-data-forge';
8
8
 
9
9
  /**
10
10
  * `#N`, and only when nothing that could end a repository name comes right
11
- * before the `#`: `other/repo#5` is another repository's #5, and treating it
12
- * as this one's would wait for a pull request the author never named.
13
- * `noshiro-pf/mono#5` is not read either, though it means this repository —
14
- * the declaration takes `#N` and nothing else, so there is one way to write
15
- * it.
11
+ * before the `#`: `other/repo#5` is another repository's #5, and treating it as
12
+ * this one's would wait for a pull request the author never named.
13
+ * `noshiro-pf/mono#5` is not read either, though it means this repository — the
14
+ * declaration takes `#N` and nothing else, so there is one way to write it.
16
15
  */
17
16
  const NUMBER_REF = /(?<![\w./-])#(?<number>\d+)\b/gu;
18
17
 
@@ -21,53 +20,84 @@ const DECLARATION_LINE = {
21
20
  'start-after': /^[^\S\n]*start-after[^\S\n]*:(?<refs>[^\n]*)$/gimu,
22
21
  } as const;
23
22
 
24
- /** The opening or closing line of a fenced code block. */
25
- const FENCE_LINE = /^[^\S\n]*(?:`{3,}|~{3,})/u;
23
+ /**
24
+ * A line that can open or close a fenced code block: a run of three or more
25
+ * backticks or tildes, and what follows it on the line.
26
+ */
27
+ const FENCE_LINE = /^[^\S\n]*(?<marker>`{3,}|~{3,})(?<rest>[^\n]*)$/u;
26
28
 
27
29
  /**
28
30
  * The pull request numbers a body declares this one must merge after.
29
31
  *
30
32
  * Duplicates are dropped and the order within the declaration is not kept:
31
33
  * what is being read is a set of things that have to have happened, not a
32
- * sequence. A number that names no pull request, or one that is already
33
- * closed, simply never appears in the open list and so constrains nothing.
34
+ * sequence. A number that names no pull request, or one that is already closed,
35
+ * simply never appears in the open list and so constrains nothing.
34
36
  */
35
37
  export const parseMergeAfter = (body: string): readonly number[] =>
36
38
  parseDeclaration(body, 'merge-after');
37
39
 
38
40
  /**
39
- * The numbers an issue body declares it waits for before it is started,
40
- * read exactly as {@link parseMergeAfter} reads its trailer
41
+ * The numbers an issue body declares it waits for before it is started, read
42
+ * exactly as {@link parseMergeAfter} reads its trailer
41
43
  * (`docs/issue-start-after.md`).
42
44
  */
43
45
  export const parseStartAfter = (body: string): readonly number[] =>
44
46
  parseDeclaration(body, 'start-after');
45
47
 
46
48
  /**
47
- * The body with the contents of its fenced code blocks blanked out, line
48
- * count preserved.
49
+ * The body with the contents of its fenced code blocks blanked out, line count
50
+ * preserved.
49
51
  *
50
52
  * A declaration and an example of one are the same text, and the pull request
51
53
  * that introduced this feature had to describe it: written in a fence, the
52
- * example would have been read as a constraint on the pull request
53
- * documenting it. A fence is how every document here already says "this is a
54
- * sample, not the thing itself", so it is the line to draw.
54
+ * example would have been read as a constraint on the pull request documenting
55
+ * it. A fence is how every document here already says "this is a sample, not
56
+ * the thing itself", so it is the line to draw.
55
57
  *
56
- * Exported because `pr-report` reads the closing keywords (`Closes #12`) out
57
- * of the same bodies and has to draw the line in the same place — a document
58
+ * Exported because `pr-report` reads the closing keywords (`Closes #12`) out of
59
+ * the same bodies and has to draw the line in the same place — a document
58
60
  * showing what a declaration looks like must not become one.
59
61
  */
60
62
  export const outsideCodeFences = (body: string): string => {
61
- let mut_inFence = false;
63
+ // The marker that opened the fence the current line is in, if any.
64
+ let mut_openedBy: string | undefined = undefined;
62
65
 
63
66
  return body
64
67
  .split('\n')
65
68
  .map((line) => {
66
- if (!FENCE_LINE.test(line)) {
67
- return mut_inFence ? '' : line;
69
+ const fence = FENCE_LINE.exec(line)?.groups;
70
+
71
+ const marker = fence?.['marker'];
72
+
73
+ const rest = fence?.['rest'] ?? '';
74
+
75
+ if (mut_openedBy === undefined) {
76
+ // A backtick fence's info string cannot hold a backtick (CommonMark),
77
+ // so ```` ```x``` ```` is inline code, not an opening line.
78
+ if (
79
+ marker === undefined ||
80
+ (marker.startsWith('`') && rest.includes('`'))
81
+ ) {
82
+ return line;
83
+ }
84
+
85
+ mut_openedBy = marker;
86
+
87
+ return '';
68
88
  }
69
89
 
70
- mut_inFence = !mut_inFence;
90
+ // Only a run of the opening character, at least as long, with nothing
91
+ // after it, closes the fence (CommonMark); any other fence line is part
92
+ // of the code, so a ``` inside a ```` or ~~~ block does not end it.
93
+ if (
94
+ marker !== undefined &&
95
+ marker.at(0) === mut_openedBy.at(0) &&
96
+ marker.length >= mut_openedBy.length &&
97
+ rest.trim() === ''
98
+ ) {
99
+ mut_openedBy = undefined;
100
+ }
71
101
 
72
102
  return '';
73
103
  })
@@ -75,52 +105,50 @@ export const outsideCodeFences = (body: string): string => {
75
105
  };
76
106
 
77
107
  /**
78
- * The `Merge-After` cycles, one entry per cycle, each listing the numbers in
79
- * the order the edges run and rotated to start at its lowest number so that
80
- * the same cycle is always reported the same way.
108
+ * The `Merge-After` cycles, each listing the numbers in the order the edges run
109
+ * and rotated to start at its lowest number so that the same cycle is always
110
+ * reported the same way.
111
+ *
112
+ * Every number that lies on any cycle appears in at least one entry — callers
113
+ * take `cycles.flat()` as the set of numbers on a cycle. Each entry is the
114
+ * shortest cycle through the first number, in key order, that no earlier entry
115
+ * covers, so a component with several loops yields one entry per loop needed to
116
+ * cover it rather than every loop it holds.
81
117
  *
82
118
  * A cycle is not a state anything here can resolve: every pull request on one
83
119
  * waits for another that waits, in the end, for it. Naming it is the whole of
84
120
  * what can be done about it — without this the members would each report
85
121
  * "waiting on #N" for as long as the loop runs, with nothing saying why that
86
- * never changes. A pull request that declares itself is a cycle of one, and
87
- * is found by the same walk.
122
+ * never changes. A pull request that declares itself is a cycle of one, and is
123
+ * found by the same search.
88
124
  */
89
125
  export const findMergeAfterCycles = (
90
126
  dependencies: ReadonlyMap<number, readonly number[]>,
91
127
  ): readonly (readonly number[])[] => {
92
- const mut_finished = new Set<number>();
128
+ const mut_covered = new Set<number>();
93
129
 
94
130
  const mut_cycles = new Map<string, readonly number[]>();
95
131
 
96
- const visit = (node: number, walked: readonly number[]): void => {
97
- const at = walked.indexOf(node);
98
-
99
- if (at !== -1) {
100
- // A back edge into the path being walked: everything from there on is
101
- // the cycle.
102
- const cycle = rotateToLowest(walked.slice(at));
103
-
104
- mut_cycles.set(cycle.join(','), cycle);
105
-
106
- return;
132
+ for (const node of dependencies.keys()) {
133
+ if (mut_covered.has(node)) {
134
+ continue;
107
135
  }
108
136
 
109
- if (mut_finished.has(node)) {
110
- return;
137
+ // A search per node rather than one depth-first walk: a walk that skips
138
+ // finished nodes misses a node whose only way back runs through one.
139
+ const cycle = shortestCycleThrough(node, dependencies);
140
+
141
+ if (cycle === undefined) {
142
+ continue;
111
143
  }
112
144
 
113
- for (const dependency of dependencies.get(node) ?? []) {
114
- visit(dependency, Arr.toPushed(walked, node));
145
+ for (const member of cycle) {
146
+ mut_covered.add(member);
115
147
  }
116
148
 
117
- // Only once every edge out of it has been walked, so no back edge into
118
- // this node's subtree can be missed by the walk that comes next.
119
- mut_finished.add(node);
120
- };
149
+ const rotated = rotateToLowest(cycle);
121
150
 
122
- for (const node of dependencies.keys()) {
123
- visit(node, []);
151
+ mut_cycles.set(rotated.join(','), rotated);
124
152
  }
125
153
 
126
154
  return Array.from(mut_cycles.values());
@@ -149,6 +177,71 @@ const parseDeclaration = (
149
177
  .toArray(),
150
178
  );
151
179
 
180
+ /** `start`, …, `end`, following `reachedFrom` back from `end`. */
181
+ const pathFromStart = (
182
+ start: number,
183
+ end: number,
184
+ reachedFrom: ReadonlyMap<number, number>,
185
+ ): readonly number[] => {
186
+ const mut_reversed: number[] = [end];
187
+
188
+ let mut_node = end;
189
+
190
+ while (mut_node !== start) {
191
+ const previous = reachedFrom.get(mut_node);
192
+
193
+ if (previous === undefined) {
194
+ break;
195
+ }
196
+
197
+ mut_reversed.push(previous);
198
+
199
+ mut_node = previous;
200
+ }
201
+
202
+ return mut_reversed.toReversed();
203
+ };
204
+
205
+ /**
206
+ * The shortest cycle through `start`, from `start` along the edges back to the
207
+ * node whose edge returns to it, or `undefined` when no path leads back.
208
+ */
209
+ const shortestCycleThrough = (
210
+ start: number,
211
+ dependencies: ReadonlyMap<number, readonly number[]>,
212
+ ): readonly number[] | undefined => {
213
+ // Each reached node, with the node it was first reached from.
214
+ const mut_reachedFrom = new Map<number, number>();
215
+
216
+ // Level by level: every node in the frontier is one edge further from `start`
217
+ // than the last, so the first edge back to `start` closes a shortest cycle.
218
+ let mut_frontier: readonly number[] = [start];
219
+
220
+ while (Arr.isNonEmpty(mut_frontier)) {
221
+ const mut_nextFrontier: number[] = [];
222
+
223
+ for (const node of mut_frontier) {
224
+ for (const next of dependencies.get(node) ?? []) {
225
+ if (next === start) {
226
+ return pathFromStart(start, node, mut_reachedFrom);
227
+ }
228
+
229
+ if (mut_reachedFrom.has(next)) {
230
+ continue;
231
+ }
232
+
233
+ mut_reachedFrom.set(next, node);
234
+
235
+ mut_nextFrontier.push(next);
236
+ }
237
+ }
238
+
239
+ mut_frontier = mut_nextFrontier;
240
+ }
241
+
242
+ return undefined;
243
+ };
244
+
152
245
  const rotateToLowest = (cycle: readonly number[]): readonly number[] => {
153
246
  if (!Arr.isNonEmpty(cycle)) {
154
247
  return cycle;
@@ -51,9 +51,9 @@ describe(parseMergeAfter, () => {
51
51
  );
52
52
  });
53
53
 
54
- // The fence markers and the column they sit in are what these two are
55
- // about, so the fixtures are arrays of lines rather than a template
56
- // literal: what `dedent` would remove is what is being tested.
54
+ // The fence markers and the column they sit in are what these two are about,
55
+ // so the fixtures are arrays of lines rather than a template literal: what
56
+ // `dedent` would remove is what is being tested.
57
57
  test('ignores a trailer inside a fenced code block', () => {
58
58
  assert.deepStrictEqual(
59
59
  parseMergeAfter(
@@ -79,6 +79,64 @@ describe(parseMergeAfter, () => {
79
79
  );
80
80
  });
81
81
 
82
+ test('ignores the trailers inside a longer fence that holds a shorter one', () => {
83
+ assert.deepStrictEqual(
84
+ parseMergeAfter(
85
+ [
86
+ '````markdown',
87
+ '```',
88
+ 'Merge-After: #1901',
89
+ '```',
90
+ 'Merge-After: #1902',
91
+ '````',
92
+ 'Merge-After: #1903',
93
+ '',
94
+ ].join('\n'),
95
+ ),
96
+ [1903],
97
+ );
98
+ });
99
+
100
+ test('ignores a trailer after a backtick fence inside a tilde fence', () => {
101
+ assert.deepStrictEqual(
102
+ parseMergeAfter(
103
+ ['~~~', '```', 'Merge-After: #1901', '~~~', ''].join('\n'),
104
+ ),
105
+ [],
106
+ );
107
+ });
108
+
109
+ test('does not close a fence on a line with an info string', () => {
110
+ assert.deepStrictEqual(
111
+ parseMergeAfter(
112
+ ['```', '```ts', 'Merge-After: #1901', '```', ''].join('\n'),
113
+ ),
114
+ [],
115
+ );
116
+ });
117
+
118
+ test('closes a fence on a longer run of the same character', () => {
119
+ assert.deepStrictEqual(
120
+ parseMergeAfter(
121
+ ['```', 'Merge-After: #1901', '`````', 'Merge-After: #1903', ''].join(
122
+ '\n',
123
+ ),
124
+ ),
125
+ [1903],
126
+ );
127
+ });
128
+
129
+ test('closes a fence on a CRLF line', () => {
130
+ assert.deepStrictEqual(
131
+ parseMergeAfter(
132
+ ['```', 'Merge-After: #1901', '```', 'Merge-After: #1903', ''].join(
133
+ '\r\n',
134
+ ),
135
+ ),
136
+ [1903],
137
+ );
138
+ });
139
+
82
140
  test('reads a body with the CRLF line endings GitHub returns', () => {
83
141
  assert.deepStrictEqual(
84
142
  parseMergeAfter('Adds the parser.\r\n\r\nMerge-After: #1901\r\n'),
@@ -218,6 +276,25 @@ describe(findMergeAfterCycles, () => {
218
276
  );
219
277
  });
220
278
 
279
+ test('reports a member whose only way back runs through a finished node', () => {
280
+ // 3 → 2 → 1 → 3 is a cycle as well as 1 ⇄ 2; a walk from 1 finishes 2
281
+ // before it reaches 3, so 3's edge into 2 must still put 3 on a cycle.
282
+ const cycles = findMergeAfterCycles(
283
+ new Map<number, readonly number[]>([
284
+ [1, [2, 3]],
285
+ [2, [1]],
286
+ [3, [2]],
287
+ ]),
288
+ );
289
+
290
+ assert.deepStrictEqual(cycles, [
291
+ [1, 2],
292
+ [1, 3, 2],
293
+ ]);
294
+
295
+ assert.deepStrictEqual(new Set(cycles.flat()), new Set<number>([1, 2, 3]));
296
+ });
297
+
221
298
  test('follows an edge to a pull request that declared nothing', () => {
222
299
  assert.deepStrictEqual(
223
300
  findMergeAfterCycles(new Map<number, readonly number[]>([[1, [2]]])),
@@ -41,8 +41,8 @@ export type QueueComment<A> = Readonly<{
41
41
  }>;
42
42
 
43
43
  /**
44
- * Reads `comments`, oldest first, for the oldest comment of each kind: each
45
- * is one comment edited in place, and of several the oldest is the one.
44
+ * Reads `comments`, oldest first, for the oldest comment of each kind: each is
45
+ * one comment edited in place, and of several the oldest is the one.
46
46
  */
47
47
  export const readQueueComments = (
48
48
  comments: readonly OwnComment[],
@@ -2,13 +2,13 @@
2
2
  * Where each pull request stands in `unblock-prs`'s queue, and the order the
3
3
  * worker picks the ones it may pick.
4
4
  *
5
- * The worker decides on someone's machine, from what `gh` and `git` return,
6
- * and the Pull Requests Manager page draws the same queue from what GraphQL
5
+ * The worker decides on someone's machine, from what `gh` and `git` return, and
6
+ * the Pull Requests Manager page draws the same queue from what GraphQL
7
7
  * returns. So the deciding is here, once, over facts either side can supply,
8
8
  * and each side does only its own reading: the worker reads some facts only
9
9
  * when the answer needs them — the checks of a `BLOCKED` pull request, the
10
- * native stack of one without auto-merge — which is why
11
- * {@link queueStanding} asks for them rather than taking them all up front.
10
+ * native stack of one without auto-merge — which is why {@link queueStanding}
11
+ * asks for them rather than taking them all up front.
12
12
  *
13
13
  * What the worker alone knows is left out: which pull requests one run has
14
14
  * demoted (`demotions.mts` in `unblock-prs`) lives in that run's memory, so
@@ -39,8 +39,8 @@ export type QueueFacts = Readonly<{
39
39
  headRef: string;
40
40
  baseRef: string;
41
41
  /**
42
- * GitHub's `mergeStateStatus` — `BEHIND`, `DIRTY`, `CLEAN`, `BLOCKED` and
43
- * so on — or as near to it as the reader can tell.
42
+ * GitHub's `mergeStateStatus` — `BEHIND`, `DIRTY`, `CLEAN`, `BLOCKED` and so
43
+ * on — or as near to it as the reader can tell.
44
44
  */
45
45
  mergeState: string;
46
46
  autoMerge: boolean;
@@ -57,8 +57,8 @@ export type QueueGraphMember = Readonly<{
57
57
  baseRef: string;
58
58
  fromFork: boolean;
59
59
  /**
60
- * The open issues it closes, whose `Start-After:` it inherits. Absent for
61
- * a reader that did not read them: nothing is inherited.
60
+ * The open issues it closes, whose `Start-After:` it inherits. Absent for a
61
+ * reader that did not read them: nothing is inherited.
62
62
  */
63
63
  closes?: readonly number[];
64
64
  }>;
@@ -68,8 +68,8 @@ export type QueueGraph = Readonly<{
68
68
  /** Which open pull request each stacked one is on: child → parent. */
69
69
  stackParents: ReadonlyMap<number, number>;
70
70
  /**
71
- * What each pull request must merge after: what it declared, the one it
72
- * is stacked on, and what it inherits.
71
+ * What each pull request must merge after: what it declared, the one it is
72
+ * stacked on, and what it inherits.
73
73
  */
74
74
  dependencies: ReadonlyMap<number, readonly number[]>;
75
75
  /**
@@ -165,9 +165,9 @@ export const versionBranchName = (defaultBranch: string): string =>
165
165
  `changeset-release/${defaultBranch}` as const;
166
166
 
167
167
  /**
168
- * Whether a pull request from `headRef` is the version pull request. The
169
- * branch is the only stable handle on it: the title and body are rewritten
170
- * on every run, and the number changes with every release.
168
+ * Whether a pull request from `headRef` is the version pull request. The branch
169
+ * is the only stable handle on it: the title and body are rewritten on every
170
+ * run, and the number changes with every release.
171
171
  */
172
172
  export const isVersionBranch = (
173
173
  headRef: string,
@@ -191,8 +191,8 @@ export const isMergeableState = (mergeState: string): boolean =>
191
191
 
192
192
  /**
193
193
  * The stacks, the declared order and its cycles, and what holds the release,
194
- * read off the open pull requests. `startAfterOf` is what the issues they
195
- * close declare, each issue's `Start-After:` by its number.
194
+ * read off the open pull requests. `startAfterOf` is what the issues they close
195
+ * declare, each issue's `Start-After:` by its number.
196
196
  */
197
197
  export const queueGraph = (
198
198
  pulls: readonly QueueGraphMember[],
@@ -219,12 +219,12 @@ export const queueGraph = (
219
219
 
220
220
  return [
221
221
  pr.number,
222
- // The version pull request declares nothing: its body is rewritten
223
- // on every push to the base, so a trailer written there would hold
224
- // for a cycle or two and then silently stop. `blocks-release`, on
225
- // the pull requests it is waiting for, is how an order is declared
226
- // on it. Other pull requests may still name *it*. A stacked pull
227
- // request waits for the layer below it whether or not it says so.
222
+ // The version pull request declares nothing: its body is rewritten on
223
+ // every push to the base, so a trailer written there would hold for a
224
+ // cycle or two and then silently stop. `blocks-release`, on the pull
225
+ // requests it is waiting for, is how an order is declared on it. Other
226
+ // pull requests may still name *it*. A stacked pull request waits for
227
+ // the layer below it whether or not it says so.
228
228
  isVersionBranch(pr.headRef, defaultBranch)
229
229
  ? []
230
230
  : Arr.uniq([
@@ -241,8 +241,8 @@ export const queueGraph = (
241
241
 
242
242
  return [
243
243
  number,
244
- // The version pull request closes nothing it means to, and is held
245
- // by `blocks-release` alone.
244
+ // The version pull request closes nothing it means to, and is held by
245
+ // `blocks-release` alone.
246
246
  isVersionBranch(headRef, defaultBranch)
247
247
  ? []
248
248
  : (inheritedOrders.get(number)?.mergeAfter ?? []).filter(
@@ -288,15 +288,15 @@ export const queueStanding = (
288
288
  if (outOfScope !== undefined) {
289
289
  // `merge-queued` is an explicit request, so anything that answers it with
290
290
  // "no" is worth saying out loud — a queued pull request left as a draft
291
- // would otherwise sit in the queue forever with nothing saying why. A
292
- // pull request that never asked is passed over in silence.
291
+ // would otherwise sit in the queue forever with nothing saying why. A pull
292
+ // request that never asked is passed over in silence.
293
293
  return hasLabel(pr, MERGE_QUEUED_LABEL)
294
294
  ? { kind: 'unactionable', reason: outOfScope }
295
295
  : { kind: 'ignore' };
296
296
  }
297
297
 
298
- // A pull request set aside, or claimed, is a skill's or a person's until
299
- // its comment is resolved (`queue-owner.mts`).
298
+ // A pull request set aside, or claimed, is a skill's or a person's until its
299
+ // comment is resolved (`queue-owner.mts`).
300
300
  const owner = context.owners.get(pr.number);
301
301
 
302
302
  if (owner !== undefined && owner.owner !== 'worker') {
@@ -327,9 +327,9 @@ export const queueStanding = (
327
327
  }
328
328
  }
329
329
 
330
- // The version pull request is rebuilt from the tip of the base by the
331
- // release workflow, never rebased here; taking `skip-ci` off is the whole
332
- // of what it can be given.
330
+ // The version pull request is rebuilt from the tip of the base by the release
331
+ // workflow, never rebased here; taking `skip-ci` off is the whole of what it
332
+ // can be given.
333
333
  if (isVersionBranch(pr.headRef, context.defaultBranch)) {
334
334
  return versionStanding(pr, context, reads);
335
335
  }
@@ -342,9 +342,9 @@ export const queueStanding = (
342
342
  }
343
343
 
344
344
  switch (pr.mergeState) {
345
- // `DIRTY` is a candidate too: it answers whether *merging* conflicts,
346
- // from a cached computation, where the worker *rebases*, and the rebase
347
- // is what says whether the conflict is real.
345
+ // `DIRTY` is a candidate too: it answers whether *merging* conflicts, from
346
+ // a cached computation, where the worker *rebases*, and the rebase is what
347
+ // says whether the conflict is real.
348
348
  case 'BEHIND':
349
349
  case 'DIRTY':
350
350
  return waitingOn(pr, context) ?? { kind: 'candidate' };
@@ -363,10 +363,10 @@ export const queueStanding = (
363
363
  };
364
364
 
365
365
  /**
366
- * Whether what {@link queueStanding} decided is worth asking the review
367
- * about: one about to be picked, or one about to be watched while GitHub
368
- * still calls it `BLOCKED`. A merge state GitHub already calls mergeable is
369
- * its own answer that nothing but the checks is left.
366
+ * Whether what {@link queueStanding} decided is worth asking the review about:
367
+ * one about to be picked, or one about to be watched while GitHub still calls
368
+ * it `BLOCKED`. A merge state GitHub already calls mergeable is its own answer
369
+ * that nothing but the checks is left.
370
370
  */
371
371
  export const asksReview = (
372
372
  standing: QueueStanding,
@@ -377,8 +377,8 @@ export const asksReview = (
377
377
 
378
378
  /**
379
379
  * The standing once the review is known: a hold that waits for someone holds
380
- * it; one that only a bypass merges, and none at all, change nothing here —
381
- * the worker leaves the first unarmed, which is not a question of order.
380
+ * it; one that only a bypass merges, and none at all, change nothing here — the
381
+ * worker leaves the first unarmed, which is not a question of order.
382
382
  */
383
383
  export const withReviewHold = (
384
384
  standing: QueueStanding,
@@ -392,9 +392,9 @@ export const withReviewHold = (
392
392
  export type QueuePriority = 'high' | 'low' | 'none' | 'both';
393
393
 
394
394
  /**
395
- * Which priority label a pull request carries: `both` is the two at once,
396
- * which contradict each other and so count as neither. `priority:mid` is the
397
- * same as neither here.
395
+ * Which priority label a pull request carries: `both` is the two at once, which
396
+ * contradict each other and so count as neither. `priority:mid` is the same as
397
+ * neither here.
398
398
  */
399
399
  export const priorityOfLabels = (labels: readonly string[]): QueuePriority => {
400
400
  const high = labels.includes(PRIORITY_HIGH_LABEL);
@@ -427,9 +427,9 @@ export const priorityRankOfLabels = (labels: readonly string[]): 0 | 1 | 2 => {
427
427
  * ruleset bypass merges ({@link ReviewHold}): that one ranks with
428
428
  * `priority:low` unless labelled `priority:high` — alone, since the two
429
429
  * together count as neither. Picked, it is released, goes green and is set
430
- * aside for a person, so the cycle spent on it lands nothing; picked last,
431
- * it still has its checks run. `priority:high` is a person saying they want
432
- * that verdict now, and is the one way to ask for it, so it still counts.
430
+ * aside for a person, so the cycle spent on it lands nothing; picked last, it
431
+ * still has its checks run. `priority:high` is a person saying they want that
432
+ * verdict now, and is the one way to ask for it, so it still counts.
433
433
  */
434
434
  export const pickPriorityRank = (
435
435
  labels: readonly string[],
@@ -443,9 +443,9 @@ export const pickPriorityRank = (
443
443
  };
444
444
 
445
445
  /**
446
- * The order the worker picks candidates in. The declared order has already
447
- * been applied, as a gate rather than a sort, so this is lowest number first
448
- * within these ranks, outermost first:
446
+ * The order the worker picks candidates in. The declared order has already been
447
+ * applied, as a gate rather than a sort, so this is lowest number first within
448
+ * these ranks, outermost first:
449
449
  *
450
450
  * 1. What a watch has seen green and still open (`demoted`) goes after
451
451
  * everything else, the release and `priority:high` included: everything
@@ -463,8 +463,8 @@ export const pickPriorityRank = (
463
463
  * release: it only says that when a queued change and the release are
464
464
  * both ready, the change goes first.
465
465
  *
466
- * `toSorted` is stable, so each sort keeps the order of the one before it
467
- * among equals, and the last one is the outermost.
466
+ * `toSorted` is stable, so each sort keeps the order of the one before it among
467
+ * equals, and the last one is the outermost.
468
468
  */
469
469
  export const orderCandidates = <P,>(
470
470
  candidates: readonly P[],
@@ -498,9 +498,8 @@ export const orderCandidates = <P,>(
498
498
  };
499
499
 
500
500
  /**
501
- * Whether `pr` carries the label `name`. A function rather than `includes`
502
- * in place, which narrows the label constant itself to `never` where it says
503
- * no.
501
+ * Whether `pr` carries the label `name`. A function rather than `includes` in
502
+ * place, which narrows the label constant itself to `never` where it says no.
504
503
  */
505
504
  const hasLabel = (
506
505
  pr: Readonly<{ labels: readonly string[] }>,
@@ -522,10 +521,9 @@ const outOfScopeReason = (
522
521
  return `not labelled ${MERGE_QUEUED_LABEL}`;
523
522
  }
524
523
 
525
- // A base that is neither the default branch nor another open pull
526
- // request's branch is one nothing here knows how to land on. A stacked one
527
- // whose parent has just merged reads this way for the moment before GitHub
528
- // moves it.
524
+ // A base that is neither the default branch nor another open pull request's
525
+ // branch is one nothing here knows how to land on. A stacked one whose parent
526
+ // has just merged reads this way for the moment before GitHub moves it.
529
527
  return pr.isDraft
530
528
  ? 'draft'
531
529
  : pr.baseRef !== context.defaultBranch &&
@@ -565,17 +563,17 @@ const byChecks = (reads: QueueReads): QueueStanding =>
565
563
  : ({ kind: 'in-flight' } as const);
566
564
 
567
565
  /**
568
- * The version pull request: held while anything labelled `blocks-release`
569
- * is open, and while the branch was built before the tip of the base, then
570
- * a candidate if paused and otherwise judged by its checks.
566
+ * The version pull request: held while anything labelled `blocks-release` is
567
+ * open, and while the branch was built before the tip of the base, then a
568
+ * candidate if paused and otherwise judged by its checks.
571
569
  */
572
570
  const versionStanding = (
573
571
  pr: QueueFacts,
574
572
  context: QueueContext,
575
573
  reads: QueueReads,
576
574
  ): QueueStanding => {
577
- // Never one of its own blockers: the label on it would otherwise hold it
578
- // for as long as it existed.
575
+ // Never one of its own blockers: the label on it would otherwise hold it for
576
+ // as long as it existed.
579
577
  const by = context.releaseBlockers.filter(
580
578
  (blocker) => blocker.number !== pr.number,
581
579
  );