@ai-dossier/sched 0.56.0 → 0.57.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.
- package/README.md +45 -8
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +31 -7
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/dispatch.d.ts +16 -1
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +52 -5
- package/dist/dispatch.js.map +1 -1
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +90 -2
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/recovery.d.ts +66 -5
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +317 -67
- package/dist/recovery.js.map +1 -1
- package/dist/resume-seed.d.ts +36 -0
- package/dist/resume-seed.d.ts.map +1 -0
- package/dist/resume-seed.js +83 -0
- package/dist/resume-seed.js.map +1 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +10 -1
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +20 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -9
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/recovery.d.ts
CHANGED
|
@@ -189,6 +189,44 @@ export declare function beginFixAttempt(state: SchedState, batchId: string, issu
|
|
|
189
189
|
export declare function resolveFixAttempt(state: SchedState, batchId: string, issue: number, outcome: 'green' | 'red', deps: RecoveryDeps): {
|
|
190
190
|
state: SchedState;
|
|
191
191
|
};
|
|
192
|
+
/** One of a batch's member branches, as it exists on origin (#840). */
|
|
193
|
+
export interface RemoteMemberBranch {
|
|
194
|
+
/** `batch/<batchId>-m<index>-<issue>` */
|
|
195
|
+
branch: string;
|
|
196
|
+
index: number;
|
|
197
|
+
issue: number;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* #840: this batch's member branches that exist on ORIGIN right now —
|
|
201
|
+
* `batch/<batchId>-m<n>-<issue>` (`memberBranchFor`'s shape), matched exactly
|
|
202
|
+
* so another batch whose id merely starts with this one's (`b1-a`, a halved
|
|
203
|
+
* split) or the integration branch (`batch/<id>-<date>`) never matches.
|
|
204
|
+
* Since #840 a member branch outlives its landing — it is deleted when the
|
|
205
|
+
* batch ships or dissolves (`teardownBatch`), so this is also how an
|
|
206
|
+
* aggregate-suite eviction finds the branch a landed member can be parked on.
|
|
207
|
+
* `null` when origin could not be read (never "no branches").
|
|
208
|
+
*/
|
|
209
|
+
export declare function listRemoteMemberBranches(exec: ExecFn, repoDir: string, batchId: string): RemoteMemberBranch[] | null;
|
|
210
|
+
/** What {@link pruneMemberBranches} did (#840). */
|
|
211
|
+
export interface MemberBranchPrune {
|
|
212
|
+
deleted: string[];
|
|
213
|
+
/** Branches a live queue entry still continues from — never deleted. */
|
|
214
|
+
kept: string[];
|
|
215
|
+
/** Branches whose delete failed (left on origin). */
|
|
216
|
+
failed: string[];
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* #840: the batch is over — delete its member branches from origin. They
|
|
220
|
+
* outlive landing so an aggregate-suite eviction can park a landed member on
|
|
221
|
+
* its branch; once the batch ships or dissolves (`teardownBatch`), or is
|
|
222
|
+
* abandoned (`sched abandon --batch`), nothing of the batch needs them —
|
|
223
|
+
* except a branch a NON-terminal queue entry's `failure_evidence.branch`
|
|
224
|
+
* still names (a parked member, or one requeued to continue from it): that
|
|
225
|
+
* branch is the member's work and is kept. One `git push --delete` per ref,
|
|
226
|
+
* so one already-gone ref cannot mask the others' outcome. `null` when
|
|
227
|
+
* origin could not be listed (nothing deleted). Never throws.
|
|
228
|
+
*/
|
|
229
|
+
export declare function pruneMemberBranches(exec: ExecFn, repoDir: string, state: SchedState, batchId: string): MemberBranchPrune | null;
|
|
192
230
|
export interface EvictionInput {
|
|
193
231
|
/** Members to evict; eviction-group partners are added automatically. */
|
|
194
232
|
issues: readonly number[];
|
|
@@ -207,6 +245,12 @@ export interface EvictionOutcome {
|
|
|
207
245
|
evicted: number[];
|
|
208
246
|
/** Members actually put back on the queue (shipped members are not). */
|
|
209
247
|
requeued: number[];
|
|
248
|
+
/**
|
|
249
|
+
* #840: members PARKED `evicted` on their still-live member branch instead
|
|
250
|
+
* of requeued — an operator's `sched requeue` continues them from it.
|
|
251
|
+
* Includes any a dissolve triggered by this eviction parked.
|
|
252
|
+
*/
|
|
253
|
+
parked: number[];
|
|
210
254
|
/** Commits reverted, in the order they were reverted (newest first). */
|
|
211
255
|
reverted: string[];
|
|
212
256
|
/** True when a revert conflicted — the batch dissolves instead. */
|
|
@@ -231,8 +275,9 @@ export interface EvictionOutcome {
|
|
|
231
275
|
*/
|
|
232
276
|
export declare function expandEvictionGroups(batch: BatchEntry, issues: readonly number[]): number[];
|
|
233
277
|
/**
|
|
234
|
-
* `attributing → evicting`: revert the offending members' commits,
|
|
235
|
-
*
|
|
278
|
+
* `attributing → evicting`: revert the offending members' commits, park each
|
|
279
|
+
* on its still-live member branch (#840; else requeue it as full-cycle) with
|
|
280
|
+
* its failure evidence, re-run the suite, and check the
|
|
236
281
|
* batch's configurable dissolve threshold (`DissolvePolicy`, AC2/AC3/AC5).
|
|
237
282
|
* Crossing it has two outcomes (#563): if the re-run suite came back green
|
|
238
283
|
* for the survivors, the batch is PRESERVED — trimmed to its survivors and
|
|
@@ -357,7 +402,15 @@ export interface BlockOptions {
|
|
|
357
402
|
export declare function blockBatch(state: SchedState, batchId: string, opts: BlockOptions, deps: RecoveryDeps): {
|
|
358
403
|
state: SchedState;
|
|
359
404
|
};
|
|
360
|
-
|
|
405
|
+
/**
|
|
406
|
+
* `reship` — rebased and re-shipping; `dissolved` — split into halves (no
|
|
407
|
+
* validated member); #840, with VALIDATED members landed: `regate` — the
|
|
408
|
+
* rebase worked but the suite is red, so the gate re-runs over the landed
|
|
409
|
+
* members; `blocked` — no clean rebase, the landed members wait for an
|
|
410
|
+
* operator (`dissolve-refused:<reason>`). In both #840 outcomes only the
|
|
411
|
+
* unvalidated members were split off.
|
|
412
|
+
*/
|
|
413
|
+
export type PrConflictAction = 'reship' | 'dissolved' | 'regate' | 'blocked';
|
|
361
414
|
export interface PrConflictOutcome {
|
|
362
415
|
state: SchedState;
|
|
363
416
|
action: PrConflictAction;
|
|
@@ -372,8 +425,16 @@ export interface PrConflictOutcome {
|
|
|
372
425
|
*
|
|
373
426
|
* First occurrence: rebase the batch branch onto the base, re-run the suite,
|
|
374
427
|
* and re-ship once. Second occurrence — or a rebase that conflicts, or a suite
|
|
375
|
-
* that is red after a clean rebase —
|
|
376
|
-
*
|
|
428
|
+
* that is red after a clean rebase — gives up on re-shipping as-is:
|
|
429
|
+
*
|
|
430
|
+
* - no VALIDATED member → the batch dissolves into two half-batches: the work
|
|
431
|
+
* is kept, the batch that could not land is not retried a third time.
|
|
432
|
+
* - #840: VALIDATED members exist → they are never re-batched or re-dispatched
|
|
433
|
+
* (`keepLandedAndSplit`): they stay landed on the integration branch, and
|
|
434
|
+
* only the unvalidated members are split into half-batches. When the rebase
|
|
435
|
+
* onto the new base succeeded (red suite) the gate re-runs over the landed
|
|
436
|
+
* members (`validating` — the shape of #822's `sched resume --batch`);
|
|
437
|
+
* otherwise the batch blocks `dissolve-refused:<reason>` for an operator.
|
|
377
438
|
*/
|
|
378
439
|
export declare function handlePrConflict(state: SchedState, batchId: string, deps: RecoveryDeps, opts?: {
|
|
379
440
|
reason?: string;
|
package/dist/recovery.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../src/recovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,WAAW,EAIjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,aAAa,EAAwB,MAAM,UAAU,CAAC;AACpE,OAAO,EAIL,KAAK,gBAAgB,EAGtB,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAkBxC,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,UAAU,EACf,KAAK,UAAU,EAGf,KAAK,cAAc,EAMnB,KAAK,YAAY,EAGjB,KAAK,SAAS,EAEd,KAAK,WAAW,EAEhB,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../src/recovery.ts"],"names":[],"mappings":"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"}
|
package/dist/recovery.js
CHANGED
|
@@ -37,6 +37,8 @@ exports.createExecMilestonePoster = createExecMilestonePoster;
|
|
|
37
37
|
exports.beginAttribution = beginAttribution;
|
|
38
38
|
exports.beginFixAttempt = beginFixAttempt;
|
|
39
39
|
exports.resolveFixAttempt = resolveFixAttempt;
|
|
40
|
+
exports.listRemoteMemberBranches = listRemoteMemberBranches;
|
|
41
|
+
exports.pruneMemberBranches = pruneMemberBranches;
|
|
40
42
|
exports.expandEvictionGroups = expandEvictionGroups;
|
|
41
43
|
exports.evictMembers = evictMembers;
|
|
42
44
|
exports.checkDissolveTrigger = checkDissolveTrigger;
|
|
@@ -302,6 +304,87 @@ function resolveFixAttempt(state, batchId, issue, outcome, deps) {
|
|
|
302
304
|
}), now);
|
|
303
305
|
return { state: next };
|
|
304
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* #840: this batch's member branches that exist on ORIGIN right now —
|
|
309
|
+
* `batch/<batchId>-m<n>-<issue>` (`memberBranchFor`'s shape), matched exactly
|
|
310
|
+
* so another batch whose id merely starts with this one's (`b1-a`, a halved
|
|
311
|
+
* split) or the integration branch (`batch/<id>-<date>`) never matches.
|
|
312
|
+
* Since #840 a member branch outlives its landing — it is deleted when the
|
|
313
|
+
* batch ships or dissolves (`teardownBatch`), so this is also how an
|
|
314
|
+
* aggregate-suite eviction finds the branch a landed member can be parked on.
|
|
315
|
+
* `null` when origin could not be read (never "no branches").
|
|
316
|
+
*/
|
|
317
|
+
function listRemoteMemberBranches(exec, repoDir, batchId) {
|
|
318
|
+
if (!attribution_1.SAFE_REF_RE.test(`batch/${batchId}`))
|
|
319
|
+
return null;
|
|
320
|
+
const out = exec('git', ['ls-remote', '--heads', 'origin', `refs/heads/batch/${batchId}-m*`], repoDir);
|
|
321
|
+
if (out === null)
|
|
322
|
+
return null;
|
|
323
|
+
const escaped = batchId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
324
|
+
const shape = new RegExp(`^refs/heads/(batch/${escaped}-m(\\d+)-(\\d+))$`);
|
|
325
|
+
const found = [];
|
|
326
|
+
for (const line of out.split('\n')) {
|
|
327
|
+
const ref = line.trim().split(/\s+/)[1];
|
|
328
|
+
const match = ref === undefined ? null : shape.exec(ref);
|
|
329
|
+
if (match === null)
|
|
330
|
+
continue;
|
|
331
|
+
found.push({
|
|
332
|
+
branch: match[1],
|
|
333
|
+
index: Number(match[2]),
|
|
334
|
+
issue: Number(match[3]),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return found;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* #840: the live member branch `issue`'s work sits on — the recorded one
|
|
341
|
+
* (parallel run / current serial member) when origin still has it, else the
|
|
342
|
+
* highest-indexed live `batch/<id>-m<n>-<issue>`. `null` when none is live.
|
|
343
|
+
* When origin could not be listed (`live === null`) nothing can be verified:
|
|
344
|
+
* a RECORDED branch (a parallel run's, or the current serial member's) is
|
|
345
|
+
* still parked on — a `sched requeue` onto a branch that turns out to be gone
|
|
346
|
+
* simply starts fresh (`runstate verify` → setup) — and anything else falls
|
|
347
|
+
* back to the full-cycle requeue, journaled by the caller.
|
|
348
|
+
*/
|
|
349
|
+
function liveMemberBranch(batch, issue, live) {
|
|
350
|
+
if (live === null)
|
|
351
|
+
return (0, state_1.recordedMemberBranch)(batch, issue);
|
|
352
|
+
const own = live.filter((b) => b.issue === issue);
|
|
353
|
+
const recorded = (0, state_1.recordedMemberBranch)(batch, issue);
|
|
354
|
+
if (recorded !== null && own.some((b) => b.branch === recorded))
|
|
355
|
+
return recorded;
|
|
356
|
+
return [...own].sort((a, b) => b.index - a.index)[0]?.branch ?? null;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* #840: the batch is over — delete its member branches from origin. They
|
|
360
|
+
* outlive landing so an aggregate-suite eviction can park a landed member on
|
|
361
|
+
* its branch; once the batch ships or dissolves (`teardownBatch`), or is
|
|
362
|
+
* abandoned (`sched abandon --batch`), nothing of the batch needs them —
|
|
363
|
+
* except a branch a NON-terminal queue entry's `failure_evidence.branch`
|
|
364
|
+
* still names (a parked member, or one requeued to continue from it): that
|
|
365
|
+
* branch is the member's work and is kept. One `git push --delete` per ref,
|
|
366
|
+
* so one already-gone ref cannot mask the others' outcome. `null` when
|
|
367
|
+
* origin could not be listed (nothing deleted). Never throws.
|
|
368
|
+
*/
|
|
369
|
+
function pruneMemberBranches(exec, repoDir, state, batchId) {
|
|
370
|
+
const live = listRemoteMemberBranches(exec, repoDir, batchId);
|
|
371
|
+
if (live === null)
|
|
372
|
+
return null;
|
|
373
|
+
const referenced = new Set(state.entries
|
|
374
|
+
.filter((e) => !types_1.TERMINAL_ISSUE_STATUSES.has(e.status))
|
|
375
|
+
.map((e) => e.failure_evidence?.branch)
|
|
376
|
+
.filter((b) => typeof b === 'string'));
|
|
377
|
+
const out = { deleted: [], kept: [], failed: [] };
|
|
378
|
+
for (const { branch } of live) {
|
|
379
|
+
if (referenced.has(branch)) {
|
|
380
|
+
out.kept.push(branch);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const ok = exec('git', ['push', 'origin', '--delete', branch], repoDir) !== null;
|
|
384
|
+
(ok ? out.deleted : out.failed).push(branch);
|
|
385
|
+
}
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
305
388
|
/**
|
|
306
389
|
* Every member that must leave with `issues`: an eviction group reverts
|
|
307
390
|
* together (RFC-0001 §E.4), because a member built on another's API cannot
|
|
@@ -345,8 +428,9 @@ function orderRevertCommits(ranges, targets) {
|
|
|
345
428
|
.map((commit) => commit.sha);
|
|
346
429
|
}
|
|
347
430
|
/**
|
|
348
|
-
* `attributing → evicting`: revert the offending members' commits,
|
|
349
|
-
*
|
|
431
|
+
* `attributing → evicting`: revert the offending members' commits, park each
|
|
432
|
+
* on its still-live member branch (#840; else requeue it as full-cycle) with
|
|
433
|
+
* its failure evidence, re-run the suite, and check the
|
|
350
434
|
* batch's configurable dissolve threshold (`DissolvePolicy`, AC2/AC3/AC5).
|
|
351
435
|
* Crossing it has two outcomes (#563): if the re-run suite came back green
|
|
352
436
|
* for the survivors, the batch is PRESERVED — trimmed to its survivors and
|
|
@@ -378,6 +462,7 @@ function evictMembers(state, batchId, input, deps) {
|
|
|
378
462
|
state: dissolve.state,
|
|
379
463
|
evicted: [],
|
|
380
464
|
requeued: dissolve.requeued,
|
|
465
|
+
parked: dissolve.parked,
|
|
381
466
|
reverted: [],
|
|
382
467
|
conflict: true,
|
|
383
468
|
// #810: a refused dissolve (validated members kept) blocks instead.
|
|
@@ -432,7 +517,11 @@ function evictMembers(state, batchId, input, deps) {
|
|
|
432
517
|
}
|
|
433
518
|
record();
|
|
434
519
|
}
|
|
435
|
-
// Requeue every reverted member with its evidence attached (AC2)
|
|
520
|
+
// Requeue every reverted member with its evidence attached (AC2) — or,
|
|
521
|
+
// since #840, PARK it `evicted` on its member branch when that branch is
|
|
522
|
+
// still live on origin (a landed member's branch now outlives the landing):
|
|
523
|
+
// the reverted work is exactly what is on it, and an operator's `sched
|
|
524
|
+
// requeue` continues it there instead of restarting from the base.
|
|
436
525
|
//
|
|
437
526
|
// A member already in `evictions[]` is skipped entirely (#595 AC1): the
|
|
438
527
|
// reverts above are idempotent (each checks the branch history first), but a
|
|
@@ -444,7 +533,15 @@ function evictMembers(state, batchId, input, deps) {
|
|
|
444
533
|
// never silently dropped.
|
|
445
534
|
const alreadyEvicted = new Map(batch.evictions.map((e) => [e.issue, e]));
|
|
446
535
|
const requeued = [];
|
|
536
|
+
const parked = [];
|
|
447
537
|
const records = [];
|
|
538
|
+
const needsBranch = targets.some((t) => !alreadyEvicted.has(t));
|
|
539
|
+
const live = needsBranch ? listRemoteMemberBranches(deps.exec, deps.repoDir, batchId) : [];
|
|
540
|
+
if (live === null) {
|
|
541
|
+
journal(deps, (0, journal_1.unitEvent)('git-failed', `batch:${batchId}`, {
|
|
542
|
+
detail: `git ls-remote origin batch/${batchId}-m* failed — member branches unverifiable: only a recorded member branch is parked on, other evicted members requeue full-cycle from the base`,
|
|
543
|
+
}), now);
|
|
544
|
+
}
|
|
448
545
|
for (const issue of targets) {
|
|
449
546
|
const prior = alreadyEvicted.get(issue);
|
|
450
547
|
if (prior) {
|
|
@@ -464,18 +561,31 @@ function evictMembers(state, batchId, input, deps) {
|
|
|
464
561
|
reverted_commits: memberCommits,
|
|
465
562
|
at: now.toISOString(),
|
|
466
563
|
};
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
564
|
+
const branch = liveMemberBranch(batch, issue, live);
|
|
565
|
+
const park = branch === null
|
|
566
|
+
? { state: next, parked: false }
|
|
567
|
+
: (0, state_1.parkMember)(next, issue, 'evicted', input.reason, now, {
|
|
568
|
+
failure_evidence: { ...evidence, branch },
|
|
569
|
+
});
|
|
570
|
+
next = park.state;
|
|
571
|
+
if (park.parked) {
|
|
572
|
+
parked.push(issue);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
const result = (0, state_1.requeueMember)(next, issue, { mode: 'full', batch: null }, input.reason, now, {
|
|
576
|
+
failure_evidence: evidence,
|
|
577
|
+
});
|
|
578
|
+
next = result.state;
|
|
579
|
+
if (result.requeued)
|
|
580
|
+
requeued.push(issue);
|
|
581
|
+
}
|
|
473
582
|
records.push({
|
|
474
583
|
issue,
|
|
475
584
|
reason: input.reason,
|
|
476
585
|
attribution: input.attribution,
|
|
477
586
|
reverted_commits: memberCommits,
|
|
478
587
|
group: targets.filter((t) => t !== issue),
|
|
588
|
+
...(park.parked ? { kind: 'evicted', branch } : {}),
|
|
479
589
|
at: now.toISOString(),
|
|
480
590
|
});
|
|
481
591
|
journal(deps, (0, journal_1.unitEvent)('member-evicted', `batch:${batchId}`, {
|
|
@@ -484,9 +594,10 @@ function evictMembers(state, batchId, input, deps) {
|
|
|
484
594
|
// on the branch while the queue says it was evicted, so the batch would
|
|
485
595
|
// ship code it believes it removed. `reverted=0` alone reads as "an
|
|
486
596
|
// empty range", which is why this says it in words.
|
|
487
|
-
detail: memberCommits.length === 0
|
|
597
|
+
detail: (memberCommits.length === 0
|
|
488
598
|
? `${input.reason} reverted=0 — NO commits found for this member on the batch branch; its work was NOT reverted`
|
|
489
|
-
: `${input.reason} reverted=${memberCommits.length}
|
|
599
|
+
: `${input.reason} reverted=${memberCommits.length}`) +
|
|
600
|
+
(park.parked ? ` — parked on ${branch}` : ''),
|
|
490
601
|
}), now);
|
|
491
602
|
}
|
|
492
603
|
// `appendEvictions` stays the state-level backstop; the loop above already
|
|
@@ -507,6 +618,7 @@ function evictMembers(state, batchId, input, deps) {
|
|
|
507
618
|
reason: input.reason,
|
|
508
619
|
evicted: targets.join(',') || 'none',
|
|
509
620
|
requeued: requeued.join(',') || 'none',
|
|
621
|
+
...(parked.length > 0 ? { parked: parked.join(',') } : {}),
|
|
510
622
|
reverted: String(reverted.length),
|
|
511
623
|
attribution: input.attribution,
|
|
512
624
|
...(withoutCommits.length > 0 ? { no_commits: withoutCommits.join(',') } : {}),
|
|
@@ -548,6 +660,7 @@ function evictMembers(state, batchId, input, deps) {
|
|
|
548
660
|
// The surviving members the dissolve requeued belong here too — a caller
|
|
549
661
|
// reading `requeued` must see everything that went back on the queue.
|
|
550
662
|
requeued: [...new Set([...requeued, ...dissolve.requeued])].sort((a, b) => a - b),
|
|
663
|
+
parked: [...new Set([...parked, ...dissolve.parked])].sort((a, b) => a - b),
|
|
551
664
|
reverted,
|
|
552
665
|
conflict: false,
|
|
553
666
|
dissolved,
|
|
@@ -559,6 +672,7 @@ function evictMembers(state, batchId, input, deps) {
|
|
|
559
672
|
state: next,
|
|
560
673
|
evicted: targets,
|
|
561
674
|
requeued,
|
|
675
|
+
parked,
|
|
562
676
|
reverted,
|
|
563
677
|
conflict: false,
|
|
564
678
|
dissolved: false,
|
|
@@ -582,6 +696,7 @@ function revertConflict(state, batchId, targets, deps, now, reverted, detail) {
|
|
|
582
696
|
state: dissolve.state,
|
|
583
697
|
evicted: [...targets],
|
|
584
698
|
requeued: dissolve.requeued,
|
|
699
|
+
parked: dissolve.parked,
|
|
585
700
|
reverted,
|
|
586
701
|
conflict: true,
|
|
587
702
|
// #810: a refused dissolve (validated members kept) blocks instead.
|
|
@@ -715,59 +830,10 @@ function dissolveBatch(state, batchId, opts, deps) {
|
|
|
715
830
|
const requeued = [];
|
|
716
831
|
const parked = [];
|
|
717
832
|
if (strategy === 'halved' && unshipped.length > 0) {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
// alone is evicted rather than silently shipped.
|
|
723
|
-
const pivot = Math.ceil(unshipped.length / 2);
|
|
724
|
-
const taken = new Set(next.batches.map((b) => b.id));
|
|
725
|
-
// A colliding id would produce two batches answering to one name:
|
|
726
|
-
// `findBatch` would return the first and `validateState` would refuse to
|
|
727
|
-
// load the file at all on the next start.
|
|
728
|
-
const freeId = (base) => {
|
|
729
|
-
let id = base;
|
|
730
|
-
let n = 2;
|
|
731
|
-
while (taken.has(id))
|
|
732
|
-
id = `${base}${n++}`;
|
|
733
|
-
taken.add(id);
|
|
734
|
-
return id;
|
|
735
|
-
};
|
|
736
|
-
const halves = [
|
|
737
|
-
{ id: freeId(`${batchId}-a`), members: unshipped.slice(0, pivot) },
|
|
738
|
-
{ id: freeId(`${batchId}-b`), members: unshipped.slice(pivot) },
|
|
739
|
-
].filter((h) => h.members.length > 0);
|
|
740
|
-
for (const half of halves) {
|
|
741
|
-
next = {
|
|
742
|
-
...next,
|
|
743
|
-
batches: [
|
|
744
|
-
...next.batches,
|
|
745
|
-
(0, state_1.createBatch)(half.id, half.members, now, {
|
|
746
|
-
base_branch: batch.base_branch,
|
|
747
|
-
anchor: batch.anchor ?? undefined,
|
|
748
|
-
run_id: batch.run_id ?? undefined,
|
|
749
|
-
dispatch_profile: batch.dispatch_profile ?? undefined,
|
|
750
|
-
// #565: a dissolve split is not a fresh batch — carry the
|
|
751
|
-
// parent's priority forward, or an operator's `--priority`
|
|
752
|
-
// (or `reprioritize`) is silently lost the moment a batch
|
|
753
|
-
// dissolves into halves.
|
|
754
|
-
priority: batch.priority,
|
|
755
|
-
// Groups survive the split, restricted to the members that landed
|
|
756
|
-
// in this half — a group spanning both halves is no longer a group.
|
|
757
|
-
eviction_groups: batch.eviction_groups
|
|
758
|
-
.map((group) => group.filter((m) => half.members.includes(m)))
|
|
759
|
-
.filter((group) => group.length > 1),
|
|
760
|
-
}),
|
|
761
|
-
],
|
|
762
|
-
};
|
|
763
|
-
newBatches.push(half.id);
|
|
764
|
-
for (const issue of half.members) {
|
|
765
|
-
const result = (0, state_1.requeueMember)(next, issue, { mode: 'slot', batch: half.id }, opts.reason, now, { failure_evidence: { ...evidence, batch: half.id } });
|
|
766
|
-
next = result.state;
|
|
767
|
-
if (result.requeued)
|
|
768
|
-
requeued.push(issue);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
833
|
+
const split = splitIntoHalves(next, batch, unshipped, opts.reason, evidence, now);
|
|
834
|
+
next = split.state;
|
|
835
|
+
newBatches.push(...split.newBatches);
|
|
836
|
+
requeued.push(...split.requeued);
|
|
771
837
|
journal(deps, (0, journal_1.unitEvent)('batch-split', `batch:${batchId}`, { detail: newBatches.join(',') }), now);
|
|
772
838
|
}
|
|
773
839
|
else {
|
|
@@ -813,6 +879,76 @@ function dissolveBatch(state, batchId, opts, deps) {
|
|
|
813
879
|
validated: [],
|
|
814
880
|
};
|
|
815
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* Split `members` of `batch` into two fresh `forming` half-batches and requeue
|
|
884
|
+
* each member into its half (§F.9: smaller batches, not abandoned work) — the
|
|
885
|
+
* `halved` dissolve's split, shared since #840 with `handlePrConflict`'s
|
|
886
|
+
* keep-landed path (which splits only the UNVALIDATED members).
|
|
887
|
+
*
|
|
888
|
+
* Split by POSITION, not by coupling: the halves are the first and second
|
|
889
|
+
* half of `members` in member order. A coupled eviction group straddling the
|
|
890
|
+
* pivot is therefore broken up — accepted, because both halves re-run
|
|
891
|
+
* through the same validate/evict rails, where a member that cannot stand
|
|
892
|
+
* alone is evicted rather than silently shipped.
|
|
893
|
+
*/
|
|
894
|
+
function splitIntoHalves(state, batch, members, reason, evidence, now) {
|
|
895
|
+
let next = state;
|
|
896
|
+
const newBatches = [];
|
|
897
|
+
const requeued = [];
|
|
898
|
+
if (members.length === 0)
|
|
899
|
+
return { state: next, newBatches, requeued };
|
|
900
|
+
const pivot = Math.ceil(members.length / 2);
|
|
901
|
+
const taken = new Set(next.batches.map((b) => b.id));
|
|
902
|
+
// A colliding id would produce two batches answering to one name:
|
|
903
|
+
// `findBatch` would return the first and `validateState` would refuse to
|
|
904
|
+
// load the file at all on the next start.
|
|
905
|
+
const freeId = (base) => {
|
|
906
|
+
let id = base;
|
|
907
|
+
let n = 2;
|
|
908
|
+
while (taken.has(id))
|
|
909
|
+
id = `${base}${n++}`;
|
|
910
|
+
taken.add(id);
|
|
911
|
+
return id;
|
|
912
|
+
};
|
|
913
|
+
const halves = [
|
|
914
|
+
{ id: freeId(`${batch.id}-a`), members: members.slice(0, pivot) },
|
|
915
|
+
{ id: freeId(`${batch.id}-b`), members: members.slice(pivot) },
|
|
916
|
+
].filter((h) => h.members.length > 0);
|
|
917
|
+
for (const half of halves) {
|
|
918
|
+
next = {
|
|
919
|
+
...next,
|
|
920
|
+
batches: [
|
|
921
|
+
...next.batches,
|
|
922
|
+
(0, state_1.createBatch)(half.id, half.members, now, {
|
|
923
|
+
base_branch: batch.base_branch,
|
|
924
|
+
anchor: batch.anchor ?? undefined,
|
|
925
|
+
run_id: batch.run_id ?? undefined,
|
|
926
|
+
dispatch_profile: batch.dispatch_profile ?? undefined,
|
|
927
|
+
// #565: a dissolve split is not a fresh batch — carry the
|
|
928
|
+
// parent's priority forward, or an operator's `--priority`
|
|
929
|
+
// (or `reprioritize`) is silently lost the moment a batch
|
|
930
|
+
// dissolves into halves.
|
|
931
|
+
priority: batch.priority,
|
|
932
|
+
// Groups survive the split, restricted to the members that landed
|
|
933
|
+
// in this half — a group spanning both halves is no longer a group.
|
|
934
|
+
eviction_groups: batch.eviction_groups
|
|
935
|
+
.map((group) => group.filter((m) => half.members.includes(m)))
|
|
936
|
+
.filter((group) => group.length > 1),
|
|
937
|
+
}),
|
|
938
|
+
],
|
|
939
|
+
};
|
|
940
|
+
newBatches.push(half.id);
|
|
941
|
+
for (const issue of half.members) {
|
|
942
|
+
const result = (0, state_1.requeueMember)(next, issue, { mode: 'slot', batch: half.id }, reason, now, {
|
|
943
|
+
failure_evidence: { ...evidence, batch: half.id },
|
|
944
|
+
});
|
|
945
|
+
next = result.state;
|
|
946
|
+
if (result.requeued)
|
|
947
|
+
requeued.push(issue);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return { state: next, newBatches, requeued };
|
|
951
|
+
}
|
|
816
952
|
/**
|
|
817
953
|
* #810: sort a dissolving batch's members. `preserved` = no longer the
|
|
818
954
|
* batch's to decide (shipped/terminal, redispatched elsewhere, or already
|
|
@@ -1050,8 +1186,16 @@ function blockBatch(state, batchId, opts, deps) {
|
|
|
1050
1186
|
*
|
|
1051
1187
|
* First occurrence: rebase the batch branch onto the base, re-run the suite,
|
|
1052
1188
|
* and re-ship once. Second occurrence — or a rebase that conflicts, or a suite
|
|
1053
|
-
* that is red after a clean rebase —
|
|
1054
|
-
*
|
|
1189
|
+
* that is red after a clean rebase — gives up on re-shipping as-is:
|
|
1190
|
+
*
|
|
1191
|
+
* - no VALIDATED member → the batch dissolves into two half-batches: the work
|
|
1192
|
+
* is kept, the batch that could not land is not retried a third time.
|
|
1193
|
+
* - #840: VALIDATED members exist → they are never re-batched or re-dispatched
|
|
1194
|
+
* (`keepLandedAndSplit`): they stay landed on the integration branch, and
|
|
1195
|
+
* only the unvalidated members are split into half-batches. When the rebase
|
|
1196
|
+
* onto the new base succeeded (red suite) the gate re-runs over the landed
|
|
1197
|
+
* members (`validating` — the shape of #822's `sched resume --batch`);
|
|
1198
|
+
* otherwise the batch blocks `dissolve-refused:<reason>` for an operator.
|
|
1055
1199
|
*/
|
|
1056
1200
|
function handlePrConflict(state, batchId, deps, opts = {}) {
|
|
1057
1201
|
const now = clock(deps);
|
|
@@ -1065,8 +1209,20 @@ function handlePrConflict(state, batchId, deps, opts = {}) {
|
|
|
1065
1209
|
if (batch.status !== 'rebasing') {
|
|
1066
1210
|
next = (0, state_1.transitionBatch)(next, batchId, 'rebasing', {}, now);
|
|
1067
1211
|
}
|
|
1068
|
-
/**
|
|
1212
|
+
/**
|
|
1213
|
+
* Every give-up path here ends the same way: keep the work. Validated
|
|
1214
|
+
* members stay landed (#840); everything else is halved.
|
|
1215
|
+
*/
|
|
1069
1216
|
const bailToHalves = (from, bailReason, rebased = false, suite = null) => {
|
|
1217
|
+
const { validated } = classifyDissolveMembers(from, batchOrThrow(from, batchId));
|
|
1218
|
+
if (validated.length > 0) {
|
|
1219
|
+
return keepLandedAndSplit(from, batchId, deps, {
|
|
1220
|
+
reason: bailReason,
|
|
1221
|
+
validated,
|
|
1222
|
+
rebased,
|
|
1223
|
+
suite,
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1070
1226
|
const dissolve = dissolveBatch(from, batchId, { strategy: 'halved', reason: bailReason, milestonePhase: 'batch-ship' }, deps);
|
|
1071
1227
|
return { state: dissolve.state, action: 'dissolved', rebased, suite, dissolve };
|
|
1072
1228
|
};
|
|
@@ -1126,4 +1282,98 @@ function handlePrConflict(state, batchId, deps, opts = {}) {
|
|
|
1126
1282
|
}, now);
|
|
1127
1283
|
return { state: next, action: 'reship', rebased: true, suite };
|
|
1128
1284
|
}
|
|
1285
|
+
/**
|
|
1286
|
+
* #840: `handlePrConflict`'s give-up path when VALIDATED members exist. They
|
|
1287
|
+
* are never re-batched or re-dispatched: they stay in this batch, landed on
|
|
1288
|
+
* its integration branch. Only the unshipped, unvalidated members leave — split
|
|
1289
|
+
* into half-batches exactly as a `halved` dissolve would, and trimmed out of
|
|
1290
|
+
* this batch (with their groups/ranges, like `preserveSurvivors`). Then:
|
|
1291
|
+
*
|
|
1292
|
+
* - `rebased` (the rebase onto the new base worked, the suite is red) →
|
|
1293
|
+
* `re-validating → validating` with `executing_member` pinned to the end,
|
|
1294
|
+
* so the engine re-runs the gate over the landed members and attributes /
|
|
1295
|
+
* evicts on the ordinary rail — the same shape as `sched resume --batch`.
|
|
1296
|
+
* - otherwise (rebase conflict, failed fetch, a recurred conflict, …) → the
|
|
1297
|
+
* batch BLOCKS `dissolve-refused:<reason>`: the landed work waits for an
|
|
1298
|
+
* operator — resolve the conflict on the recorded PR by hand
|
|
1299
|
+
* (`reconcileStaleBlockedBatches` settles the batch once it merges), or
|
|
1300
|
+
* close that PR and `sched resume --batch` (which refuses while a PR is
|
|
1301
|
+
* recorded), or `sched abandon --batch`.
|
|
1302
|
+
*
|
|
1303
|
+
* Library behaviour: the engine does not yet route a conflicting batch PR
|
|
1304
|
+
* here (#867).
|
|
1305
|
+
*/
|
|
1306
|
+
function keepLandedAndSplit(state, batchId, deps, ctx) {
|
|
1307
|
+
const now = clock(deps);
|
|
1308
|
+
const batch = batchOrThrow(state, batchId);
|
|
1309
|
+
const { unshipped } = classifyDissolveMembers(state, batch);
|
|
1310
|
+
// A member with a recorded range has commits ON the integration branch this
|
|
1311
|
+
// batch keeps — splitting it off would ship its code here AND re-run it in
|
|
1312
|
+
// a half. It stays (not validated, so the tail never names it; the re-run
|
|
1313
|
+
// gate or the operator decides it). Only unlanded, unvalidated members go.
|
|
1314
|
+
const landed = new Set(batch.ranges.map((r) => r.issue));
|
|
1315
|
+
const toSplit = unshipped.filter((issue) => !ctx.validated.includes(issue) && !landed.has(issue));
|
|
1316
|
+
const evidence = (0, state_1.memberExitEvidence)(batchId, ctx.reason, null, now);
|
|
1317
|
+
const split = splitIntoHalves(state, batch, toSplit, ctx.reason, evidence, now);
|
|
1318
|
+
let next = split.state;
|
|
1319
|
+
if (split.newBatches.length > 0) {
|
|
1320
|
+
journal(deps, (0, journal_1.unitEvent)('batch-split', `batch:${batchId}`, {
|
|
1321
|
+
detail: `${split.newBatches.join(',')} — unvalidated member(s) ${toSplit.join(',')} only; validated ${ctx.validated.join(',')} stay landed`,
|
|
1322
|
+
}), now);
|
|
1323
|
+
}
|
|
1324
|
+
const kept = batch.members.filter((issue) => !toSplit.includes(issue));
|
|
1325
|
+
next = (0, state_1.patchBatch)(next, batchId, {
|
|
1326
|
+
members: kept,
|
|
1327
|
+
eviction_groups: batch.eviction_groups
|
|
1328
|
+
.map((group) => group.filter((m) => kept.includes(m)))
|
|
1329
|
+
.filter((group) => group.length > 1),
|
|
1330
|
+
ranges: batch.ranges.filter((r) => kept.includes(r.issue)),
|
|
1331
|
+
executing_member: kept.length,
|
|
1332
|
+
}, now);
|
|
1333
|
+
const dissolve = {
|
|
1334
|
+
state: next,
|
|
1335
|
+
requeued: split.requeued,
|
|
1336
|
+
preserved: ctx.validated,
|
|
1337
|
+
newBatches: split.newBatches,
|
|
1338
|
+
parked: [],
|
|
1339
|
+
blocked: !ctx.rebased,
|
|
1340
|
+
validated: ctx.validated,
|
|
1341
|
+
};
|
|
1342
|
+
if (ctx.rebased) {
|
|
1343
|
+
next = (0, state_1.transitionBatch)(next, batchId, 'validating', {}, now);
|
|
1344
|
+
journal(deps, (0, journal_1.unitEvent)('batch-regate', `batch:${batchId}`, {
|
|
1345
|
+
detail: `${ctx.reason} — gate re-runs over landed member(s) ${ctx.validated.join(',')} on the rebased branch; split=${split.newBatches.join(',') || 'none'}`,
|
|
1346
|
+
}), now);
|
|
1347
|
+
return {
|
|
1348
|
+
state: next,
|
|
1349
|
+
action: 'regate',
|
|
1350
|
+
rebased: true,
|
|
1351
|
+
suite: ctx.suite,
|
|
1352
|
+
dissolve: { ...dissolve, state: next },
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
const blockedReason = `${exports.DISSOLVE_REFUSED_PREFIX}${ctx.reason}`;
|
|
1356
|
+
next = (0, state_1.transitionBatch)(next, batchId, 'blocked', { blocked_reason: blockedReason }, now);
|
|
1357
|
+
journal(deps, (0, journal_1.unitEvent)('batch-blocked', `batch:${batchId}`, {
|
|
1358
|
+
detail: `${blockedReason} — validated member(s) ${ctx.validated.join(',')} stay landed on ${batch.branch ?? 'the integration branch'}; split=${split.newBatches.join(',') || 'none'}`,
|
|
1359
|
+
}), now);
|
|
1360
|
+
post(deps, batchOrThrow(next, batchId), {
|
|
1361
|
+
phase: 'batch-ship',
|
|
1362
|
+
status: 'blocked',
|
|
1363
|
+
kv: {
|
|
1364
|
+
reason: blockedReason,
|
|
1365
|
+
dissolved: 'false',
|
|
1366
|
+
validated: ctx.validated.join(','),
|
|
1367
|
+
requeued: split.requeued.join(',') || 'none',
|
|
1368
|
+
...(split.newBatches.length > 0 ? { split_into: split.newBatches.join(',') } : {}),
|
|
1369
|
+
},
|
|
1370
|
+
}, now);
|
|
1371
|
+
return {
|
|
1372
|
+
state: next,
|
|
1373
|
+
action: 'blocked',
|
|
1374
|
+
rebased: false,
|
|
1375
|
+
suite: ctx.suite,
|
|
1376
|
+
dissolve: { ...dissolve, state: next },
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1129
1379
|
//# sourceMappingURL=recovery.js.map
|