@ai-dossier/sched 0.57.6 → 0.58.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 +28 -9
- package/dist/anchor-close.d.ts +55 -18
- package/dist/anchor-close.d.ts.map +1 -1
- package/dist/anchor-close.js +156 -58
- package/dist/anchor-close.js.map +1 -1
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +23 -11
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/dossier-root.d.ts +26 -0
- package/dist/dossier-root.d.ts.map +1 -0
- package/dist/dossier-root.js +87 -0
- package/dist/dossier-root.js.map +1 -0
- package/dist/groundtruth.d.ts +29 -0
- package/dist/groundtruth.d.ts.map +1 -1
- package/dist/groundtruth.js +48 -7
- package/dist/groundtruth.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1606,14 +1606,32 @@ predicate in `anchor-close.ts`) over `blocked`/`done` batches touched within the
|
|
|
1606
1606
|
issue's author can close their own issue. One more path counts for a hand close: a
|
|
1607
1607
|
PR of the project's repository, MERGED into the base, that GitHub lists as closing
|
|
1608
1608
|
the issue (`closedByPullRequestsReferences` — `Closes #N` parsed but not acted on,
|
|
1609
|
-
the imboard#4116 shape); that still needs a
|
|
1610
|
-
|
|
1611
|
-
the
|
|
1612
|
-
(
|
|
1613
|
-
`member-closed-by-hand-reopen
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1609
|
+
the imboard#4116 shape); that still needs a PR merged by someone with write access — though
|
|
1610
|
+
not necessarily by whoever wrote its `Closes #N`. That reference must also have merged inside
|
|
1611
|
+
the member's last open stretch, on every hand close: strictly AFTER the stretch began — the
|
|
1612
|
+
last reopen (#799; one merged before it did not finish the issue, that is why it was
|
|
1613
|
+
reopened: `member-closed-by-hand-ref-pr-<n>-predates-reopen`), or, never reopened, the
|
|
1614
|
+
issue's creation (#850; a PR merged before the issue existed can only have been linked by an
|
|
1615
|
+
edit made after the fact: `…-predates-issue`) — and no later than its close (#850; one merged
|
|
1616
|
+
after the close did not back it: `…-postdates-close`). The bounds are on when the PR merged,
|
|
1617
|
+
not on when its `Closes #N` was written: a reference edited into a PR that merged inside the
|
|
1618
|
+
stretch still vouches (#850 chose the time bound over reading when the link was made). A
|
|
1619
|
+
reopen, creation or close time that cannot be read refuses outright, however long the
|
|
1620
|
+
reference list (`member-closed-by-hand-reopen-unreadable`, `…-created-time-unreadable`,
|
|
1621
|
+
`…-close-time-unreadable`); a reference whose merge time cannot be read never vouches
|
|
1622
|
+
(`…-merge-time-unreadable`). A refusal names only the PRs failing the first bound that
|
|
1623
|
+
applies — merge time unreadable, then merged after the close, then merged before the
|
|
1624
|
+
stretch — so a mixed list does not name every rejected PR. The reference list is read one
|
|
1625
|
+
page (100) deep: a longer list whose first page holds no reference that vouches reads
|
|
1626
|
+
`unknown` (`member-closed-by-hand-refs-truncated`, #850), not a refusal — one may be on a
|
|
1627
|
+
later page — unless another member already makes the anchor needs-operator (the truncated
|
|
1628
|
+
member is then named among its reasons). It does not clear on a re-read: list every reference
|
|
1629
|
+
with `gh api graphql --paginate -f query='query($endCursor:String){repository(owner:"<owner>",name:"<name>"){issue(number:<n>){closedByPullRequestsReferences(first:100,after:$endCursor,includeClosedPrs:true){pageInfo{hasNextPage endCursor} nodes{number merged mergedAt baseRefName}}}}}'`
|
|
1630
|
+
and close the anchor by hand if one merged into the base inside the stretch. Remedy for a
|
|
1631
|
+
refusal: a new PR or commit that closes the member, or close the anchor by hand — reopening
|
|
1632
|
+
and re-closing the member does not help (the reference then predates the reopen). A member
|
|
1633
|
+
issue that no longer resolves (deleted/transferred) reads `member-missing` — needs-operator,
|
|
1634
|
+
not an outage.
|
|
1617
1635
|
- **Never over a failure trail.** Any evicted member, any member in a failure status
|
|
1618
1636
|
(`ISSUE_UNIVERSAL_FAILURE_EDGES` + `evicted`/`handed-back`/`requeued`), any member requeued out of
|
|
1619
1637
|
the batch, a dissolved/stopped batch, or a `decision-pending` label on the anchor or a
|
|
@@ -1691,7 +1709,8 @@ predicate in `anchor-close.ts`) over `blocked`/`done` batches touched within the
|
|
|
1691
1709
|
Everything else is surfaced, never closed: `sched status --anchors` (opt-in; `status`
|
|
1692
1710
|
makes no GitHub call without it) lists each still-open anchor of a batch no longer in
|
|
1693
1711
|
flight as `closable`, `needs-operator` or `unknown`, with every member's GitHub and
|
|
1694
|
-
ledger state, stopping its reads at the first failed one
|
|
1712
|
+
ledger state, stopping its reads at the first failed one (a truncated reference list's
|
|
1713
|
+
`unknown` is not a failed read and stops nothing, #850). A blocked batch milestone now
|
|
1695
1714
|
reads `next=operator`, not `next=done`.
|
|
1696
1715
|
|
|
1697
1716
|
Journal: `anchor-closed` (including "found already closed") and `anchor-close-failed`
|
package/dist/anchor-close.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Pure verdicts plus one idempotent effect (`closeAnchor`); everything that
|
|
22
22
|
* talks to GitHub or git is injected, so tests never shell out.
|
|
23
23
|
*/
|
|
24
|
-
import type
|
|
24
|
+
import { type IssueCloseTruth } from './groundtruth';
|
|
25
25
|
import type { ExecFn } from './project';
|
|
26
26
|
import type { BatchEntry, BatchStatus, IssueStatus, SchedState } from './types';
|
|
27
27
|
/** Reads one issue's closure record; `undefined` = the poll failed (unreachable). */
|
|
@@ -66,19 +66,49 @@ export interface AnchorMemberReport {
|
|
|
66
66
|
/**
|
|
67
67
|
* The anchor-close predicate's answer. Every open-anchor verdict carries
|
|
68
68
|
* `reasons` (empty for `closable`; for `unknown`, which read failed — ground
|
|
69
|
-
* truth unreachable,
|
|
69
|
+
* truth unreachable — or, #850, which member GitHub answered for without
|
|
70
|
+
* settling it; either way nothing is decided this pass).
|
|
70
71
|
*/
|
|
71
72
|
export type AnchorVerdict = {
|
|
72
73
|
kind: 'anchor-closed';
|
|
73
74
|
} | {
|
|
74
|
-
kind: 'closable' | 'needs-operator'
|
|
75
|
+
kind: 'closable' | 'needs-operator';
|
|
75
76
|
reasons: string[];
|
|
76
77
|
members: AnchorMemberReport[];
|
|
78
|
+
} | {
|
|
79
|
+
kind: 'unknown';
|
|
80
|
+
reasons: string[];
|
|
81
|
+
members: AnchorMemberReport[];
|
|
82
|
+
/**
|
|
83
|
+
* #850: `true` when a FAILED read produced it (ground truth unreachable
|
|
84
|
+
* — a sweep stops reading); `false` when GitHub answered but could not
|
|
85
|
+
* settle a member (a truncated closing-reference list — a sweep reads
|
|
86
|
+
* on). Required, so no `unknown` can leave the question open.
|
|
87
|
+
*/
|
|
88
|
+
unreachable: boolean;
|
|
77
89
|
};
|
|
78
90
|
/** The verdict of an anchor that is still open. */
|
|
79
91
|
export type OpenAnchorVerdict = Exclude<AnchorVerdict, {
|
|
80
92
|
kind: 'anchor-closed';
|
|
81
93
|
}>;
|
|
94
|
+
/**
|
|
95
|
+
* {@link shippingEvidence}'s answer: `shipped` (by what), `refused` (why), or —
|
|
96
|
+
* #850 — `unknown` (why GitHub's answer could not settle it).
|
|
97
|
+
*/
|
|
98
|
+
export type ShippingEvidence = {
|
|
99
|
+
shipped: string;
|
|
100
|
+
} | {
|
|
101
|
+
refused: string;
|
|
102
|
+
} | {
|
|
103
|
+
unknown: string;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* #850: the evidence reason for a hand close whose closing-reference list is
|
|
107
|
+
* longer than the page read and holds no vouching reference on it — `unknown`,
|
|
108
|
+
* never a refusal. A re-read does not clear it (the README's anchor section
|
|
109
|
+
* gives the operator's remedy).
|
|
110
|
+
*/
|
|
111
|
+
export declare const REFS_TRUNCATED_REASON = "closed-by-hand-refs-truncated";
|
|
82
112
|
/**
|
|
83
113
|
* The verified shipping evidence for a member closed as completed, or the
|
|
84
114
|
* reason it is not evidence. Only code that landed in `baseBranch` counts:
|
|
@@ -87,13 +117,11 @@ export type OpenAnchorVerdict = Exclude<AnchorVerdict, {
|
|
|
87
117
|
* ({@link closingReferenceEvidence}) — or a commit reachable from it. A hand
|
|
88
118
|
* close with no qualifying reference, an unmerged or other-base PR, and an
|
|
89
119
|
* unverifiable commit are all refusals — anyone who can close the issue could
|
|
90
|
-
* otherwise mint "shipped".
|
|
120
|
+
* otherwise mint "shipped". `unknown` (#850) is neither: GitHub answered, but
|
|
121
|
+
* only with the first page of a longer closing-reference list, none of which
|
|
122
|
+
* vouches — so a refusal could be wrong, and nothing is decided.
|
|
91
123
|
*/
|
|
92
|
-
export declare function shippingEvidence(truth: IssueCloseTruth, baseBranch: string, repo: string | undefined, commitInBase: CommitInBase | undefined):
|
|
93
|
-
shipped: string;
|
|
94
|
-
} | {
|
|
95
|
-
refused: string;
|
|
96
|
-
};
|
|
124
|
+
export declare function shippingEvidence(truth: IssueCloseTruth, baseBranch: string, repo: string | undefined, commitInBase: CommitInBase | undefined): ShippingEvidence;
|
|
97
125
|
export interface MembersVerdictOptions {
|
|
98
126
|
/** Read every member even after a disqualifier (the sweep); the engine stops early. */
|
|
99
127
|
exhaustive?: boolean;
|
|
@@ -121,9 +149,10 @@ export type OrphanShippingCheckOptions = ShippingCheckOptions & {
|
|
|
121
149
|
* members-closed evidence, so the two can never disagree about what
|
|
122
150
|
* "shipped" means.
|
|
123
151
|
*
|
|
124
|
-
* `exhaustive: false` (the engine) stops at the first disqualifying fact
|
|
125
|
-
* skips GitHub entirely when the ledger already
|
|
126
|
-
* stays blocked is re-checked every tick, so the
|
|
152
|
+
* `exhaustive: false` (the engine) stops at the first disqualifying fact or
|
|
153
|
+
* undecided member (#850) and skips GitHub entirely when the ledger already
|
|
154
|
+
* disqualifies — a batch that stays blocked is re-checked every tick, so the
|
|
155
|
+
* cheap answer matters there.
|
|
127
156
|
* `exhaustive: true` (the `sched status --anchors` sweep) reads every member
|
|
128
157
|
* so the operator sees each one's state. Either way a failed read stops the
|
|
129
158
|
* reads: GitHub being unreachable is not worth N more timeouts.
|
|
@@ -171,10 +200,11 @@ export interface AnchorReportItem {
|
|
|
171
200
|
* `sched status --anchors`' sweep (#768): every still-open anchor of a batch
|
|
172
201
|
* that is no longer in flight, marked `closable` (the strict condition holds —
|
|
173
202
|
* the engine will close it, within its 7-day window), `needs-operator`
|
|
174
|
-
* (anything else), or `unknown` (a GitHub read failed —
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
203
|
+
* (anything else), or `unknown` (a GitHub read failed, or — #850 — a member's
|
|
204
|
+
* closing-reference list was truncated; nothing decided), with each member's
|
|
205
|
+
* state. Report-only by construction: it takes no exec for writes and has no
|
|
206
|
+
* way to close anything. The first FAILED read stops the sweep's reads; every
|
|
207
|
+
* later batch is reported `unknown` without a call.
|
|
178
208
|
*/
|
|
179
209
|
export declare function sweepAnchors(state: SchedState, read: IssueCloseReader, opts?: ShippingCheckOptions): AnchorReportItem[];
|
|
180
210
|
/**
|
|
@@ -245,9 +275,15 @@ export interface OrphanAnchorCandidate {
|
|
|
245
275
|
* confirm, not the ledger-backed `sweepAnchors`' stronger `closable`.
|
|
246
276
|
*/
|
|
247
277
|
export type OrphanAnchorVerdict = {
|
|
248
|
-
kind: 'orphan-closable-candidate' | 'orphan-needs-operator'
|
|
278
|
+
kind: 'orphan-closable-candidate' | 'orphan-needs-operator';
|
|
279
|
+
reasons: string[];
|
|
280
|
+
members: AnchorMemberReport[];
|
|
281
|
+
} | {
|
|
282
|
+
kind: 'orphan-unknown';
|
|
249
283
|
reasons: string[];
|
|
250
284
|
members: AnchorMemberReport[];
|
|
285
|
+
/** #850: as on {@link AnchorVerdict}'s `unknown` — `true` only when a FAILED read produced it. */
|
|
286
|
+
unreachable: boolean;
|
|
251
287
|
};
|
|
252
288
|
/**
|
|
253
289
|
* Classify one orphan anchor candidate, or `null` when it turns out not to be
|
|
@@ -326,7 +362,8 @@ export interface OrphanAnchorSweepResult {
|
|
|
326
362
|
* left real orphan candidates unprobed. Classifying stops at the first
|
|
327
363
|
* failed member/anchor read (the same fail-closed posture as
|
|
328
364
|
* `sweepAnchors`): every anchor after that point is reported `orphan-unknown`
|
|
329
|
-
* without a further call.
|
|
365
|
+
* without a further call. An `orphan-unknown` from a truncated
|
|
366
|
+
* closing-reference list (#850) is not a failed read and stops nothing.
|
|
330
367
|
*/
|
|
331
368
|
export declare function sweepOrphanAnchors(state: SchedState, list: OpenAnchorLister, read: IssueCloseReader, opts?: OrphanShippingCheckOptions): OrphanAnchorSweepResult | null;
|
|
332
369
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anchor-close.d.ts","sourceRoot":"","sources":["../src/anchor-close.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"anchor-close.d.ts","sourceRoot":"","sources":["../src/anchor-close.ts"],"names":[],"mappings":"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"}
|
package/dist/anchor-close.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* talks to GitHub or git is injected, so tests never shell out.
|
|
24
24
|
*/
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.ORPHAN_SWEEP_MAX_ANCHORS = exports.ORPHAN_SWEEP_MAX_MEMBERS = exports.DEFAULT_BASE_BRANCH = exports.ANCHOR_CLOSE_BATCH_STATUSES = void 0;
|
|
26
|
+
exports.ORPHAN_SWEEP_MAX_ANCHORS = exports.ORPHAN_SWEEP_MAX_MEMBERS = exports.DEFAULT_BASE_BRANCH = exports.REFS_TRUNCATED_REASON = exports.ANCHOR_CLOSE_BATCH_STATUSES = void 0;
|
|
27
27
|
exports.openAnchorBatches = openAnchorBatches;
|
|
28
28
|
exports.formatBatchStatus = formatBatchStatus;
|
|
29
29
|
exports.anchorLedgerBlockers = anchorLedgerBlockers;
|
|
@@ -40,6 +40,7 @@ exports.orphanAnchorListArgs = orphanAnchorListArgs;
|
|
|
40
40
|
exports.sweepOrphanAnchors = sweepOrphanAnchors;
|
|
41
41
|
exports.batchAnchorStillOpen = batchAnchorStillOpen;
|
|
42
42
|
const attribution_1 = require("./attribution");
|
|
43
|
+
const groundtruth_1 = require("./groundtruth");
|
|
43
44
|
const labels_1 = require("./labels");
|
|
44
45
|
const state_1 = require("./state");
|
|
45
46
|
/**
|
|
@@ -118,6 +119,13 @@ function anchorLedgerBlockers(state, batch) {
|
|
|
118
119
|
}
|
|
119
120
|
return [...new Set(reasons)];
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* #850: the evidence reason for a hand close whose closing-reference list is
|
|
124
|
+
* longer than the page read and holds no vouching reference on it — `unknown`,
|
|
125
|
+
* never a refusal. A re-read does not clear it (the README's anchor section
|
|
126
|
+
* gives the operator's remedy).
|
|
127
|
+
*/
|
|
128
|
+
exports.REFS_TRUNCATED_REASON = 'closed-by-hand-refs-truncated';
|
|
121
129
|
/**
|
|
122
130
|
* The verified shipping evidence for a member closed as completed, or the
|
|
123
131
|
* reason it is not evidence. Only code that landed in `baseBranch` counts:
|
|
@@ -126,7 +134,9 @@ function anchorLedgerBlockers(state, batch) {
|
|
|
126
134
|
* ({@link closingReferenceEvidence}) — or a commit reachable from it. A hand
|
|
127
135
|
* close with no qualifying reference, an unmerged or other-base PR, and an
|
|
128
136
|
* unverifiable commit are all refusals — anyone who can close the issue could
|
|
129
|
-
* otherwise mint "shipped".
|
|
137
|
+
* otherwise mint "shipped". `unknown` (#850) is neither: GitHub answered, but
|
|
138
|
+
* only with the first page of a longer closing-reference list, none of which
|
|
139
|
+
* vouches — so a refusal could be wrong, and nothing is decided.
|
|
130
140
|
*/
|
|
131
141
|
function shippingEvidence(truth, baseBranch, repo, commitInBase) {
|
|
132
142
|
const closer = truth.closer;
|
|
@@ -154,37 +164,96 @@ function shippingEvidence(truth, baseBranch, repo, commitInBase) {
|
|
|
154
164
|
* A hand close (no closer on the close event) is still shipped when a PR of
|
|
155
165
|
* the pinned repo, MERGED into the base, names the issue as closed
|
|
156
166
|
* (`Closes #N` — GitHub parsed it, then did not act: the imboard#4116 shape).
|
|
157
|
-
*
|
|
167
|
+
* Only a merged PR counts, and a merge needs write access, so an author's
|
|
168
|
+
* self-close alone never counts.
|
|
169
|
+
*
|
|
170
|
+
* The reference must also have merged inside the member's last open stretch
|
|
171
|
+
* ({@link refBound}): strictly AFTER its start — the last reopen (#799; one
|
|
172
|
+
* merged before it did not finish the issue, that is why it was reopened), or
|
|
173
|
+
* the issue's creation when it was never reopened (#850 review; a PR merged
|
|
174
|
+
* before the issue existed can only have been linked by an edit made after
|
|
175
|
+
* the fact) — and no later than its close (#850; one merged after the close
|
|
176
|
+
* did not back it). Every bound applies to every hand close; a time that
|
|
177
|
+
* cannot be read is not evidence either way: fail closed. An unreadable
|
|
178
|
+
* reopen, creation or close time refuses outright; otherwise the refusal
|
|
179
|
+
* names only the PRs failing the first bound that applies — merge time
|
|
180
|
+
* unreadable, then merged after the close, then merged before the stretch
|
|
181
|
+
* began — so a mixed list does not name every rejected PR. The remedy is a
|
|
182
|
+
* new PR or commit that closes the member, or an operator closing the anchor
|
|
183
|
+
* by hand; reopening and re-closing the member does not help (the reference
|
|
184
|
+
* then predates the reopen).
|
|
185
|
+
*
|
|
186
|
+
* The bounds are on when the PR MERGED, not on when its `Closes #N` was
|
|
187
|
+
* written: a reference edited into a PR that merged inside the stretch still
|
|
188
|
+
* vouches (#850 chose the time bound over reading when the link was made).
|
|
158
189
|
*
|
|
159
|
-
* #
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* the member, or an operator closing the anchor by hand.
|
|
190
|
+
* #850: `closingPrs` is one page. With more on the next
|
|
191
|
+
* (`closingPrsTruncated`), no vouching reference on this page is `unknown`
|
|
192
|
+
* ({@link REFS_TRUNCATED_REASON}), not a refusal — one may be on the next page.
|
|
193
|
+
* A definite answer still stands: a reference on this page that vouches is
|
|
194
|
+
* shipped, and an unreadable reopen, creation or close time refuses whatever
|
|
195
|
+
* the next page holds.
|
|
166
196
|
*/
|
|
167
197
|
function closingReferenceEvidence(truth, baseBranch, repo) {
|
|
168
198
|
const refs = truth.closingPrs.filter((pr) => pr.merged &&
|
|
169
199
|
pr.baseRefName === baseBranch &&
|
|
170
200
|
repo !== undefined &&
|
|
171
201
|
pr.repo?.toLowerCase() === repo.toLowerCase());
|
|
172
|
-
|
|
202
|
+
const truncated = truth.closingPrsTruncated === true;
|
|
203
|
+
if (refs.length === 0 && !truncated)
|
|
173
204
|
return { refused: 'closed-by-hand' };
|
|
174
|
-
|
|
175
|
-
|
|
205
|
+
// `undefined` (an unreadable reopen timeline) parses to NaN, like garbage.
|
|
206
|
+
const reopened = truth.lastReopenedAt !== null;
|
|
207
|
+
const reopenedMs = reopened ? (0, groundtruth_1.timestampMs)(truth.lastReopenedAt) : Number.NEGATIVE_INFINITY;
|
|
208
|
+
if (Number.isNaN(reopenedMs))
|
|
176
209
|
return { refused: 'closed-by-hand-reopen-unreadable' };
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
const createdMs = (0, groundtruth_1.timestampMs)(truth.createdAt);
|
|
211
|
+
if (Number.isNaN(createdMs))
|
|
212
|
+
return { refused: 'closed-by-hand-created-time-unreadable' };
|
|
213
|
+
const closedMs = (0, groundtruth_1.timestampMs)(truth.closedAt);
|
|
214
|
+
if (Number.isNaN(closedMs))
|
|
215
|
+
return { refused: 'closed-by-hand-close-time-unreadable' };
|
|
216
|
+
const stretch = { openedMs: Math.max(createdMs, reopenedMs), closedMs };
|
|
217
|
+
const bounds = refs.map((pr) => ({ pr, bound: refBound(pr, stretch) }));
|
|
218
|
+
const vouching = bounds.find((b) => b.bound === 'vouches');
|
|
219
|
+
if (vouching !== undefined) {
|
|
220
|
+
return { shipped: `PR #${vouching.pr.number} (closing reference; issue closed by hand)` };
|
|
182
221
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
222
|
+
if (truncated)
|
|
223
|
+
return { unknown: exports.REFS_TRUNCATED_REASON };
|
|
224
|
+
// `refs` is non-empty here and none vouches, so some refusal bound applies;
|
|
225
|
+
// the `??` only satisfies the type checker.
|
|
226
|
+
const failed = REFUSAL_BOUND_ORDER.find((o) => bounds.some((b) => b.bound === o)) ?? 'predates-open';
|
|
227
|
+
const prs = bounds
|
|
228
|
+
.filter((b) => b.bound === failed)
|
|
229
|
+
.map((b) => b.pr.number)
|
|
230
|
+
.join('+');
|
|
231
|
+
const why = failed !== 'predates-open'
|
|
232
|
+
? failed
|
|
233
|
+
: reopenedMs > createdMs
|
|
234
|
+
? 'predates-reopen'
|
|
235
|
+
: 'predates-issue';
|
|
236
|
+
return { refused: `closed-by-hand-ref-pr-${prs}-${why}` };
|
|
237
|
+
}
|
|
238
|
+
/** The refusal bounds in the order {@link closingReferenceEvidence} names them. */
|
|
239
|
+
const REFUSAL_BOUND_ORDER = [
|
|
240
|
+
'merge-time-unreadable',
|
|
241
|
+
'postdates-close',
|
|
242
|
+
'predates-open',
|
|
243
|
+
];
|
|
244
|
+
/**
|
|
245
|
+
* Whether `pr` merged inside the stretch `(openedMs, closedMs]`, or which
|
|
246
|
+
* bound it fails. The merge time goes through the ground truth's one
|
|
247
|
+
* timestamp rule ({@link timestampMs}), so a missing or loose time can never
|
|
248
|
+
* pass a bound — even from an `IssueCloseTruth` the parser did not build.
|
|
249
|
+
*/
|
|
250
|
+
function refBound(pr, stretch) {
|
|
251
|
+
const mergedMs = (0, groundtruth_1.timestampMs)(pr.mergedAt);
|
|
252
|
+
if (Number.isNaN(mergedMs))
|
|
253
|
+
return 'merge-time-unreadable';
|
|
254
|
+
if (mergedMs > stretch.closedMs)
|
|
255
|
+
return 'postdates-close';
|
|
256
|
+
return mergedMs > stretch.openedMs ? 'vouches' : 'predates-open';
|
|
188
257
|
}
|
|
189
258
|
/**
|
|
190
259
|
* The blank `AnchorMemberReport` skeleton shared by {@link membersShippedVerdict}'s
|
|
@@ -214,7 +283,8 @@ function unreadMemberReports(members, ledgerStatus) {
|
|
|
214
283
|
* can reuse the identical shipping-evidence logic for a batch that, by
|
|
215
284
|
* definition, has no ledger entry to consult.
|
|
216
285
|
*
|
|
217
|
-
* `exhaustive: false` stops at the first disqualifying fact
|
|
286
|
+
* `exhaustive: false` stops at the first disqualifying fact — or undecided
|
|
287
|
+
* member (#850): either way the batch is not closable this pass; `exhaustive:
|
|
218
288
|
* true` reads every member so the operator sees each one's state. Either way
|
|
219
289
|
* a failed read stops the reads: GitHub being unreachable is not worth N
|
|
220
290
|
* more timeouts.
|
|
@@ -222,11 +292,12 @@ function unreadMemberReports(members, ledgerStatus) {
|
|
|
222
292
|
function readMembersShipping(members, read, baseBranch, opts, ledgerStatus) {
|
|
223
293
|
const reports = unreadMemberReports(members, ledgerStatus);
|
|
224
294
|
const reasons = [];
|
|
225
|
-
|
|
295
|
+
const undecided = [];
|
|
296
|
+
let unreachableIssue = null;
|
|
226
297
|
for (const member of reports) {
|
|
227
298
|
const truth = read(member.issue);
|
|
228
299
|
if (truth === undefined) {
|
|
229
|
-
|
|
300
|
+
unreachableIssue = member.issue;
|
|
230
301
|
break;
|
|
231
302
|
}
|
|
232
303
|
if (truth.state === 'MISSING') {
|
|
@@ -248,16 +319,18 @@ function readMembersShipping(members, read, baseBranch, opts, ledgerStatus) {
|
|
|
248
319
|
const evidence = shippingEvidence(truth, baseBranch, opts.repo, opts.commitInBase);
|
|
249
320
|
if ('shipped' in evidence)
|
|
250
321
|
member.shipped_by = evidence.shipped;
|
|
251
|
-
else
|
|
322
|
+
else if ('refused' in evidence)
|
|
252
323
|
reasons.push(`member-${evidence.refused}:#${member.issue}`);
|
|
324
|
+
else
|
|
325
|
+
undecided.push(`member-${evidence.unknown}:#${member.issue}`);
|
|
253
326
|
}
|
|
254
327
|
if ((0, labels_1.hasLabel)(truth.labels, labels_1.DECISION_PENDING_LABEL)) {
|
|
255
328
|
reasons.push(`member-handed-back:#${member.issue}`);
|
|
256
329
|
}
|
|
257
|
-
if (reasons.length > 0 && !opts.exhaustive)
|
|
330
|
+
if ((reasons.length > 0 || undecided.length > 0) && !opts.exhaustive)
|
|
258
331
|
break;
|
|
259
332
|
}
|
|
260
|
-
return { reasons, members: reports,
|
|
333
|
+
return { reasons, undecided, members: reports, unreachableIssue };
|
|
261
334
|
}
|
|
262
335
|
/**
|
|
263
336
|
* The member half of the predicate: every member CLOSED as `COMPLETED` by
|
|
@@ -266,9 +339,10 @@ function readMembersShipping(members, read, baseBranch, opts, ledgerStatus) {
|
|
|
266
339
|
* members-closed evidence, so the two can never disagree about what
|
|
267
340
|
* "shipped" means.
|
|
268
341
|
*
|
|
269
|
-
* `exhaustive: false` (the engine) stops at the first disqualifying fact
|
|
270
|
-
* skips GitHub entirely when the ledger already
|
|
271
|
-
* stays blocked is re-checked every tick, so the
|
|
342
|
+
* `exhaustive: false` (the engine) stops at the first disqualifying fact or
|
|
343
|
+
* undecided member (#850) and skips GitHub entirely when the ledger already
|
|
344
|
+
* disqualifies — a batch that stays blocked is re-checked every tick, so the
|
|
345
|
+
* cheap answer matters there.
|
|
272
346
|
* `exhaustive: true` (the `sched status --anchors` sweep) reads every member
|
|
273
347
|
* so the operator sees each one's state. Either way a failed read stops the
|
|
274
348
|
* reads: GitHub being unreachable is not worth N more timeouts.
|
|
@@ -285,8 +359,9 @@ function membersShippedVerdict(state, batch, read, opts = {}) {
|
|
|
285
359
|
};
|
|
286
360
|
}
|
|
287
361
|
const gh = readMembersShipping(batch.members, read, batch.base_branch, { exhaustive, repo: opts.repo, commitInBase: opts.commitInBase }, ledgerStatus);
|
|
288
|
-
// A known disqualifier outranks an unreachable read
|
|
289
|
-
//
|
|
362
|
+
// A known disqualifier outranks an unreachable read or an undecided member
|
|
363
|
+
// (#850): whatever the missing poll or the unread reference page would have
|
|
364
|
+
// said, the batch is not closable on this pass.
|
|
290
365
|
return foldMembersVerdict(ledgerReasons, gh, {
|
|
291
366
|
needsOperator: 'needs-operator',
|
|
292
367
|
unknown: 'unknown',
|
|
@@ -298,23 +373,39 @@ function membersShippedVerdict(state, batch, read, opts = {}) {
|
|
|
298
373
|
* {@link classifyOrphanAnchor} (#830): both fold `prefixReasons` (reasons
|
|
299
374
|
* already known before {@link readMembersShipping} ran — ledger blockers for
|
|
300
375
|
* one, the anchor's own handed-back check for the other) together with `gh`'s
|
|
301
|
-
* per-member reasons and unreachable-read signal into the
|
|
302
|
-
* verdict shape — differing only in which kind-name vocabulary
|
|
303
|
-
* uses (`needs-operator`/`unknown`/`closable` vs the ledger-less
|
|
304
|
-
* sweep's own `orphan-*` names, kept deliberately distinct — see
|
|
305
|
-
* {@link OrphanAnchorVerdict}).
|
|
376
|
+
* per-member reasons, undecided members and unreachable-read signal into the
|
|
377
|
+
* SAME three-way verdict shape — differing only in which kind-name vocabulary
|
|
378
|
+
* each caller uses (`needs-operator`/`unknown`/`closable` vs the ledger-less
|
|
379
|
+
* orphan sweep's own `orphan-*` names, kept deliberately distinct — see
|
|
380
|
+
* {@link OrphanAnchorVerdict}). A known disqualifier outranks both kinds of
|
|
381
|
+
* `unknown`, but what could not be settled still rides along after it — each
|
|
382
|
+
* undecided member, then a failed read — so an exhaustive sweep's row names
|
|
383
|
+
* every member it could not settle. Only an `unknown` a failed read produced
|
|
384
|
+
* is `unreachable` (#850); a needs-operator verdict never is, so a sweep
|
|
385
|
+
* reads on past it (the #790 ruling).
|
|
306
386
|
*/
|
|
307
387
|
function foldMembersVerdict(prefixReasons, gh, kinds) {
|
|
308
388
|
const reasons = [...prefixReasons, ...gh.reasons];
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
389
|
+
const failedRead = gh.unreachableIssue === null ? [] : [`issue #${gh.unreachableIssue} unreachable`];
|
|
390
|
+
if (reasons.length > 0) {
|
|
391
|
+
return {
|
|
392
|
+
kind: kinds.needsOperator,
|
|
393
|
+
reasons: [...reasons, ...gh.undecided, ...failedRead],
|
|
394
|
+
members: gh.members,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (failedRead.length > 0) {
|
|
312
398
|
return {
|
|
313
399
|
kind: kinds.unknown,
|
|
314
|
-
reasons: [
|
|
400
|
+
reasons: [...gh.undecided, ...failedRead],
|
|
315
401
|
members: gh.members,
|
|
402
|
+
unreachable: true,
|
|
316
403
|
};
|
|
317
404
|
}
|
|
405
|
+
// #850: every read answered, but not every answer settled its member.
|
|
406
|
+
if (gh.undecided.length > 0) {
|
|
407
|
+
return { kind: kinds.unknown, reasons: gh.undecided, members: gh.members, unreachable: false };
|
|
408
|
+
}
|
|
318
409
|
return { kind: kinds.closable, reasons: [], members: gh.members };
|
|
319
410
|
}
|
|
320
411
|
function anchorGroundVerdict(read, anchorIssue) {
|
|
@@ -343,8 +434,9 @@ function classifyAnchor(state, batch, read, opts = {}) {
|
|
|
343
434
|
if (ground.kind === 'anchor-missing') {
|
|
344
435
|
return { kind: 'needs-operator', reasons: ['anchor-missing'], members: [] };
|
|
345
436
|
}
|
|
346
|
-
if (ground.kind === 'unknown')
|
|
347
|
-
return { kind: 'unknown', reasons: ground.reasons, members: [] };
|
|
437
|
+
if (ground.kind === 'unknown') {
|
|
438
|
+
return { kind: 'unknown', reasons: ground.reasons, members: [], unreachable: true };
|
|
439
|
+
}
|
|
348
440
|
const extraReasons = ground.handedBack ? ['anchor-handed-back'] : [];
|
|
349
441
|
return membersShippedVerdict(state, batch, read, { ...opts, extraReasons });
|
|
350
442
|
}
|
|
@@ -416,24 +508,28 @@ function closeAnchor(exec, repoDir, repo, batch, anchorIssue, members) {
|
|
|
416
508
|
? 'close-failed'
|
|
417
509
|
: 'closed';
|
|
418
510
|
}
|
|
511
|
+
/** The reason on every candidate a sweep reports without reading, after a failed read. */
|
|
512
|
+
const SWEEP_ABORTED_REASON = 'GitHub unreachable — sweep aborted';
|
|
419
513
|
/**
|
|
420
514
|
* The fail-closed sweep loop shared by {@link sweepAnchors} and
|
|
421
515
|
* {@link sweepOrphanAnchors} (#830): classify each candidate in order via
|
|
422
516
|
* `classify`, which returns `null` to SKIP a candidate entirely (both sweeps
|
|
423
517
|
* hit this on the same list-then-read race — the anchor closed between the
|
|
424
|
-
* list and this read). Once a classified verdict
|
|
425
|
-
* every LATER candidate is reported `abortedVerdict` WITHOUT calling
|
|
518
|
+
* list and this read). Once a classified verdict is `unreachable` (a read
|
|
519
|
+
* failed), every LATER candidate is reported `abortedVerdict` WITHOUT calling
|
|
426
520
|
* `classify` again — GitHub being unreachable is not worth N more timeouts,
|
|
427
|
-
* and a partial outage must never look like "the rest are clean".
|
|
521
|
+
* and a partial outage must never look like "the rest are clean". An
|
|
522
|
+
* `unknown` GitHub answered but could not settle (#850) is no outage: the
|
|
523
|
+
* sweep reads on, so it never reports later batches as unreachable.
|
|
428
524
|
*/
|
|
429
|
-
function sweepFailClosed(candidates,
|
|
525
|
+
function sweepFailClosed(candidates, abortedVerdict, classify, toItem) {
|
|
430
526
|
const items = [];
|
|
431
527
|
let aborted = false;
|
|
432
528
|
for (const candidate of candidates) {
|
|
433
529
|
const verdict = aborted ? abortedVerdict : classify(candidate);
|
|
434
530
|
if (verdict === null)
|
|
435
531
|
continue;
|
|
436
|
-
if (verdict.
|
|
532
|
+
if (verdict.unreachable === true)
|
|
437
533
|
aborted = true;
|
|
438
534
|
items.push(toItem(candidate, verdict));
|
|
439
535
|
}
|
|
@@ -443,13 +539,14 @@ function sweepFailClosed(candidates, unknownKind, abortedVerdict, classify, toIt
|
|
|
443
539
|
* `sched status --anchors`' sweep (#768): every still-open anchor of a batch
|
|
444
540
|
* that is no longer in flight, marked `closable` (the strict condition holds —
|
|
445
541
|
* the engine will close it, within its 7-day window), `needs-operator`
|
|
446
|
-
* (anything else), or `unknown` (a GitHub read failed —
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
542
|
+
* (anything else), or `unknown` (a GitHub read failed, or — #850 — a member's
|
|
543
|
+
* closing-reference list was truncated; nothing decided), with each member's
|
|
544
|
+
* state. Report-only by construction: it takes no exec for writes and has no
|
|
545
|
+
* way to close anything. The first FAILED read stops the sweep's reads; every
|
|
546
|
+
* later batch is reported `unknown` without a call.
|
|
450
547
|
*/
|
|
451
548
|
function sweepAnchors(state, read, opts = {}) {
|
|
452
|
-
return sweepFailClosed(openAnchorBatches(state, SWEEP_BATCH_STATUSES),
|
|
549
|
+
return sweepFailClosed(openAnchorBatches(state, SWEEP_BATCH_STATUSES), { kind: 'unknown', reasons: [SWEEP_ABORTED_REASON], members: [], unreachable: true }, (batch) => {
|
|
453
550
|
const verdict = classifyAnchor(state, batch, read, { exhaustive: true, ...opts });
|
|
454
551
|
return verdict.kind === 'anchor-closed' ? null : verdict;
|
|
455
552
|
}, (batch, verdict) => ({
|
|
@@ -569,7 +666,7 @@ function classifyOrphanAnchor(candidate, read, opts = {}) {
|
|
|
569
666
|
return { kind: 'orphan-needs-operator', reasons: ['anchor-missing'], members: [] };
|
|
570
667
|
}
|
|
571
668
|
if (ground.kind === 'unknown') {
|
|
572
|
-
return { kind: 'orphan-unknown', reasons: ground.reasons, members: [] };
|
|
669
|
+
return { kind: 'orphan-unknown', reasons: ground.reasons, members: [], unreachable: true };
|
|
573
670
|
}
|
|
574
671
|
if (candidate.members.length === 0) {
|
|
575
672
|
return { kind: 'orphan-needs-operator', reasons: ['no-members-recovered'], members: [] };
|
|
@@ -651,7 +748,8 @@ exports.ORPHAN_SWEEP_MAX_ANCHORS = 20;
|
|
|
651
748
|
* left real orphan candidates unprobed. Classifying stops at the first
|
|
652
749
|
* failed member/anchor read (the same fail-closed posture as
|
|
653
750
|
* `sweepAnchors`): every anchor after that point is reported `orphan-unknown`
|
|
654
|
-
* without a further call.
|
|
751
|
+
* without a further call. An `orphan-unknown` from a truncated
|
|
752
|
+
* closing-reference list (#850) is not a failed read and stops nothing.
|
|
655
753
|
*/
|
|
656
754
|
function sweepOrphanAnchors(state, list, read, opts = {}) {
|
|
657
755
|
const anchors = list();
|
|
@@ -660,7 +758,7 @@ function sweepOrphanAnchors(state, list, read, opts = {}) {
|
|
|
660
758
|
const ledgerAnchors = new Set(state.batches.map((b) => b.anchor).filter((a) => a !== null));
|
|
661
759
|
const orphanCandidates = anchors.filter((issue) => !ledgerAnchors.has(issue.number));
|
|
662
760
|
const truncated = orphanCandidates.length > exports.ORPHAN_SWEEP_MAX_ANCHORS;
|
|
663
|
-
const items = sweepFailClosed(orphanCandidates.slice(0, exports.ORPHAN_SWEEP_MAX_ANCHORS),
|
|
761
|
+
const items = sweepFailClosed(orphanCandidates.slice(0, exports.ORPHAN_SWEEP_MAX_ANCHORS), { kind: 'orphan-unknown', reasons: [SWEEP_ABORTED_REASON], members: [], unreachable: true }, (issue) => {
|
|
664
762
|
const { members, base_branch, members_over_cap } = parseOrphanAnchorBody(issue.body);
|
|
665
763
|
return classifyOrphanAnchor({ anchor: issue.number, members, base_branch, members_over_cap }, read, opts);
|
|
666
764
|
}, (issue, verdict) => ({
|