@ai-dossier/sched 0.3.0 → 0.4.1
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 +97 -3
- package/dist/attribution.d.ts +132 -0
- package/dist/attribution.d.ts.map +1 -0
- package/dist/attribution.js +268 -0
- package/dist/attribution.js.map +1 -0
- package/dist/bisect.d.ts +67 -0
- package/dist/bisect.d.ts.map +1 -0
- package/dist/bisect.js +122 -0
- package/dist/bisect.js.map +1 -0
- package/dist/dispatch.d.ts +22 -0
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +50 -1
- package/dist/dispatch.js.map +1 -1
- package/dist/enqueue.d.ts +9 -0
- package/dist/enqueue.d.ts.map +1 -1
- package/dist/enqueue.js +96 -17
- package/dist/enqueue.js.map +1 -1
- package/dist/groundtruth.d.ts +6 -0
- package/dist/groundtruth.d.ts.map +1 -1
- package/dist/groundtruth.js +11 -3
- package/dist/groundtruth.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -2
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +8 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +18 -0
- package/dist/json.js.map +1 -0
- package/dist/recovery.d.ts +263 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +730 -0
- package/dist/recovery.js.map +1 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +6 -28
- package/dist/scheduler.js.map +1 -1
- package/dist/state.d.ts +55 -3
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +205 -6
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +114 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +33 -3
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ machines, crash-safe persistence, the **dispatch engine** (#464: spawning agent
|
|
|
7
7
|
processes, verifying their completion against ground truth, mechanizing the
|
|
8
8
|
stall/escalation ladder), and since #468 the **PR watcher + tail work** (parked-PR
|
|
9
9
|
watching, script-based teardown, cheap-tier report dispatch — retiring the fleet
|
|
10
|
-
pattern of re-dispatching a full-cycle run for the tail)
|
|
10
|
+
pattern of re-dispatching a full-cycle run for the tail), and since #472 **batch failure
|
|
11
|
+
recovery** (attribution, bisect, one bounded fix, eviction, dissolve). The scheduler itself **never
|
|
11
12
|
invokes an LLM** — it spawns the agent process the operator configured and reconciles
|
|
12
13
|
the durable record (`ai-dossier runstate` / `gh` / `git`) that the spawned run leaves
|
|
13
14
|
behind.
|
|
@@ -85,6 +86,77 @@ Two engine-safety policies were explicit product decisions on #464:
|
|
|
85
86
|
Only `issue:<n>` units are dispatched today — batch member sequencing is a follow-up
|
|
86
87
|
(#464 non-goal).
|
|
87
88
|
|
|
89
|
+
## Batch failure recovery (#472)
|
|
90
|
+
|
|
91
|
+
What happens when a batch's aggregate suite goes red, or its PR will not merge
|
|
92
|
+
(RFC-0001 §F.2/F.8/F.9).
|
|
93
|
+
|
|
94
|
+
**Not yet wired into `sched start`** — `tick()` still dispatches only `issue:<n>` units
|
|
95
|
+
(the batch execution loop is a follow-up, see above). These modules are the library
|
|
96
|
+
surface that loop will call, and they are tested standalone against real scratch repos.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
validating → attributing → fixing (ONE bounded attempt) → validating
|
|
100
|
+
→ evicting (revert the member's commits) → validating
|
|
101
|
+
> ⅓ of members evicted, or a revert conflict → dissolving → members requeued
|
|
102
|
+
awaiting-merge (CONFLICTING | auto-merge-blocked)
|
|
103
|
+
→ rebasing → re-validating → shipping
|
|
104
|
+
→ (2nd occurrence) dissolving into two half-batches
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
1. **Attribution (AC1)** — `attributeByOverlap` maps each failing test to a member by
|
|
108
|
+
focused-test match, then by changed-path overlap. Exactly one candidate attributes;
|
|
109
|
+
more than one is AMBIGUOUS and none is UNATTRIBUTED — neither is ever guessed. When
|
|
110
|
+
the caller supplies a `BisectSpec`, both go to `runAttributionBisect`: a real
|
|
111
|
+
`git bisect run` over the branch's `good..bad` range executing ONLY the failing tests,
|
|
112
|
+
whose first-bad commit is mapped to a member through the `(#N)` subject trailer on the
|
|
113
|
+
branch's issue-boundary commits (every unresolved test is then attributed to that
|
|
114
|
+
member). A first-bad commit with no trailer, or an abbreviated sha matching two
|
|
115
|
+
commits, reports `unattributable` rather than blaming a neighbour. The bisect refuses
|
|
116
|
+
to run at all unless the test command actually discriminates — it must fail at `bad`
|
|
117
|
+
AND pass at `good`, so a missing runner cannot silently convict the earliest member —
|
|
118
|
+
and it always resets the checkout to where it found it. Without a `BisectSpec`,
|
|
119
|
+
overlap is the whole verdict and unresolved tests stay unattributed.
|
|
120
|
+
2. **One bounded fix attempt (AC2)** — `beginFixAttempt` returns the mid-tier command and
|
|
121
|
+
prompt for the CALLER to spawn (sched never invokes an LLM) and records the attempt.
|
|
122
|
+
A second call for the same member returns `null`: the next step is eviction, so a
|
|
123
|
+
batch cannot burn its budget on one broken member.
|
|
124
|
+
3. **Eviction (AC2)** — `evictMembers` reverts the member's commits newest-first across
|
|
125
|
+
members (an eviction group reverts together), requeues it as full-cycle with
|
|
126
|
+
`failure_evidence` attached (batch, reason, failing tests, attribution method, reverted
|
|
127
|
+
commits), re-runs the suite and checks the dissolve trigger. A conflicting revert is
|
|
128
|
+
aborted so the worktree is clean and the batch dissolves — the reverts that already
|
|
129
|
+
landed ride along on the abandoned branch, which is why it is abandoned rather than
|
|
130
|
+
reused. An eviction group that reaches an already-shipped member dissolves instead of
|
|
131
|
+
reverting merged work.
|
|
132
|
+
4. **Dissolve (AC3)** — `dissolveBatch` marks the batch `dissolved` and requeues every
|
|
133
|
+
UNSHIPPED member: `full` (each as its own full-cycle run) or `halved` (one or two fresh
|
|
134
|
+
`forming` half-batches — a single remaining member yields one — entries retagged,
|
|
135
|
+
eviction groups inherited where they survive the split). Shipped and terminal members
|
|
136
|
+
keep their outcome; nothing green is discarded, and no git runs — the batch branch is
|
|
137
|
+
simply left behind unmerged, since sched deletes nothing.
|
|
138
|
+
5. **PR conflict (AC4)** — `handlePrConflict` rebases the batch branch, re-runs the suite
|
|
139
|
+
and re-ships ONCE. A second occurrence, a conflicting rebase, a failed fetch, an
|
|
140
|
+
unusable `base_branch`, a checkout that is not on the batch branch, or a red suite
|
|
141
|
+
after a clean rebase dissolves into two half-batches.
|
|
142
|
+
6. **Milestones (AC5)** — every eviction and dissolve posts a `batch-validate` /
|
|
143
|
+
`batch-ship` milestone to the batch ANCHOR issue via `ai-dossier runstate post`, with
|
|
144
|
+
the reason, the evicted/requeued/preserved members and the attribution method (a
|
|
145
|
+
successful re-ship posts `batch-ship awaiting-merge`); each per-member outcome is
|
|
146
|
+
journaled and kept in the batch's `evictions` (the classifier feedback signal). A batch
|
|
147
|
+
with no `anchor` or no `run_id` cannot post — the CLI requires both — so the milestone
|
|
148
|
+
it could not post is journaled in full instead of vanishing.
|
|
149
|
+
|
|
150
|
+
Ten journal events carry the detail: `suite-failed`, `attributed`, `fix-dispatched`,
|
|
151
|
+
`fix-resolved`, `member-evicted`, `revert-conflict`, `batch-rebased`, `batch-dissolved`,
|
|
152
|
+
`batch-split` and `milestone-post-failed`, plus `git-failed` for any git command that
|
|
153
|
+
returned non-zero (the injected `ExecFn` collapses every git failure into `null`, so the
|
|
154
|
+
command that produced one is always recorded).
|
|
155
|
+
|
|
156
|
+
Schema 1.3.0 carries the new state: `BatchEntry` gains `anchor`, `branch`, `run_id`,
|
|
157
|
+
`eviction_groups`, `evictions`, `fix_attempts` and `rebase_attempts`; `QueueEntry` gains
|
|
158
|
+
`failure_evidence`. 1.2.0 states migrate on load.
|
|
159
|
+
|
|
88
160
|
## API surface
|
|
89
161
|
|
|
90
162
|
```ts
|
|
@@ -114,6 +186,26 @@ import {
|
|
|
114
186
|
runTeardown, // #468 script teardown for a merged unit (pool return / worktree remove)
|
|
115
187
|
isSafeWorktree, // worktree-path containment check (CWE-22)
|
|
116
188
|
TEARDOWN_TIMEOUT_MS, // teardown subprocess timeout (120 s)
|
|
189
|
+
attributeByOverlap, // #472 pure stage-1 attribution: failing tests → members
|
|
190
|
+
parseVitestJson, // vitest --reporter=json → failing tests
|
|
191
|
+
parseBoundaryCommits, // git log → issue-boundary commits via the (#N) trailer
|
|
192
|
+
memberRanges, // boundary commits → each member's commit list
|
|
193
|
+
runAttributionBisect, // stage-2: real git bisect over the failing tests only
|
|
194
|
+
beginAttribution, // validating → attributing (overlap, then bisect if needed)
|
|
195
|
+
beginFixAttempt, // the ONE bounded mid-tier fix dispatch instruction
|
|
196
|
+
resolveFixAttempt, // record its outcome, back to validating
|
|
197
|
+
evictMembers, // revert + requeue with evidence + suite re-run + dissolve check
|
|
198
|
+
checkDissolveTrigger, // pure: > ⅓ of members evicted
|
|
199
|
+
dissolveBatch, // full or halved dissolve; preserves everything green
|
|
200
|
+
handlePrConflict, // rebase + re-ship once, then dissolve into halves
|
|
201
|
+
createExecMilestonePoster, // batch milestones via `ai-dossier runstate post`
|
|
202
|
+
expandEvictionGroups, // members that must revert together (§E.4 eviction groups)
|
|
203
|
+
requeueMember, // the one requeue path abandon/evict/dissolve all take
|
|
204
|
+
isPreservedMember, // the single definition of "already green"
|
|
205
|
+
createBatch, // the single BatchEntry constructor
|
|
206
|
+
type RecoveryDeps, // inject exec/repoDir/journal/milestone-poster/suite-runner/clock
|
|
207
|
+
type SuiteRunner, // re-runs the aggregate suite after a revert or rebase
|
|
208
|
+
type BatchMilestonePoster, // batch-milestone sink (createExecMilestonePoster is default)
|
|
117
209
|
Journal, // append-only events.jsonl
|
|
118
210
|
transitionIssue, transitionBatch, transitionSlot, // typed §D transitions
|
|
119
211
|
TRANSITIONS, // the transition tables themselves (for previews)
|
|
@@ -150,8 +242,10 @@ fake agents and stub ground truth; no LLM calls anywhere.
|
|
|
150
242
|
- **Corrupt state is loud**: `load()` throws `CorruptStateError` naming the file —
|
|
151
243
|
never a silent queue reset. `state.json` is deletable and rebuildable from GitHub,
|
|
152
244
|
which remains the system of record.
|
|
153
|
-
- **Schema**: state/config files from #460 (schema 1.0.0)
|
|
154
|
-
migrate to 1.
|
|
245
|
+
- **Schema**: state/config files from #460 (schema 1.0.0), #464 (1.1.0) and #468 (1.2.0)
|
|
246
|
+
load and migrate to 1.3.0 automatically (slot `branch`/`last_head`/`pid_start`, entry
|
|
247
|
+
`pr`/`cleanup`/`failure_evidence`, batch `anchor`/`branch`/`run_id`/`eviction_groups`/
|
|
248
|
+
`evictions`/`fix_attempts`/`rebase_attempts`,
|
|
155
249
|
and state-level `last_pr_poll_at` backfill to null).
|
|
156
250
|
- **`max_slots`** bounds live units (`assigned | running | recovering`); dependency
|
|
157
251
|
edges gate readiness — an issue with an unmerged dependency, and a batch behind an
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failure attribution, stage 1 (#472 AC1): map failing tests to batch members
|
|
3
|
+
* without an LLM, using only what the batch run already recorded.
|
|
4
|
+
*
|
|
5
|
+
* Two overlap signals, in order of confidence:
|
|
6
|
+
* 1. **focused tests** — the member's own plan/implement phase named these
|
|
7
|
+
* test files; a failure in one of them is that member's.
|
|
8
|
+
* 2. **changed paths** — the failing test file was changed by the member, or
|
|
9
|
+
* sits under a directory the member changed (the `src/x.ts` →
|
|
10
|
+
* `src/__tests__/x.test.ts` relationship, expressed structurally rather
|
|
11
|
+
* than by naming convention).
|
|
12
|
+
*
|
|
13
|
+
* A test with exactly one candidate member is ATTRIBUTED; more than one is
|
|
14
|
+
* AMBIGUOUS and none is UNATTRIBUTED. Both of those go to stage 2 (`bisect.ts`)
|
|
15
|
+
* — this module never guesses, because a wrong attribution evicts innocent
|
|
16
|
+
* work. Everything here is pure; `recovery.ts` owns the effects.
|
|
17
|
+
*/
|
|
18
|
+
/** One failing test, as the aggregate suite reported it. */
|
|
19
|
+
export interface FailingTest {
|
|
20
|
+
/** Test file path, repo-relative where the runner reports it that way. */
|
|
21
|
+
file: string;
|
|
22
|
+
/** Full test name (describe path + title). */
|
|
23
|
+
name: string;
|
|
24
|
+
/** Stable id used in evidence and journal lines: `<file>::<name>`. */
|
|
25
|
+
id: string;
|
|
26
|
+
}
|
|
27
|
+
/** What one batch member touched — the attribution surface, recorded as it ran. */
|
|
28
|
+
export interface MemberFootprint {
|
|
29
|
+
issue: number;
|
|
30
|
+
/** Repo-relative paths the member's commits changed. */
|
|
31
|
+
changedPaths: string[];
|
|
32
|
+
/** Test files the member's own phases ran focused (strongest signal). */
|
|
33
|
+
focusedTests: string[];
|
|
34
|
+
}
|
|
35
|
+
/** A failing test that could belong to more than one member. */
|
|
36
|
+
export interface AmbiguousTest {
|
|
37
|
+
test: FailingTest;
|
|
38
|
+
/** Member issue numbers that all overlap it, ascending. */
|
|
39
|
+
candidates: number[];
|
|
40
|
+
}
|
|
41
|
+
/** Stage-1 attribution verdict for one suite run. */
|
|
42
|
+
export interface OverlapAttribution {
|
|
43
|
+
/** Member issue → the failing tests uniquely attributed to it. */
|
|
44
|
+
attributed: Map<number, FailingTest[]>;
|
|
45
|
+
/** Tests overlapping several members — bisect decides. */
|
|
46
|
+
ambiguous: AmbiguousTest[];
|
|
47
|
+
/** Tests overlapping no member at all — bisect decides. */
|
|
48
|
+
unattributed: FailingTest[];
|
|
49
|
+
}
|
|
50
|
+
/** Build a `FailingTest` with the derived id, so the id format exists once. */
|
|
51
|
+
export declare function failingTest(file: string, name: string): FailingTest;
|
|
52
|
+
/**
|
|
53
|
+
* Stage-1 attribution: overlap only, no git, no LLM (AC1). Deterministic —
|
|
54
|
+
* candidates are compared in ascending issue order so the same inputs always
|
|
55
|
+
* produce the same verdict.
|
|
56
|
+
*/
|
|
57
|
+
export declare function attributeByOverlap(failing: readonly FailingTest[], footprints: readonly MemberFootprint[]): OverlapAttribution;
|
|
58
|
+
/** Members with at least one failing test attributed to them, ascending. */
|
|
59
|
+
export declare function offendersOf(attributed: ReadonlyMap<number, unknown>): number[];
|
|
60
|
+
/**
|
|
61
|
+
* Failing tests from `vitest run --reporter=json` output. Tolerates the
|
|
62
|
+
* surrounding noise a real run prints around the JSON document (vitest writes
|
|
63
|
+
* the report to stdout alongside its own banner), and skips any record it
|
|
64
|
+
* cannot read rather than throwing — an unparseable suite report degrades to
|
|
65
|
+
* "no attributable tests", which routes to bisect, never to a guess.
|
|
66
|
+
*/
|
|
67
|
+
export declare function parseVitestJson(stdout: string | null): FailingTest[];
|
|
68
|
+
/** One commit on the batch branch, with the member it belongs to. */
|
|
69
|
+
export interface BoundaryCommit {
|
|
70
|
+
sha: string;
|
|
71
|
+
subject: string;
|
|
72
|
+
/** Member issue from the `(#N)` subject trailer; null when the commit has none. */
|
|
73
|
+
issue: number | null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A full or abbreviated sha as git prints it — 7 to 40 hex characters. Bisect
|
|
77
|
+
* endpoints and revert argv are validated against this before they become git
|
|
78
|
+
* arguments (CWE-88).
|
|
79
|
+
*/
|
|
80
|
+
export declare const SHA_RE: RegExp;
|
|
81
|
+
/**
|
|
82
|
+
* A git ref name safe to interpolate into argv (CWE-88) — the pattern
|
|
83
|
+
* `groundtruth.ts` established for branch names off untrusted milestone text.
|
|
84
|
+
* Defined here, beside `SHA_RE`, so the package has one "validate before it
|
|
85
|
+
* becomes a git argument" home rather than a copy per module.
|
|
86
|
+
*/
|
|
87
|
+
export declare const SAFE_REF_RE: RegExp;
|
|
88
|
+
/**
|
|
89
|
+
* Parse `git log --reverse --format=%H%x09%s <base>..<head>` — OLDEST FIRST,
|
|
90
|
+
* which is the order eviction ranges and bisect boundaries are built in.
|
|
91
|
+
* Malformed lines are skipped; a line whose subject has no `(#N)` trailer
|
|
92
|
+
* still yields a commit, with `issue: null`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function parseBoundaryCommits(gitLogOutput: string | null): BoundaryCommit[];
|
|
95
|
+
/** The commits one member contributed to the batch branch, oldest first. */
|
|
96
|
+
export interface MemberRange {
|
|
97
|
+
issue: number;
|
|
98
|
+
/** Oldest commit of the member. */
|
|
99
|
+
from: string;
|
|
100
|
+
/** Newest commit of the member. */
|
|
101
|
+
to: string;
|
|
102
|
+
/** Every commit of the member, oldest first. */
|
|
103
|
+
commits: string[];
|
|
104
|
+
/**
|
|
105
|
+
* Each commit's index in the boundary list, parallel to `commits`. Grouping
|
|
106
|
+
* by member loses branch order; eviction needs it back to revert newest-first
|
|
107
|
+
* ACROSS members (see `memberRanges`).
|
|
108
|
+
*/
|
|
109
|
+
positions: number[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Group boundary commits (oldest first) by member.
|
|
113
|
+
*
|
|
114
|
+
* `from`/`to` describe the member's span for reporting; `commits` is what a
|
|
115
|
+
* revert actually walks. They differ when members interleave — reverting the
|
|
116
|
+
* literal `from^..to` range would then revert a neighbour's commits too, so
|
|
117
|
+
* eviction reverts the explicit commits instead, ordered by `positions`:
|
|
118
|
+
* for a branch `A1 B1 A2 B2`, evicting both members must revert
|
|
119
|
+
* `B2 A2 B1 A1`, not each member's commits as a block.
|
|
120
|
+
*/
|
|
121
|
+
export declare function memberRanges(boundary: readonly BoundaryCommit[]): MemberRange[];
|
|
122
|
+
/**
|
|
123
|
+
* The member owning `sha`, or null when the commit carries no `(#N)` trailer.
|
|
124
|
+
*
|
|
125
|
+
* Either side may be abbreviated (bisect reports a short sha; boundary commits
|
|
126
|
+
* are full), so matching is prefix-based — but an AMBIGUOUS prefix returns null
|
|
127
|
+
* rather than the first hit. Attribution feeds eviction, and eviction reverts
|
|
128
|
+
* work: a wrong answer here destroys an innocent member's commits, so "cannot
|
|
129
|
+
* tell" must never collapse into "probably this one".
|
|
130
|
+
*/
|
|
131
|
+
export declare function memberOfCommit(boundary: readonly BoundaryCommit[], sha: string): number | null;
|
|
132
|
+
//# sourceMappingURL=attribution.d.ts.map
|
|
@@ -0,0 +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;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"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Failure attribution, stage 1 (#472 AC1): map failing tests to batch members
|
|
4
|
+
* without an LLM, using only what the batch run already recorded.
|
|
5
|
+
*
|
|
6
|
+
* Two overlap signals, in order of confidence:
|
|
7
|
+
* 1. **focused tests** — the member's own plan/implement phase named these
|
|
8
|
+
* test files; a failure in one of them is that member's.
|
|
9
|
+
* 2. **changed paths** — the failing test file was changed by the member, or
|
|
10
|
+
* sits under a directory the member changed (the `src/x.ts` →
|
|
11
|
+
* `src/__tests__/x.test.ts` relationship, expressed structurally rather
|
|
12
|
+
* than by naming convention).
|
|
13
|
+
*
|
|
14
|
+
* A test with exactly one candidate member is ATTRIBUTED; more than one is
|
|
15
|
+
* AMBIGUOUS and none is UNATTRIBUTED. Both of those go to stage 2 (`bisect.ts`)
|
|
16
|
+
* — this module never guesses, because a wrong attribution evicts innocent
|
|
17
|
+
* work. Everything here is pure; `recovery.ts` owns the effects.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.SAFE_REF_RE = exports.SHA_RE = void 0;
|
|
21
|
+
exports.failingTest = failingTest;
|
|
22
|
+
exports.attributeByOverlap = attributeByOverlap;
|
|
23
|
+
exports.offendersOf = offendersOf;
|
|
24
|
+
exports.parseVitestJson = parseVitestJson;
|
|
25
|
+
exports.parseBoundaryCommits = parseBoundaryCommits;
|
|
26
|
+
exports.memberRanges = memberRanges;
|
|
27
|
+
exports.memberOfCommit = memberOfCommit;
|
|
28
|
+
/** Build a `FailingTest` with the derived id, so the id format exists once. */
|
|
29
|
+
function failingTest(file, name) {
|
|
30
|
+
return { file, name, id: `${file}::${name}` };
|
|
31
|
+
}
|
|
32
|
+
/** Normalize a path for comparison: strip `./`, collapse `\` to `/`, drop a trailing `/`. */
|
|
33
|
+
function normalizePath(p) {
|
|
34
|
+
const slashed = p.replaceAll('\\', '/').replace(/^\.\//, '');
|
|
35
|
+
return slashed.endsWith('/') ? slashed.slice(0, -1) : slashed;
|
|
36
|
+
}
|
|
37
|
+
/** The directory part of a path, or `''` for a top-level file. */
|
|
38
|
+
function dirOf(p) {
|
|
39
|
+
const idx = p.lastIndexOf('/');
|
|
40
|
+
return idx === -1 ? '' : p.slice(0, idx);
|
|
41
|
+
}
|
|
42
|
+
/** Whether `member` explicitly ran `normalizedFile` focused — the strongest signal. */
|
|
43
|
+
function focusesOn(member, normalizedFile) {
|
|
44
|
+
return member.focusedTests.some((t) => normalizePath(t) === normalizedFile);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether `member` overlaps `testFile`. Focused tests win outright; otherwise
|
|
48
|
+
* the file must be one the member changed, or live under a directory it
|
|
49
|
+
* changed. A member that changed only top-level files (dir `''`) never matches
|
|
50
|
+
* by directory — an empty prefix would otherwise own the entire repo.
|
|
51
|
+
*/
|
|
52
|
+
function overlaps(member, testFile) {
|
|
53
|
+
const file = normalizePath(testFile);
|
|
54
|
+
if (focusesOn(member, file))
|
|
55
|
+
return true;
|
|
56
|
+
for (const raw of member.changedPaths) {
|
|
57
|
+
const changed = normalizePath(raw);
|
|
58
|
+
if (changed === file)
|
|
59
|
+
return true;
|
|
60
|
+
const dir = dirOf(changed);
|
|
61
|
+
if (dir !== '' && file.startsWith(`${dir}/`))
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Stage-1 attribution: overlap only, no git, no LLM (AC1). Deterministic —
|
|
68
|
+
* candidates are compared in ascending issue order so the same inputs always
|
|
69
|
+
* produce the same verdict.
|
|
70
|
+
*/
|
|
71
|
+
function attributeByOverlap(failing, footprints) {
|
|
72
|
+
const attributed = new Map();
|
|
73
|
+
const ambiguous = [];
|
|
74
|
+
const unattributed = [];
|
|
75
|
+
const members = [...footprints].sort((a, b) => a.issue - b.issue);
|
|
76
|
+
for (const test of failing) {
|
|
77
|
+
// A focused-test match is the stronger signal: when any member claims the
|
|
78
|
+
// file explicitly, members that merely changed a neighbouring path are not
|
|
79
|
+
// candidates — otherwise the strong signal could never break a tie.
|
|
80
|
+
const file = normalizePath(test.file);
|
|
81
|
+
const focused = members.filter((m) => focusesOn(m, file));
|
|
82
|
+
const candidates = focused.length > 0 ? focused : members.filter((m) => overlaps(m, test.file));
|
|
83
|
+
if (candidates.length === 1) {
|
|
84
|
+
const issue = candidates[0].issue;
|
|
85
|
+
attributed.set(issue, [...(attributed.get(issue) ?? []), test]);
|
|
86
|
+
}
|
|
87
|
+
else if (candidates.length > 1) {
|
|
88
|
+
ambiguous.push({ test, candidates: candidates.map((m) => m.issue) });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
unattributed.push(test);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { attributed, ambiguous, unattributed };
|
|
95
|
+
}
|
|
96
|
+
/** Members with at least one failing test attributed to them, ascending. */
|
|
97
|
+
function offendersOf(attributed) {
|
|
98
|
+
return [...attributed.keys()].sort((a, b) => a - b);
|
|
99
|
+
}
|
|
100
|
+
// --- Parsers ---
|
|
101
|
+
/**
|
|
102
|
+
* Failing tests from `vitest run --reporter=json` output. Tolerates the
|
|
103
|
+
* surrounding noise a real run prints around the JSON document (vitest writes
|
|
104
|
+
* the report to stdout alongside its own banner), and skips any record it
|
|
105
|
+
* cannot read rather than throwing — an unparseable suite report degrades to
|
|
106
|
+
* "no attributable tests", which routes to bisect, never to a guess.
|
|
107
|
+
*/
|
|
108
|
+
function parseVitestJson(stdout) {
|
|
109
|
+
if (stdout === null)
|
|
110
|
+
return [];
|
|
111
|
+
const parsed = extractJsonObject(stdout);
|
|
112
|
+
if (parsed === null)
|
|
113
|
+
return [];
|
|
114
|
+
const results = parsed.testResults;
|
|
115
|
+
if (!Array.isArray(results))
|
|
116
|
+
return [];
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const raw of results) {
|
|
119
|
+
if (raw === null || typeof raw !== 'object')
|
|
120
|
+
continue;
|
|
121
|
+
const suite = raw;
|
|
122
|
+
const file = typeof suite.name === 'string' ? suite.name : null;
|
|
123
|
+
if (file === null || !Array.isArray(suite.assertionResults))
|
|
124
|
+
continue;
|
|
125
|
+
for (const rawAssertion of suite.assertionResults) {
|
|
126
|
+
if (rawAssertion === null || typeof rawAssertion !== 'object')
|
|
127
|
+
continue;
|
|
128
|
+
const assertion = rawAssertion;
|
|
129
|
+
if (assertion.status !== 'failed')
|
|
130
|
+
continue;
|
|
131
|
+
const name = typeof assertion.fullName === 'string'
|
|
132
|
+
? assertion.fullName
|
|
133
|
+
: typeof assertion.title === 'string'
|
|
134
|
+
? assertion.title
|
|
135
|
+
: '';
|
|
136
|
+
out.push(failingTest(file, name));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/** The first balanced `{...}` document in `text`, parsed; null when there is none. */
|
|
142
|
+
function extractJsonObject(text) {
|
|
143
|
+
const start = text.indexOf('{');
|
|
144
|
+
if (start === -1)
|
|
145
|
+
return null;
|
|
146
|
+
let depth = 0;
|
|
147
|
+
let inString = false;
|
|
148
|
+
let escaped = false;
|
|
149
|
+
for (let i = start; i < text.length; i++) {
|
|
150
|
+
const ch = text[i];
|
|
151
|
+
if (inString) {
|
|
152
|
+
if (escaped)
|
|
153
|
+
escaped = false;
|
|
154
|
+
else if (ch === '\\')
|
|
155
|
+
escaped = true;
|
|
156
|
+
else if (ch === '"')
|
|
157
|
+
inString = false;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (ch === '"')
|
|
161
|
+
inString = true;
|
|
162
|
+
else if (ch === '{')
|
|
163
|
+
depth++;
|
|
164
|
+
else if (ch === '}') {
|
|
165
|
+
depth--;
|
|
166
|
+
if (depth === 0) {
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(text.slice(start, i + 1));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The established commit ↔ member key: the `(#N)` trailer a squash-merge (and
|
|
180
|
+
* this project's commit convention) leaves at the end of the subject. A commit
|
|
181
|
+
* WITHOUT one maps to `issue: null` — unattributable, never a guess.
|
|
182
|
+
*/
|
|
183
|
+
const ISSUE_TRAILER_RE = /\(#(\d+)\)\s*$/;
|
|
184
|
+
/**
|
|
185
|
+
* A full or abbreviated sha as git prints it — 7 to 40 hex characters. Bisect
|
|
186
|
+
* endpoints and revert argv are validated against this before they become git
|
|
187
|
+
* arguments (CWE-88).
|
|
188
|
+
*/
|
|
189
|
+
exports.SHA_RE = /^[0-9a-f]{7,40}$/i;
|
|
190
|
+
/**
|
|
191
|
+
* A git ref name safe to interpolate into argv (CWE-88) — the pattern
|
|
192
|
+
* `groundtruth.ts` established for branch names off untrusted milestone text.
|
|
193
|
+
* Defined here, beside `SHA_RE`, so the package has one "validate before it
|
|
194
|
+
* becomes a git argument" home rather than a copy per module.
|
|
195
|
+
*/
|
|
196
|
+
exports.SAFE_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
197
|
+
/**
|
|
198
|
+
* Parse `git log --reverse --format=%H%x09%s <base>..<head>` — OLDEST FIRST,
|
|
199
|
+
* which is the order eviction ranges and bisect boundaries are built in.
|
|
200
|
+
* Malformed lines are skipped; a line whose subject has no `(#N)` trailer
|
|
201
|
+
* still yields a commit, with `issue: null`.
|
|
202
|
+
*/
|
|
203
|
+
function parseBoundaryCommits(gitLogOutput) {
|
|
204
|
+
if (gitLogOutput === null)
|
|
205
|
+
return [];
|
|
206
|
+
const out = [];
|
|
207
|
+
for (const line of gitLogOutput.split('\n')) {
|
|
208
|
+
if (line.trim() === '')
|
|
209
|
+
continue;
|
|
210
|
+
const tab = line.indexOf('\t');
|
|
211
|
+
const sha = (tab === -1 ? line : line.slice(0, tab)).trim();
|
|
212
|
+
if (!exports.SHA_RE.test(sha))
|
|
213
|
+
continue;
|
|
214
|
+
const subject = tab === -1 ? '' : line.slice(tab + 1);
|
|
215
|
+
const match = ISSUE_TRAILER_RE.exec(subject);
|
|
216
|
+
out.push({ sha, subject, issue: match ? Number.parseInt(match[1], 10) : null });
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Group boundary commits (oldest first) by member.
|
|
222
|
+
*
|
|
223
|
+
* `from`/`to` describe the member's span for reporting; `commits` is what a
|
|
224
|
+
* revert actually walks. They differ when members interleave — reverting the
|
|
225
|
+
* literal `from^..to` range would then revert a neighbour's commits too, so
|
|
226
|
+
* eviction reverts the explicit commits instead, ordered by `positions`:
|
|
227
|
+
* for a branch `A1 B1 A2 B2`, evicting both members must revert
|
|
228
|
+
* `B2 A2 B1 A1`, not each member's commits as a block.
|
|
229
|
+
*/
|
|
230
|
+
function memberRanges(boundary) {
|
|
231
|
+
const byIssue = new Map();
|
|
232
|
+
boundary.forEach((commit, position) => {
|
|
233
|
+
if (commit.issue === null)
|
|
234
|
+
return;
|
|
235
|
+
const group = byIssue.get(commit.issue) ?? { commits: [], positions: [] };
|
|
236
|
+
group.commits.push(commit.sha);
|
|
237
|
+
group.positions.push(position);
|
|
238
|
+
byIssue.set(commit.issue, group);
|
|
239
|
+
});
|
|
240
|
+
return [...byIssue.entries()].map(([issue, group]) => ({
|
|
241
|
+
issue,
|
|
242
|
+
from: group.commits[0],
|
|
243
|
+
to: group.commits[group.commits.length - 1],
|
|
244
|
+
commits: group.commits,
|
|
245
|
+
positions: group.positions,
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The member owning `sha`, or null when the commit carries no `(#N)` trailer.
|
|
250
|
+
*
|
|
251
|
+
* Either side may be abbreviated (bisect reports a short sha; boundary commits
|
|
252
|
+
* are full), so matching is prefix-based — but an AMBIGUOUS prefix returns null
|
|
253
|
+
* rather than the first hit. Attribution feeds eviction, and eviction reverts
|
|
254
|
+
* work: a wrong answer here destroys an innocent member's commits, so "cannot
|
|
255
|
+
* tell" must never collapse into "probably this one".
|
|
256
|
+
*/
|
|
257
|
+
function memberOfCommit(boundary, sha) {
|
|
258
|
+
const needle = sha.toLowerCase();
|
|
259
|
+
const matches = boundary.filter((c) => {
|
|
260
|
+
const candidate = c.sha.toLowerCase();
|
|
261
|
+
return candidate.startsWith(needle) || needle.startsWith(candidate);
|
|
262
|
+
});
|
|
263
|
+
const issues = new Set(matches.map((m) => m.issue));
|
|
264
|
+
if (matches.length === 0 || issues.size > 1)
|
|
265
|
+
return null;
|
|
266
|
+
return matches[0].issue;
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=attribution.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attribution.js","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"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"}
|
package/dist/bisect.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failure attribution, stage 2 (#472 AC1): when overlap cannot say which
|
|
3
|
+
* member broke a test, `git bisect` decides — a pure script over the batch
|
|
4
|
+
* branch's issue-boundary commits, running ONLY the failing tests. No LLM is
|
|
5
|
+
* involved, and no attribution is ever guessed: a first-bad commit that is not
|
|
6
|
+
* a member's commit reports `unattributable` rather than blaming a neighbour.
|
|
7
|
+
*
|
|
8
|
+
* Every subprocess goes through the injectable `ExecFn` (project.ts pattern),
|
|
9
|
+
* and the bisect state is ALWAYS reset — a repo left mid-bisect poisons every
|
|
10
|
+
* later git operation in that checkout.
|
|
11
|
+
*/
|
|
12
|
+
import { type BoundaryCommit } from './attribution';
|
|
13
|
+
import type { ExecFn } from './project';
|
|
14
|
+
/** What a bisect run concluded. */
|
|
15
|
+
export type BisectOutcome =
|
|
16
|
+
/** The first bad commit is a member's — that member owns the failure. */
|
|
17
|
+
{
|
|
18
|
+
kind: 'first-bad';
|
|
19
|
+
sha: string;
|
|
20
|
+
issue: number;
|
|
21
|
+
}
|
|
22
|
+
/** The failing tests pass at `bad` — nothing to attribute (someone already fixed it). */
|
|
23
|
+
| {
|
|
24
|
+
kind: 'green';
|
|
25
|
+
}
|
|
26
|
+
/** A first bad commit exists but belongs to no member (e.g. a batch-level fix commit). */
|
|
27
|
+
| {
|
|
28
|
+
kind: 'unattributable';
|
|
29
|
+
sha: string;
|
|
30
|
+
detail: string;
|
|
31
|
+
}
|
|
32
|
+
/** The bisect could not run or its output could not be read — never a guess. */
|
|
33
|
+
| {
|
|
34
|
+
kind: 'error';
|
|
35
|
+
detail: string;
|
|
36
|
+
};
|
|
37
|
+
export interface BisectOptions {
|
|
38
|
+
/** The batch checkout to bisect in. */
|
|
39
|
+
repoDir: string;
|
|
40
|
+
/** Last known-good commit (the batch base). */
|
|
41
|
+
good: string;
|
|
42
|
+
/** Known-bad commit (the batch branch head). */
|
|
43
|
+
bad: string;
|
|
44
|
+
/**
|
|
45
|
+
* The command `git bisect run` executes at each step. MUST be scoped to the
|
|
46
|
+
* failing tests only — a full suite makes the bisect cost O(members × suite).
|
|
47
|
+
* It is executed as a binary, so it must come from operator configuration —
|
|
48
|
+
* never from issue-comment or suite-output text.
|
|
49
|
+
*/
|
|
50
|
+
testCommand: readonly string[];
|
|
51
|
+
/** Boundary commits of the batch branch, for mapping the first-bad sha to a member. */
|
|
52
|
+
boundary: readonly BoundaryCommit[];
|
|
53
|
+
/**
|
|
54
|
+
* Called when cleanup (bisect reset / checkout restore) fails. This module
|
|
55
|
+
* has no journal of its own; the caller routes these into one.
|
|
56
|
+
*/
|
|
57
|
+
onWarn?: (detail: string) => void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Bisect the batch branch for the commit that broke `testCommand`.
|
|
61
|
+
*
|
|
62
|
+
* The known-bad end is verified first: if the failing tests PASS at `bad`
|
|
63
|
+
* there is nothing to attribute (`green`), and starting a bisect from a good
|
|
64
|
+
* "bad" end would otherwise walk the whole range and report nonsense.
|
|
65
|
+
*/
|
|
66
|
+
export declare function runAttributionBisect(exec: ExecFn, opts: BisectOptions): BisectOutcome;
|
|
67
|
+
//# sourceMappingURL=bisect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bisect.d.ts","sourceRoot":"","sources":["../src/bisect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,cAAc,EAAuC,MAAM,eAAe,CAAC;AACzF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,mCAAmC;AACnC,MAAM,MAAM,aAAa;AACvB,yEAAyE;AACvE;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACnD,yFAAyF;GACvF;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE;AACnB,0FAA0F;GACxF;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACzD,gFAAgF;GAC9E;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,MAAM,WAAW,aAAa;IAC5B,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,uFAAuF;IACvF,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IACpC;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAKD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,aAAa,CAuGrF"}
|