@feigi/fleet-ctl 3.99.1 → 3.99.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feigi/fleet-ctl",
3
- "version": "3.99.1",
3
+ "version": "3.99.3",
4
4
  "description": "Agent fleet: run-team controller, merge bot, PR reviewer, and the ticket pipeline they share",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -18,12 +18,20 @@
18
18
  // the Pull whose implementer rows carry that PR in `pr`.
19
19
  // - A nested member books with its nearest booked ancestor.
20
20
  //
21
- // RULING. A Pull's candidate Ruling is `rulingFor` of its ticket and date. One
22
- // PR's merge counts once: among the Pulls whose candidate names it, the Pull
23
- // whose implementer rows carry the PR takes it, with the verdict of the latest
24
- // such ruling row in file order. When none carries it, the last of them in
25
- // file order takes it and is unpriced, since its PR's review and fix spend
26
- // cannot be booked. The other candidates stay unruled.
21
+ // RULING. One PR's merge counts once, to one Pull. First, carrier binding: a
22
+ // Pull takes the Ruling of a PR it is the carrier of (the last Pull in file
23
+ // order whose implementer rows carry the PR) when a ruling row names that PR,
24
+ // the latest such row in file order, whatever its ticket or date. A Pull that
25
+ // carries several ruled PRs takes the one whose ruling row is latest in file
26
+ // order and leaves the others to the date floor. Every ruling row of the bound
27
+ // row's tickets is checked as `rulingFor` checks the Pull's own ticket's, so a
28
+ // Pull can be refused for a bad row of a ticket that is not its own.
29
+ // Then the date-floor fallback: each other Pull's candidate Ruling is
30
+ // `rulingFor` of its ticket and date, unless a carrier took that PR. Among the
31
+ // Pulls whose candidate names a PR, the last in file order takes it, with the
32
+ // verdict of the latest such ruling row in file order, and is unpriced, since
33
+ // its PR's review and fix spend cannot be booked. The other candidates stay
34
+ // unruled.
27
35
  //
28
36
  // MERGED: the Pull holds a Ruling and, when PR states are given, its PR is
29
37
  // MERGED. With PR states, a ruled PR that is OPEN or not in the list, or an
@@ -305,20 +313,34 @@ export function attribute({ features, members, tiers, prs = null, windowStart =
305
313
  }
306
314
  }
307
315
 
308
- // Rulings: each PR's merge to one Pull.
316
+ // Rulings: each PR's merge to one Pull, carriers first, then the date floor.
309
317
  const inWindow = pulls.filter((p) => windowStart === null || p.run_date >= windowStart);
310
318
  const rulings = rulingsByTicket(tiers);
311
319
  const fileOrder = new Map(tiers.map((t, i) => [t, i]));
312
- const candidates = new Map(); // pr -> [{ pull, ruling }]
320
+ // Each PR to its latest ruling row in file order, whatever its ticket or date.
321
+ const ruleRows = new Set([...rulings.values()].flat());
322
+ const rulingOfPr = new Map(tiers.filter((t) => ruleRows.has(t)).map((t) => [t.pr, t]));
323
+ const ruled = new Map(); // pull -> { pr, ruling, carried }
324
+ const candidates = new Map(); // pr -> [{ pull, ruling }], the Pulls no carrier binding ruled
313
325
  for (const p of inWindow) {
314
- const ruling = rulingFor(rulings, p.ticket, p.run_date);
315
- if (ruling) (candidates.get(ruling.pr) ?? candidates.set(ruling.pr, []).get(ruling.pr)).push({ pull: p, ruling });
326
+ // Called for every Pull, bound or not, so a bad ruling row of its ticket is refused either way.
327
+ const candidate = rulingFor(rulings, p.ticket, p.run_date);
328
+ let bound = null;
329
+ for (const r of impls.get(p)) {
330
+ const row = carrier.get(r.pr) === p ? rulingOfPr.get(r.pr) : undefined;
331
+ if (row && (bound === null || fileOrder.get(row) > fileOrder.get(bound))) bound = row;
332
+ }
333
+ if (bound) {
334
+ // A bound row of another ticket is refused on the same terms as one of the Pull's own.
335
+ for (const t of String(bound.ticket).split("+")) rulingFor(rulings, t, "");
336
+ ruled.set(p, { pr: bound.pr, ruling: bound, carried: true });
337
+ } else if (candidate) (candidates.get(candidate.pr) ?? candidates.set(candidate.pr, []).get(candidate.pr)).push({ pull: p, ruling: candidate });
316
338
  }
317
- const ruled = new Map(); // pull -> { pr, ruling, carried }
339
+ const taken = new Set([...ruled.values()].map((r) => r.pr));
318
340
  for (const [pr, cands] of candidates) {
341
+ if (taken.has(pr)) continue;
319
342
  const ruling = cands.reduce((a, c) => (fileOrder.get(c.ruling) > fileOrder.get(a) ? c.ruling : a), cands[0].ruling);
320
- const held = cands.find((c) => c.pull === carrier.get(pr));
321
- ruled.set(held?.pull ?? cands.at(-1).pull, { pr, ruling, carried: !!held });
343
+ ruled.set(cands.at(-1).pull, { pr, ruling, carried: false });
322
344
  }
323
345
 
324
346
  const state = prs && new Map(prs.map((p) => [String(p.number), p.state]));