@ai-dossier/sched 0.21.1 → 0.22.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 +43 -10
- package/dist/attribution.d.ts +42 -0
- package/dist/attribution.d.ts.map +1 -1
- package/dist/attribution.js +89 -0
- package/dist/attribution.js.map +1 -1
- package/dist/batch-dispatch.d.ts +11 -2
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +123 -46
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +28 -1
- package/dist/recovery.js.map +1 -1
- package/dist/state.d.ts +25 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +45 -0
- package/dist/state.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +7 -1
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -331,7 +331,20 @@ awaiting-merge (CONFLICTING | auto-merge-blocked)
|
|
|
331
331
|
commits were actually found and reverted — the batch is PRESERVED instead: trimmed to
|
|
332
332
|
its survivors and carried straight to `reviewing`, only the evicted members requeue. A
|
|
333
333
|
red or unreadable re-run, or an evicted member whose commits were never found on the
|
|
334
|
-
branch, still dissolves in full.
|
|
334
|
+
branch, still dissolves in full. A member already named in `batch.evictions` is never
|
|
335
|
+
recorded twice (#595): `appendEvictions` (the one place either eviction rail —
|
|
336
|
+
`evictMembers` here or `evictMemberDirectly`'s no-commits path in
|
|
337
|
+
[Batch dispatch (#523)](#batch-dispatch-523) — appends to `evictions[]`) is a no-op
|
|
338
|
+
for a repeat issue and journals `eviction-duplicate` instead; the repeat requeue is
|
|
339
|
+
skipped with it, so a second call cannot overwrite the first eviction's
|
|
340
|
+
`failure_evidence` or kill a live re-dispatch of that member. **Read
|
|
341
|
+
`docs/agent-traps.md` before "fixing" the dissolve rule:** the dissolve trigger has
|
|
342
|
+
counted DISTINCT member ids since #572 (`evictedMemberIds`) and was never inflatable
|
|
343
|
+
by a duplicate. What a duplicate did inflate is the raw `evictions[]` array itself —
|
|
344
|
+
any eviction-RATE metric derived from it (RFC-0001 §E.5) and `sched status`'s eviction
|
|
345
|
+
column. `buildStatusReport` de-dups on read as well (`distinctEvictions`), so a
|
|
346
|
+
`state.json` persisted before this fix shows one row per member in both `sched status`
|
|
347
|
+
and `sched status --json`; the stored array is left as written.
|
|
335
348
|
4. **Dissolve (AC3)** — `dissolveBatch` marks the batch `dissolved` and requeues every
|
|
336
349
|
UNSHIPPED member: `full` (each as its own full-cycle run), `halved` (one or two fresh
|
|
337
350
|
`forming` half-batches — a single remaining member yields one — entries retagged,
|
|
@@ -356,8 +369,11 @@ awaiting-merge (CONFLICTING | auto-merge-blocked)
|
|
|
356
369
|
with no `anchor` or no `run_id` cannot post — the CLI requires both — so the milestone
|
|
357
370
|
it could not post is journaled in full instead of vanishing.
|
|
358
371
|
|
|
359
|
-
|
|
360
|
-
`fix-resolved`, `member-evicted`, `
|
|
372
|
+
Thirteen journal events carry the detail: `suite-failed`, `attributed`, `fix-dispatched`,
|
|
373
|
+
`fix-resolved`, `member-evicted`, `eviction-duplicate` (#595 — a second eviction call
|
|
374
|
+
named a member already in `evictions[]`; the append and the requeue are both no-ops and
|
|
375
|
+
the attempt is journaled rather than dropped), `revert-conflict`, `batch-rebased`,
|
|
376
|
+
`batch-dissolved`,
|
|
361
377
|
`batch-preserved` (#563 — the dissolve threshold was crossed but the survivors' re-run
|
|
362
378
|
suite came back green, so the batch ships them instead of dissolving), `batch-blocked`
|
|
363
379
|
(#562 — the suite report was unreadable), `batch-split` and `milestone-post-failed`, plus
|
|
@@ -483,12 +499,18 @@ failure rails: executing → dissolving (a member self-reports blocked)
|
|
|
483
499
|
kept on `BatchEntry.ranges` for eviction. An incremental gate (`ai-dossier cap run
|
|
484
500
|
typecheck.run` / `test.focused`, when the repo has a manifest) runs after each member
|
|
485
501
|
before advancing — a second, independent check that the member's self-reported "done"
|
|
486
|
-
is real.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
(`
|
|
490
|
-
|
|
491
|
-
|
|
502
|
+
is real. Four-way outcome policy (#583, split further by #594): a `task-failed` whose
|
|
503
|
+
`output_tail` carries recognizable evidence that the capability EARNED it — failing-test
|
|
504
|
+
output for a `test.*` capability, compiler/build errors for the others
|
|
505
|
+
(`hasEarnedFailureEvidence`) — evicts the member (same rail as a self-reported block);
|
|
506
|
+
a `task-failed` with an empty or framing-only capture proves nothing and joins
|
|
507
|
+
`automation-broken`/`capability-unavailable` — the gate itself couldn't reach a verdict
|
|
508
|
+
— on the block-the-batch path instead of silently proceeding (`gate-inconclusive:<cap>`,
|
|
509
|
+
`member_gates`/`blocked_reason` on `BatchEntry`, surfaced in `sched status`; the journal
|
|
510
|
+
detail names WHICH of the two branches fired). `sched resume --batch <id>` re-runs the
|
|
511
|
+
gate later to resolve the block once the capability is fixed, and applies the same
|
|
512
|
+
evidence bar on the recheck — a still-unevidenced `task-failed` stays blocked rather
|
|
513
|
+
than evicting on a resume.
|
|
492
514
|
- **The batch's single slot is claimed FRESH for each live step** (a member, the tail
|
|
493
515
|
agent, the report agent, a bounded fix agent) — never held across a wait. The aggregate
|
|
494
516
|
suite itself runs with NO slot claimed at all (deterministic engine work, not an LLM
|
|
@@ -532,7 +554,9 @@ New journal events: `batch-setup-done`, `batch-setup-failed`, `member-advanced`,
|
|
|
532
554
|
claim emits neither). `gate-inconclusive` (#583 — the incremental gate came back
|
|
533
555
|
`automation-broken`/`capability-unavailable` rather than a definite `ok`/`task-failed`;
|
|
534
556
|
sits alongside `batch-blocked` as the per-member analogue of the aggregate suite's
|
|
535
|
-
"block, don't dissolve" precedent
|
|
557
|
+
"block, don't dissolve" precedent; #594 routes an unevidenced `task-failed` here too).
|
|
558
|
+
`eviction-duplicate` (#595 — `evictMemberDirectly` and `evictMembers` both emit it when
|
|
559
|
+
asked to evict a member already in `evictions[]`). Member/tail/report/fix-agent spawn, progress,
|
|
536
560
|
completion and park events reuse the existing unit-generic names (`assigned`/`spawned`/`unit-failed`/
|
|
537
561
|
`external-advance`/`pr-parked`/`merge-accepted`/`report-dispatched`/`teardown-done`/
|
|
538
562
|
`teardown-failed`) with `unit = batch:<id>`.
|
|
@@ -601,6 +625,11 @@ import {
|
|
|
601
625
|
parseVitestJson, // vitest --reporter=json → failing tests
|
|
602
626
|
isReadableVitestReport, // #562: a parseable { testResults: [...] } document exists —
|
|
603
627
|
// distinct from "zero failures"
|
|
628
|
+
hasFailingTestEvidence, // #594: an output_tail that PROVES a suite ran and went red —
|
|
629
|
+
// an empty/framing-only capture, or a green report, is not
|
|
630
|
+
// a real task-failed
|
|
631
|
+
hasEarnedFailureEvidence, // #594: the same bar per capability — test.* is held to
|
|
632
|
+
// failing-test output, others may prove it with compiler errors
|
|
604
633
|
parseBoundaryCommits, // git log → issue-boundary commits via the (#N) trailer
|
|
605
634
|
memberRanges, // boundary commits → each member's commit list
|
|
606
635
|
runAttributionBisect, // stage-2: real git bisect over the failing tests only
|
|
@@ -616,6 +645,10 @@ import {
|
|
|
616
645
|
createExecMilestonePoster, // batch milestones via `ai-dossier runstate post`
|
|
617
646
|
expandEvictionGroups, // members that must revert together (§E.4 eviction groups)
|
|
618
647
|
requeueMember, // the one requeue path abandon/evict/dissolve all take
|
|
648
|
+
appendEvictions, // #595: the one evictions[] append — skips an issue already
|
|
649
|
+
// recorded, returns the duplicates so the caller can journal
|
|
650
|
+
distinctEvictions, // #595 read side: one record per member for a legacy state.json
|
|
651
|
+
duplicateEvictionDetail, // the one eviction-duplicate wording, shared by both rails
|
|
619
652
|
isPreservedMember, // the single definition of "already green"
|
|
620
653
|
createBatch, // the single BatchEntry constructor
|
|
621
654
|
type RecoveryDeps, // inject exec/repoDir/journal/milestone-poster/suite-runner/clock
|
package/dist/attribution.d.ts
CHANGED
|
@@ -75,6 +75,48 @@ export declare function parseVitestJson(stdout: string | null): FailingTest[];
|
|
|
75
75
|
* exit code that happens to carry no parseable body.
|
|
76
76
|
*/
|
|
77
77
|
export declare function isReadableVitestReport(stdout: string | null): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* `isReadableVitestReport` generalized to the per-member incremental gate's
|
|
80
|
+
* `output_tail` (#594) — a free-form capture of a capability's combined
|
|
81
|
+
* stdout+stderr, not necessarily a `--reporter=json` document (`test.focused`
|
|
82
|
+
* commonly runs a runner's default text reporter, or a wrapper script's own
|
|
83
|
+
* framing around one). A `task-failed` outcome is only as trustworthy as the
|
|
84
|
+
* evidence behind it: a vitest JSON report **naming at least one failed
|
|
85
|
+
* assertion**, or one of `FAILING_TEST_MARKERS`, means a suite genuinely ran
|
|
86
|
+
* and found something red. Anything else — an empty capture, or a body that is
|
|
87
|
+
* only a script's own framing (the pilot attempt 4 shape: 765 bytes of wrapper
|
|
88
|
+
* output and a `tee: /dev/stderr` error, zero test output) — is
|
|
89
|
+
* indistinguishable from a capability that fabricated its exit code.
|
|
90
|
+
*
|
|
91
|
+
* A *readable but green* JSON report is deliberately not evidence: a wrapper
|
|
92
|
+
* that emits a passing `--reporter=json` document and then exits 1 is the
|
|
93
|
+
* fabricated-exit-code shape itself, so `isReadableVitestReport` (which is
|
|
94
|
+
* true for zero failures by design, #562) is the wrong test here.
|
|
95
|
+
*
|
|
96
|
+
* `outputTail` absent entirely (`null`/`undefined`) is NOT the same as
|
|
97
|
+
* "captured and empty": some callers never populate it at all, and a caller
|
|
98
|
+
* that never tried to capture output has no basis to second-guess an earned
|
|
99
|
+
* exit code — this returns `true` (trust the outcome) in that case, matching
|
|
100
|
+
* every `runIncrementalGate` caller that predates `output_tail` (#583). The
|
|
101
|
+
* production runner never reaches that branch: it falls back to the
|
|
102
|
+
* subprocess's own captured output when the envelope omits `output_tail`
|
|
103
|
+
* (`cli/src/commands/sched.ts`), so a stale capability build cannot silently
|
|
104
|
+
* buy back the pre-#594 evict-on-any-`task-failed` behaviour.
|
|
105
|
+
*/
|
|
106
|
+
export declare function hasFailingTestEvidence(outputTail: string | null | undefined): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* The evidence bar for a `task-failed` from capability `capabilityId` (#594).
|
|
109
|
+
*
|
|
110
|
+
* The incremental gate runs `typecheck.run` alongside `test.focused`, and a
|
|
111
|
+
* real type error carries none of `FAILING_TEST_MARKERS` — gating it on
|
|
112
|
+
* test-shaped evidence would route every genuine build break to the
|
|
113
|
+
* block-the-batch path and wedge the batch there, since `sched resume --batch`
|
|
114
|
+
* would re-run the same deterministically failing typecheck forever. So a
|
|
115
|
+
* non-test capability may prove itself with compiler markers too; only a test
|
|
116
|
+
* capability is held to test output, which is where the fabricated exit codes
|
|
117
|
+
* were observed.
|
|
118
|
+
*/
|
|
119
|
+
export declare function hasEarnedFailureEvidence(capabilityId: string, outputTail: string | null | undefined): boolean;
|
|
78
120
|
/** One commit on the batch branch, with the member it belongs to. */
|
|
79
121
|
export interface BoundaryCommit {
|
|
80
122
|
sha: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attribution.d.ts","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,yEAAyE;IACzE,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,2DAA2D;IAC3D,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,qDAAqD;AACrD,MAAM,WAAW,kBAAkB;IACjC,kEAAkE;IAClE,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACvC,0DAA0D;IAC1D,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,2DAA2D;IAC3D,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAEnE;AAqCD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,UAAU,EAAE,SAAS,eAAe,EAAE,GACrC,kBAAkB,CAwBpB;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAE9E;AAID;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,EAAE,CA0BpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAMrE;AAiCD,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AASD;;;;GAIG;AACH,eAAO,MAAM,MAAM,QAAsB,CAAC;AAE1C;;;;;GAKG;AACH,eAAO,MAAM,WAAW,QAAkC,CAAC;AAE3D;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,cAAc,EAAE,CAalF;AAED,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;;;OAIG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,WAAW,EAAE,CAgB/E;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS9F"}
|
|
1
|
+
{"version":3,"file":"attribution.d.ts","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"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"}
|
package/dist/attribution.js
CHANGED
|
@@ -23,6 +23,8 @@ exports.attributeByOverlap = attributeByOverlap;
|
|
|
23
23
|
exports.offendersOf = offendersOf;
|
|
24
24
|
exports.parseVitestJson = parseVitestJson;
|
|
25
25
|
exports.isReadableVitestReport = isReadableVitestReport;
|
|
26
|
+
exports.hasFailingTestEvidence = hasFailingTestEvidence;
|
|
27
|
+
exports.hasEarnedFailureEvidence = hasEarnedFailureEvidence;
|
|
26
28
|
exports.parseBoundaryCommits = parseBoundaryCommits;
|
|
27
29
|
exports.memberRanges = memberRanges;
|
|
28
30
|
exports.memberOfCommit = memberOfCommit;
|
|
@@ -157,6 +159,93 @@ function isReadableVitestReport(stdout) {
|
|
|
157
159
|
const results = parsed.testResults;
|
|
158
160
|
return Array.isArray(results);
|
|
159
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Markers a test runner prints only when a suite actually ran and went red.
|
|
164
|
+
*
|
|
165
|
+
* Every one is anchored to the start of a line (or to a digit-led count), which
|
|
166
|
+
* is the whole point: an unanchored `/fail/i` also matches a wrapper script's
|
|
167
|
+
* own prose about the run. The recorded pilot attempt-4 body ends
|
|
168
|
+
* `... run test exited 1 — real test failure.` — "failure" is a substring
|
|
169
|
+
* match, so a bare `/fail/i` accepts the exact 765-byte capture that carries
|
|
170
|
+
* zero test output (`docs/reports/batch-pilot-2-execution.md`), which is the
|
|
171
|
+
* body #594 exists to reject. Same for pnpm's universal
|
|
172
|
+
* `ELIFECYCLE Command failed with exit code 1.` and npm's
|
|
173
|
+
* `Lifecycle script \`test\` failed`.
|
|
174
|
+
*/
|
|
175
|
+
const FAILING_TEST_MARKERS = [
|
|
176
|
+
/^\s*(?:❯\s*)?FAIL(?:ED)?\b/m, // vitest/jest per-file line
|
|
177
|
+
/^\s*[✗×✕●]\s/m, // vitest / jest / mocha per-test bullet
|
|
178
|
+
/^not ok \d+/m, // TAP
|
|
179
|
+
/^\s*\d+\s+(?:tests?|specs?|assertions?|examples?)?\s*fail(?:ed|ing|ures?)\b/im, // "1 failing", "2 tests failed"
|
|
180
|
+
/^[\s=]*(?:Tests?|Test Files|Specs?|Examples?)\b[^\n]*?\b\d+\s+fail(?:ed|ing|ures?)\b/im, // runner summary line
|
|
181
|
+
];
|
|
182
|
+
/** Markers a compiler / build tool prints for a genuine build failure. */
|
|
183
|
+
const BUILD_FAILURE_MARKERS = [
|
|
184
|
+
/^[^\n]*\berror TS\d+/m, // tsc
|
|
185
|
+
/^[^\n]*\(\d+,\d+\):\s*error\b/m, // tsc / msbuild positional form
|
|
186
|
+
/^[^\n]*?:\d+:\d+:\s*error\b/im, // gcc/clang/esbuild/swc positional form
|
|
187
|
+
/^\s*Found \d+ errors?\b/im, // tsc / mypy summary
|
|
188
|
+
/^\s*error\[?[A-Z]?\d*\]?:/im, // rustc / generic "error: ..."
|
|
189
|
+
];
|
|
190
|
+
/**
|
|
191
|
+
* `isReadableVitestReport` generalized to the per-member incremental gate's
|
|
192
|
+
* `output_tail` (#594) — a free-form capture of a capability's combined
|
|
193
|
+
* stdout+stderr, not necessarily a `--reporter=json` document (`test.focused`
|
|
194
|
+
* commonly runs a runner's default text reporter, or a wrapper script's own
|
|
195
|
+
* framing around one). A `task-failed` outcome is only as trustworthy as the
|
|
196
|
+
* evidence behind it: a vitest JSON report **naming at least one failed
|
|
197
|
+
* assertion**, or one of `FAILING_TEST_MARKERS`, means a suite genuinely ran
|
|
198
|
+
* and found something red. Anything else — an empty capture, or a body that is
|
|
199
|
+
* only a script's own framing (the pilot attempt 4 shape: 765 bytes of wrapper
|
|
200
|
+
* output and a `tee: /dev/stderr` error, zero test output) — is
|
|
201
|
+
* indistinguishable from a capability that fabricated its exit code.
|
|
202
|
+
*
|
|
203
|
+
* A *readable but green* JSON report is deliberately not evidence: a wrapper
|
|
204
|
+
* that emits a passing `--reporter=json` document and then exits 1 is the
|
|
205
|
+
* fabricated-exit-code shape itself, so `isReadableVitestReport` (which is
|
|
206
|
+
* true for zero failures by design, #562) is the wrong test here.
|
|
207
|
+
*
|
|
208
|
+
* `outputTail` absent entirely (`null`/`undefined`) is NOT the same as
|
|
209
|
+
* "captured and empty": some callers never populate it at all, and a caller
|
|
210
|
+
* that never tried to capture output has no basis to second-guess an earned
|
|
211
|
+
* exit code — this returns `true` (trust the outcome) in that case, matching
|
|
212
|
+
* every `runIncrementalGate` caller that predates `output_tail` (#583). The
|
|
213
|
+
* production runner never reaches that branch: it falls back to the
|
|
214
|
+
* subprocess's own captured output when the envelope omits `output_tail`
|
|
215
|
+
* (`cli/src/commands/sched.ts`), so a stale capability build cannot silently
|
|
216
|
+
* buy back the pre-#594 evict-on-any-`task-failed` behaviour.
|
|
217
|
+
*/
|
|
218
|
+
function hasFailingTestEvidence(outputTail) {
|
|
219
|
+
if (outputTail === null || outputTail === undefined)
|
|
220
|
+
return true;
|
|
221
|
+
const text = outputTail.trim();
|
|
222
|
+
if (text.length === 0)
|
|
223
|
+
return false;
|
|
224
|
+
if (isReadableVitestReport(text))
|
|
225
|
+
return parseVitestJson(text).length > 0;
|
|
226
|
+
return FAILING_TEST_MARKERS.some((re) => re.test(text));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The evidence bar for a `task-failed` from capability `capabilityId` (#594).
|
|
230
|
+
*
|
|
231
|
+
* The incremental gate runs `typecheck.run` alongside `test.focused`, and a
|
|
232
|
+
* real type error carries none of `FAILING_TEST_MARKERS` — gating it on
|
|
233
|
+
* test-shaped evidence would route every genuine build break to the
|
|
234
|
+
* block-the-batch path and wedge the batch there, since `sched resume --batch`
|
|
235
|
+
* would re-run the same deterministically failing typecheck forever. So a
|
|
236
|
+
* non-test capability may prove itself with compiler markers too; only a test
|
|
237
|
+
* capability is held to test output, which is where the fabricated exit codes
|
|
238
|
+
* were observed.
|
|
239
|
+
*/
|
|
240
|
+
function hasEarnedFailureEvidence(capabilityId, outputTail) {
|
|
241
|
+
if (hasFailingTestEvidence(outputTail))
|
|
242
|
+
return true;
|
|
243
|
+
if (capabilityId.startsWith('test.'))
|
|
244
|
+
return false;
|
|
245
|
+
if (outputTail === null || outputTail === undefined)
|
|
246
|
+
return true;
|
|
247
|
+
return BUILD_FAILURE_MARKERS.some((re) => re.test(outputTail.trim()));
|
|
248
|
+
}
|
|
160
249
|
/** The first balanced `{...}` document in `text`, parsed; null when there is none. */
|
|
161
250
|
function extractJsonObject(text) {
|
|
162
251
|
const start = text.indexOf('{');
|
package/dist/attribution.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attribution.js","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;AAuCH,kCAEC;AA0CD,gDA2BC;AAGD,kCAEC;AAWD,0CA0BC;AAWD,wDAMC;AAqED,oDAaC;AA6BD,oCAgBC;AAWD,wCASC;
|
|
1
|
+
{"version":3,"file":"attribution.js","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"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"}
|
package/dist/batch-dispatch.d.ts
CHANGED
|
@@ -136,8 +136,10 @@ export interface BatchTickResult {
|
|
|
136
136
|
*
|
|
137
137
|
* - still `automation-broken`/`capability-unavailable` → stays `blocked`,
|
|
138
138
|
* no state change (the capability still isn't fixed).
|
|
139
|
-
* - `task-failed`
|
|
140
|
-
* the
|
|
139
|
+
* - `task-failed` with recognizable failing-test evidence (#594,
|
|
140
|
+
* `hasFailingTestEvidence`) → the member really is broken; evict via the
|
|
141
|
+
* same rail the live gate uses. `task-failed` with no evidence stays
|
|
142
|
+
* `blocked`, same as the still-inconclusive case above.
|
|
141
143
|
* - `ok` → the member really was fine; complete it via the same rail the
|
|
142
144
|
* live gate uses.
|
|
143
145
|
*
|
|
@@ -153,6 +155,13 @@ export interface BatchTickResult {
|
|
|
153
155
|
export declare function resumeBlockedGate(deps: BatchDispatchDeps, config: SchedConfig, dispatch: ResolvedDispatch, batchId: string, now: Date): {
|
|
154
156
|
outcome: 'still-blocked' | 'evicted' | 'completed';
|
|
155
157
|
capability: string;
|
|
158
|
+
/**
|
|
159
|
+
* Why the batch is still blocked, when it is. Both branches land on
|
|
160
|
+
* `gate-inconclusive:<cap>`, but the operator's next action differs — fix
|
|
161
|
+
* the capability's availability, or fix the wrapper that reports a failure
|
|
162
|
+
* it cannot evidence — so the caller must not have to guess from the detail.
|
|
163
|
+
*/
|
|
164
|
+
blockedBy?: 'inconclusive' | 'unevidenced-failure';
|
|
156
165
|
detail?: string;
|
|
157
166
|
};
|
|
158
167
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;
|
|
1
|
+
{"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAmBH,OAAO,EAUL,KAAK,gBAAgB,EAErB,KAAK,SAAS,EACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,KAAK,WAAW,EAOjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,OAAO,EAAa,MAAM,WAAW,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAUL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAcpB,OAAO,EAAE,KAAK,QAAQ,EAA2D,MAAM,YAAY,CAAC;AACpG,OAAO,KAAK,EAGV,oBAAoB,EAIpB,WAAW,EAIZ,MAAM,SAAS,CAAC;AAMjB,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,EAAE,SAAS,CAAC;IACrB,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sGAAsG;IACtG,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,WAAW,CAAC;IAC5C;;;;;;;;;;;;;;;;;;;OAmBG;IACH,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,oBAAoB,CAAC;IACjF,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,8FAA8F;AAC9F,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,8FAA8F;IAC9F,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AA0zCD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,iBAAiB,EACvB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,IAAI,GACR;IACD,OAAO,EAAE,eAAe,GAAG,SAAS,GAAG,WAAW,CAAC;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,cAAc,GAAG,qBAAqB,CAAC;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CA2GA;AAqhBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,iBAAiB,EACvB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,gBAAgB,GACzB,eAAe,CAsFjB"}
|
package/dist/batch-dispatch.js
CHANGED
|
@@ -158,6 +158,25 @@ function gateDetailExcerpt(outputTail, reason) {
|
|
|
158
158
|
const buf = Buffer.from(text, 'utf-8');
|
|
159
159
|
return buf.length > 500 ? buf.subarray(buf.length - 500).toString('utf-8') : text;
|
|
160
160
|
}
|
|
161
|
+
/** A journal detail with the gate's output excerpt appended when there is one. */
|
|
162
|
+
function withExcerpt(message, excerpt) {
|
|
163
|
+
return excerpt ? `${message}: ${excerpt}` : message;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Which block-the-batch branch a gate result took (#594 AC3). Both an
|
|
167
|
+
* `automation-broken`/`capability-unavailable` result and a `task-failed` the
|
|
168
|
+
* gate could not evidence land on `gate-inconclusive:<cap>`; only the detail
|
|
169
|
+
* distinguishes "the capability is broken" from "the capability claimed a
|
|
170
|
+
* failure it did not prove", and those need different operator responses.
|
|
171
|
+
*/
|
|
172
|
+
function describeInconclusive(gate) {
|
|
173
|
+
if (gate.outcome !== 'task-failed')
|
|
174
|
+
return `reported ${gate.outcome}`;
|
|
175
|
+
const captured = gate.outputTail?.trim();
|
|
176
|
+
return captured
|
|
177
|
+
? `reported task-failed with no failing-test evidence in its ${Buffer.byteLength(captured, 'utf-8')}-byte capture`
|
|
178
|
+
: 'reported task-failed with an empty capture — no evidence at all';
|
|
179
|
+
}
|
|
161
180
|
/**
|
|
162
181
|
* Best-effort per-gate diagnostic log (#583 AC1) — mirrors `appendCapLog`'s
|
|
163
182
|
* never-crash contract: a log-write failure must not interrupt the gate
|
|
@@ -1006,6 +1025,13 @@ function evictMemberAndContinue(deps, config, dispatch, batchId, batch, memberIs
|
|
|
1006
1025
|
* script that legitimately could not run its suite must not be read as
|
|
1007
1026
|
* either a pass or a real failure). Only both `ok` falls through.
|
|
1008
1027
|
*
|
|
1028
|
+
* A `task-failed` is itself split in two (#594): its `outputTail` must carry
|
|
1029
|
+
* recognizable evidence of a failing test (`hasFailingTestEvidence`) before
|
|
1030
|
+
* it is trusted as a red suite. A `task-failed` whose output proves nothing —
|
|
1031
|
+
* empty, or only a wrapper script's own framing — did not earn its exit
|
|
1032
|
+
* code, and joins the inconclusive case on the block-the-batch path instead
|
|
1033
|
+
* of evicting a member for a failure that never happened.
|
|
1034
|
+
*
|
|
1009
1035
|
* Returns `true` when the gate already decided the member's fate (evicted or
|
|
1010
1036
|
* blocked, both of which `return` from the caller); `false` when no hook is
|
|
1011
1037
|
* configured or both checks came back `ok`, meaning the caller should treat
|
|
@@ -1020,37 +1046,48 @@ function runIncrementalGate(deps, config, dispatch, batchId, batch, memberIssue,
|
|
|
1020
1046
|
id,
|
|
1021
1047
|
...runCapability(worktree, id),
|
|
1022
1048
|
}));
|
|
1023
|
-
const
|
|
1049
|
+
const rawFailure = gateResults.find((r) => r.outcome === 'task-failed');
|
|
1024
1050
|
const gateInconclusive = gateResults.find((r) => r.outcome === 'automation-broken' || r.outcome === 'capability-unavailable');
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1051
|
+
const earnedFailure = rawFailure && (0, attribution_1.hasEarnedFailureEvidence)(rawFailure.id, rawFailure.outputTail)
|
|
1052
|
+
? rawFailure
|
|
1053
|
+
: undefined;
|
|
1054
|
+
const unevidencedFailure = rawFailure && !earnedFailure ? rawFailure : undefined;
|
|
1055
|
+
// Record the gate the batch is actually routed on, not merely the worst
|
|
1056
|
+
// outcome: an unevidenced `task-failed` alongside a genuine
|
|
1057
|
+
// `automation-broken` blocks on the LATTER, and `member_gates` naming the
|
|
1058
|
+
// former would contradict `blocked_reason` in `sched status` — and would
|
|
1059
|
+
// name a different capability than `resumeBlockedGate` rechecks, since that
|
|
1060
|
+
// parses `blocked_reason`.
|
|
1061
|
+
const decisiveGate = earnedFailure ?? gateInconclusive ?? unevidencedFailure;
|
|
1062
|
+
if (decisiveGate) {
|
|
1063
|
+
recordMemberGate(deps, batchId, memberIssue, decisiveGate, now);
|
|
1028
1064
|
}
|
|
1029
|
-
if (
|
|
1030
|
-
const reason = `incremental-gate-failed:${
|
|
1031
|
-
writeGateLog(deps, batchId,
|
|
1032
|
-
const excerpt = gateDetailExcerpt(
|
|
1065
|
+
if (earnedFailure) {
|
|
1066
|
+
const reason = `incremental-gate-failed:${earnedFailure.id}`;
|
|
1067
|
+
writeGateLog(deps, batchId, earnedFailure.id, memberIssue, earnedFailure.outputTail);
|
|
1068
|
+
const excerpt = gateDetailExcerpt(earnedFailure.outputTail, earnedFailure.reason);
|
|
1033
1069
|
journalEvent(deps, 'unit-failed', unit(batchId), {
|
|
1034
1070
|
issue: memberIssue,
|
|
1035
1071
|
reason,
|
|
1036
|
-
detail: excerpt
|
|
1037
|
-
? `cap run ${gateFailure.id} reported task-failed after member review done: ${excerpt}`
|
|
1038
|
-
: `cap run ${gateFailure.id} reported task-failed after member review done`,
|
|
1072
|
+
detail: withExcerpt(`cap run ${earnedFailure.id} reported task-failed with failing-test evidence after member review done`, excerpt),
|
|
1039
1073
|
});
|
|
1040
1074
|
deps.store.withLock((s) => ({ state: releaseSlot(s, batchId, now), result: undefined }));
|
|
1041
1075
|
evictMemberAndContinue(deps, config, dispatch, batchId, batch, memberIssue, reason, now, result);
|
|
1042
1076
|
return true;
|
|
1043
1077
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1078
|
+
const inconclusive = gateInconclusive ?? unevidencedFailure;
|
|
1079
|
+
if (inconclusive) {
|
|
1080
|
+
const reason = `gate-inconclusive:${inconclusive.id}`;
|
|
1081
|
+
writeGateLog(deps, batchId, inconclusive.id, memberIssue, inconclusive.outputTail);
|
|
1082
|
+
const excerpt = gateDetailExcerpt(inconclusive.outputTail, inconclusive.reason);
|
|
1083
|
+
// #594 AC3: say WHICH of the two block-the-batch branches fired. Both land
|
|
1084
|
+
// on `gate-inconclusive:<cap>`, and a later run must not have to re-derive
|
|
1085
|
+
// "the capability was broken" from "the capability reported a failure it
|
|
1086
|
+
// could not evidence" — the operator's next action differs.
|
|
1048
1087
|
journalEvent(deps, 'gate-inconclusive', unit(batchId), {
|
|
1049
1088
|
issue: memberIssue,
|
|
1050
1089
|
reason,
|
|
1051
|
-
detail: excerpt
|
|
1052
|
-
? `cap run ${gateInconclusive.id} reported ${gateInconclusive.outcome} after member review done: ${excerpt}`
|
|
1053
|
-
: `cap run ${gateInconclusive.id} reported ${gateInconclusive.outcome} after member review done`,
|
|
1090
|
+
detail: withExcerpt(`cap run ${inconclusive.id} ${describeInconclusive(inconclusive)} after member review done`, excerpt),
|
|
1054
1091
|
});
|
|
1055
1092
|
deps.store.withLock((s) => ({ state: releaseSlot(s, batchId, now), result: undefined }));
|
|
1056
1093
|
const stateNow = deps.store.load();
|
|
@@ -1100,8 +1137,10 @@ function completeMemberGate(deps, config, dispatch, batchId, batch, memberIssue,
|
|
|
1100
1137
|
*
|
|
1101
1138
|
* - still `automation-broken`/`capability-unavailable` → stays `blocked`,
|
|
1102
1139
|
* no state change (the capability still isn't fixed).
|
|
1103
|
-
* - `task-failed`
|
|
1104
|
-
* the
|
|
1140
|
+
* - `task-failed` with recognizable failing-test evidence (#594,
|
|
1141
|
+
* `hasFailingTestEvidence`) → the member really is broken; evict via the
|
|
1142
|
+
* same rail the live gate uses. `task-failed` with no evidence stays
|
|
1143
|
+
* `blocked`, same as the still-inconclusive case above.
|
|
1105
1144
|
* - `ok` → the member really was fine; complete it via the same rail the
|
|
1106
1145
|
* live gate uses.
|
|
1107
1146
|
*
|
|
@@ -1140,13 +1179,31 @@ function resumeBlockedGate(deps, config, dispatch, batchId, now) {
|
|
|
1140
1179
|
journalEvent(deps, 'gate-inconclusive', unit(batchId), {
|
|
1141
1180
|
issue: memberIssue,
|
|
1142
1181
|
reason: `gate-inconclusive:${capabilityId}`,
|
|
1143
|
-
detail: excerpt
|
|
1144
|
-
? `sched resume --batch: cap run ${capabilityId} still reports ${recheck.outcome} on recheck: ${excerpt}`
|
|
1145
|
-
: `sched resume --batch: cap run ${capabilityId} still reports ${recheck.outcome} on recheck`,
|
|
1182
|
+
detail: withExcerpt(`sched resume --batch: cap run ${capabilityId} still reports ${recheck.outcome} on recheck`, excerpt),
|
|
1146
1183
|
});
|
|
1147
1184
|
return {
|
|
1148
1185
|
outcome: 'still-blocked',
|
|
1149
1186
|
capability: capabilityId,
|
|
1187
|
+
blockedBy: 'inconclusive',
|
|
1188
|
+
detail: excerpt,
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
// A recheck can still come back `task-failed` with no evidence behind it
|
|
1192
|
+
// (#594) — the same routing the live gate applies, so a resumed batch
|
|
1193
|
+
// never evicts a member for a failure `hasFailingTestEvidence` cannot
|
|
1194
|
+
// confirm.
|
|
1195
|
+
if (recheck.outcome === 'task-failed' &&
|
|
1196
|
+
!(0, attribution_1.hasEarnedFailureEvidence)(capabilityId, recheck.outputTail)) {
|
|
1197
|
+
writeGateLog(deps, batchId, capabilityId, memberIssue, recheck.outputTail);
|
|
1198
|
+
journalEvent(deps, 'gate-inconclusive', unit(batchId), {
|
|
1199
|
+
issue: memberIssue,
|
|
1200
|
+
reason: `gate-inconclusive:${capabilityId}`,
|
|
1201
|
+
detail: withExcerpt(`sched resume --batch: cap run ${capabilityId} still reports task-failed with no failing-test evidence on recheck`, excerpt),
|
|
1202
|
+
});
|
|
1203
|
+
return {
|
|
1204
|
+
outcome: 'still-blocked',
|
|
1205
|
+
capability: capabilityId,
|
|
1206
|
+
blockedBy: 'unevidenced-failure',
|
|
1150
1207
|
detail: excerpt,
|
|
1151
1208
|
};
|
|
1152
1209
|
}
|
|
@@ -1160,9 +1217,7 @@ function resumeBlockedGate(deps, config, dispatch, batchId, now) {
|
|
|
1160
1217
|
journalEvent(deps, 'unit-failed', unit(batchId), {
|
|
1161
1218
|
issue: memberIssue,
|
|
1162
1219
|
reason,
|
|
1163
|
-
detail: excerpt
|
|
1164
|
-
? `sched resume --batch: cap run ${capabilityId} reported task-failed on recheck: ${excerpt}`
|
|
1165
|
-
: `sched resume --batch: cap run ${capabilityId} reported task-failed on recheck`,
|
|
1220
|
+
detail: withExcerpt(`sched resume --batch: cap run ${capabilityId} reported task-failed on recheck`, excerpt),
|
|
1166
1221
|
});
|
|
1167
1222
|
evictMemberAndContinue(deps, config, dispatch, batchId, batch, memberIssue, reason, now, result);
|
|
1168
1223
|
return { outcome: 'evicted', capability: capabilityId, detail: excerpt };
|
|
@@ -1319,20 +1374,33 @@ function reconcileMemberSlot(deps, config, dispatch, batchId, slot, now, result)
|
|
|
1319
1374
|
}
|
|
1320
1375
|
/**
|
|
1321
1376
|
* Requeue a member full-cycle, record the eviction, and dissolve if this tips
|
|
1322
|
-
* the batch past
|
|
1323
|
-
* `recovery.ts`'s `evictMembers` (which needs
|
|
1324
|
-
* and a commit range to revert; a member
|
|
1325
|
-
* whether the batch dissolved.
|
|
1377
|
+
* the batch past its `dissolve_policy` threshold (#563; RFC F.1/F.8) — WITHOUT
|
|
1378
|
+
* going through `recovery.ts`'s `evictMembers` (which needs
|
|
1379
|
+
* `attributing`/`evicting` status and a commit range to revert; a member
|
|
1380
|
+
* evicted here has neither). Returns whether the batch dissolved.
|
|
1381
|
+
*
|
|
1382
|
+
* A member already in `evictions[]` is a NO-OP here, not merely a skipped
|
|
1383
|
+
* record (#595 AC1): the requeue must not run a second time either. It would
|
|
1384
|
+
* overwrite the first eviction's `failure_evidence` — leaving `evictions[]`
|
|
1385
|
+
* and the queue entry disagreeing about why the member was evicted — and, if
|
|
1386
|
+
* the member has since been re-dispatched full-cycle, would take the
|
|
1387
|
+
* `executing → evicted → requeued` rail and kill that live run.
|
|
1326
1388
|
*/
|
|
1327
1389
|
function evictMemberDirectly(deps, config, batchId, memberIssue, reason, now) {
|
|
1328
1390
|
const dissolvePolicy = (0, types_1.resolveDissolvePolicy)(config.dissolve_policy);
|
|
1329
1391
|
// Pass 1 (pure — requeue + record the eviction): safe to run entirely
|
|
1330
1392
|
// inside the lock, unlike `dissolveBatch` below, which shells out
|
|
1331
1393
|
// (`deps.exec`/`postMilestone`) and so must NOT hold the lock while it runs.
|
|
1332
|
-
const triggered = deps.store.withLock((s) => {
|
|
1394
|
+
const { triggered, duplicate } = deps.store.withLock((s) => {
|
|
1333
1395
|
const b = (0, state_1.findBatch)(s, batchId);
|
|
1334
1396
|
if (!b)
|
|
1335
|
-
return { state: s, result: false };
|
|
1397
|
+
return { state: s, result: { triggered: false, duplicate: false } };
|
|
1398
|
+
if (b.evictions.some((e) => e.issue === memberIssue)) {
|
|
1399
|
+
return {
|
|
1400
|
+
state: s,
|
|
1401
|
+
result: { triggered: (0, recovery_1.checkDissolveTrigger)(b, dissolvePolicy), duplicate: true },
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1336
1404
|
const evidence = {
|
|
1337
1405
|
batch: batchId,
|
|
1338
1406
|
reason,
|
|
@@ -1343,25 +1411,34 @@ function evictMemberDirectly(deps, config, batchId, memberIssue, reason, now) {
|
|
|
1343
1411
|
};
|
|
1344
1412
|
const requeueResult = (0, state_1.requeueMember)(s, memberIssue, { mode: 'full', batch: null }, reason, now, { failure_evidence: evidence });
|
|
1345
1413
|
let next = requeueResult.state;
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
}, now);
|
|
1414
|
+
// `appendEvictions` stays the state-level backstop even though the
|
|
1415
|
+
// duplicate is already short-circuited above — it is the one append path.
|
|
1416
|
+
const { evictions, duplicate: duplicateRecords } = (0, state_1.appendEvictions)(b, [
|
|
1417
|
+
{
|
|
1418
|
+
issue: memberIssue,
|
|
1419
|
+
reason,
|
|
1420
|
+
attribution: 'none',
|
|
1421
|
+
reverted_commits: [],
|
|
1422
|
+
group: [],
|
|
1423
|
+
at: now.toISOString(),
|
|
1424
|
+
},
|
|
1425
|
+
]);
|
|
1426
|
+
next = (0, state_1.patchBatch)(next, batchId, { evictions }, now);
|
|
1359
1427
|
const updated = (0, state_1.findBatch)(next, batchId);
|
|
1360
1428
|
return {
|
|
1361
1429
|
state: next,
|
|
1362
|
-
result:
|
|
1430
|
+
result: {
|
|
1431
|
+
triggered: updated !== undefined && (0, recovery_1.checkDissolveTrigger)(updated, dissolvePolicy),
|
|
1432
|
+
duplicate: duplicateRecords.length > 0,
|
|
1433
|
+
},
|
|
1363
1434
|
};
|
|
1364
1435
|
});
|
|
1436
|
+
if (duplicate) {
|
|
1437
|
+
journalEvent(deps, 'eviction-duplicate', unit(batchId), {
|
|
1438
|
+
issue: memberIssue,
|
|
1439
|
+
detail: (0, state_1.duplicateEvictionDetail)(memberIssue, reason),
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1365
1442
|
if (!triggered)
|
|
1366
1443
|
return false;
|
|
1367
1444
|
// Pass 2 (outside the lock — dissolveBatch shells out): re-load fresh
|