edsger 0.39.2 → 0.40.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/dist/phases/pr-execution/__tests__/file-assigner.test.d.ts +1 -0
- package/dist/phases/pr-execution/__tests__/file-assigner.test.js +294 -0
- package/dist/phases/pr-execution/context.d.ts +5 -1
- package/dist/phases/pr-execution/context.js +81 -9
- package/dist/phases/pr-execution/file-assigner.d.ts +45 -0
- package/dist/phases/pr-execution/file-assigner.js +244 -0
- package/dist/phases/pr-execution/index.js +20 -4
- package/dist/phases/pr-execution/prompts.d.ts +2 -1
- package/dist/phases/pr-execution/prompts.js +1 -1
- package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.d.ts +1 -0
- package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.js +331 -0
- package/dist/phases/pr-splitting/import-dep-validator.d.ts +67 -0
- package/dist/phases/pr-splitting/import-dep-validator.js +247 -0
- package/dist/phases/pr-splitting/index.js +33 -23
- package/dist/skills/phase/pr-execution/SKILL.md +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { applyAssignments, findUnassignedFiles, removeDeletedFilesFromPRs, } from '../file-assigner.js';
|
|
4
|
+
function makePR(overrides) {
|
|
5
|
+
return {
|
|
6
|
+
feature_id: 'feat-1',
|
|
7
|
+
name: `PR ${overrides.sequence}`,
|
|
8
|
+
description: null,
|
|
9
|
+
branch_name: null,
|
|
10
|
+
base_pr_id: null,
|
|
11
|
+
pull_request_url: null,
|
|
12
|
+
pull_request_number: null,
|
|
13
|
+
compare_url: null,
|
|
14
|
+
status: 'pending',
|
|
15
|
+
last_synced_commit: null,
|
|
16
|
+
files: null,
|
|
17
|
+
created_by: 'user',
|
|
18
|
+
created_at: '2026-01-01',
|
|
19
|
+
updated_at: '2026-01-01',
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function cf(path, change_type = 'modified') {
|
|
24
|
+
return { path, change_type };
|
|
25
|
+
}
|
|
26
|
+
// ============================================================
|
|
27
|
+
// findUnassignedFiles
|
|
28
|
+
// ============================================================
|
|
29
|
+
void describe('findUnassignedFiles', () => {
|
|
30
|
+
void it('returns all files when no PR has files', () => {
|
|
31
|
+
const prs = [makePR({ id: '1', sequence: 1, files: null })];
|
|
32
|
+
const result = findUnassignedFiles([cf('a.ts'), cf('b.ts')], prs);
|
|
33
|
+
assert.deepStrictEqual(result, [cf('a.ts'), cf('b.ts')]);
|
|
34
|
+
});
|
|
35
|
+
void it('returns empty when all files are assigned', () => {
|
|
36
|
+
const prs = [
|
|
37
|
+
makePR({
|
|
38
|
+
id: '1',
|
|
39
|
+
sequence: 1,
|
|
40
|
+
files: [
|
|
41
|
+
{ path: 'a.ts', change_type: 'modified' },
|
|
42
|
+
{ path: 'b.ts', change_type: 'added' },
|
|
43
|
+
],
|
|
44
|
+
}),
|
|
45
|
+
];
|
|
46
|
+
const result = findUnassignedFiles([cf('a.ts'), cf('b.ts', 'added')], prs);
|
|
47
|
+
assert.deepStrictEqual(result, []);
|
|
48
|
+
});
|
|
49
|
+
void it('returns only unassigned files', () => {
|
|
50
|
+
const prs = [
|
|
51
|
+
makePR({
|
|
52
|
+
id: '1',
|
|
53
|
+
sequence: 1,
|
|
54
|
+
files: [{ path: 'a.ts', change_type: 'modified' }],
|
|
55
|
+
}),
|
|
56
|
+
makePR({
|
|
57
|
+
id: '2',
|
|
58
|
+
sequence: 2,
|
|
59
|
+
files: [{ path: 'b.ts', change_type: 'added' }],
|
|
60
|
+
}),
|
|
61
|
+
];
|
|
62
|
+
const result = findUnassignedFiles([cf('a.ts'), cf('b.ts'), cf('c.ts'), cf('d.ts', 'added')], prs);
|
|
63
|
+
assert.deepStrictEqual(result, [cf('c.ts'), cf('d.ts', 'added')]);
|
|
64
|
+
});
|
|
65
|
+
void it('handles PRs with empty files array', () => {
|
|
66
|
+
const prs = [
|
|
67
|
+
makePR({ id: '1', sequence: 1, files: [] }),
|
|
68
|
+
makePR({
|
|
69
|
+
id: '2',
|
|
70
|
+
sequence: 2,
|
|
71
|
+
files: [{ path: 'a.ts', change_type: 'modified' }],
|
|
72
|
+
}),
|
|
73
|
+
];
|
|
74
|
+
const result = findUnassignedFiles([cf('a.ts'), cf('b.ts')], prs);
|
|
75
|
+
assert.deepStrictEqual(result, [cf('b.ts')]);
|
|
76
|
+
});
|
|
77
|
+
void it('returns empty for empty changed files', () => {
|
|
78
|
+
const prs = [
|
|
79
|
+
makePR({
|
|
80
|
+
id: '1',
|
|
81
|
+
sequence: 1,
|
|
82
|
+
files: [{ path: 'a.ts', change_type: 'modified' }],
|
|
83
|
+
}),
|
|
84
|
+
];
|
|
85
|
+
const result = findUnassignedFiles([], prs);
|
|
86
|
+
assert.deepStrictEqual(result, []);
|
|
87
|
+
});
|
|
88
|
+
void it('handles files spread across multiple PRs', () => {
|
|
89
|
+
const prs = [
|
|
90
|
+
makePR({
|
|
91
|
+
id: '1',
|
|
92
|
+
sequence: 1,
|
|
93
|
+
files: [
|
|
94
|
+
{ path: 'src/auth/login.ts', change_type: 'modified' },
|
|
95
|
+
{ path: 'src/auth/session.ts', change_type: 'modified' },
|
|
96
|
+
],
|
|
97
|
+
}),
|
|
98
|
+
makePR({
|
|
99
|
+
id: '2',
|
|
100
|
+
sequence: 2,
|
|
101
|
+
files: [
|
|
102
|
+
{ path: 'src/api/users.ts', change_type: 'added' },
|
|
103
|
+
{ path: 'src/api/roles.ts', change_type: 'added' },
|
|
104
|
+
],
|
|
105
|
+
}),
|
|
106
|
+
];
|
|
107
|
+
const changedFiles = [
|
|
108
|
+
cf('src/auth/login.ts'),
|
|
109
|
+
cf('src/auth/session.ts'),
|
|
110
|
+
cf('src/api/users.ts'),
|
|
111
|
+
cf('src/api/roles.ts'),
|
|
112
|
+
cf('src/auth/oauth.ts', 'added'),
|
|
113
|
+
cf('README.md'),
|
|
114
|
+
];
|
|
115
|
+
const result = findUnassignedFiles(changedFiles, prs);
|
|
116
|
+
assert.deepStrictEqual(result, [
|
|
117
|
+
cf('src/auth/oauth.ts', 'added'),
|
|
118
|
+
cf('README.md'),
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
// ============================================================
|
|
123
|
+
// applyAssignments
|
|
124
|
+
// ============================================================
|
|
125
|
+
void describe('applyAssignments', () => {
|
|
126
|
+
void it('merges new files into existing PR files', async () => {
|
|
127
|
+
const prs = [
|
|
128
|
+
makePR({
|
|
129
|
+
id: 'pr-1',
|
|
130
|
+
sequence: 1,
|
|
131
|
+
files: [{ path: 'a.ts', change_type: 'modified' }],
|
|
132
|
+
}),
|
|
133
|
+
];
|
|
134
|
+
const unassigned = [cf('b.ts', 'added')];
|
|
135
|
+
const updates = [];
|
|
136
|
+
const mockUpdater = async (prId, u) => {
|
|
137
|
+
updates.push({ prId, files: u.files });
|
|
138
|
+
};
|
|
139
|
+
const count = await applyAssignments([{ file: 'b.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
|
|
140
|
+
assert.strictEqual(count, 1);
|
|
141
|
+
assert.strictEqual(updates.length, 1);
|
|
142
|
+
assert.strictEqual(updates[0].prId, 'pr-1');
|
|
143
|
+
assert.deepStrictEqual(updates[0].files, [
|
|
144
|
+
{ path: 'a.ts', change_type: 'modified' },
|
|
145
|
+
{ path: 'b.ts', change_type: 'added' },
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
void it('deduplicates files already in PR', async () => {
|
|
149
|
+
const prs = [
|
|
150
|
+
makePR({
|
|
151
|
+
id: 'pr-1',
|
|
152
|
+
sequence: 1,
|
|
153
|
+
files: [{ path: 'a.ts', change_type: 'modified' }],
|
|
154
|
+
}),
|
|
155
|
+
];
|
|
156
|
+
const unassigned = [cf('a.ts')];
|
|
157
|
+
const updates = [];
|
|
158
|
+
const mockUpdater = async (_prId, u) => {
|
|
159
|
+
updates.push({ files: u.files });
|
|
160
|
+
};
|
|
161
|
+
await applyAssignments([{ file: 'a.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
|
|
162
|
+
// File list should not have duplicates
|
|
163
|
+
assert.deepStrictEqual(updates[0].files, [
|
|
164
|
+
{ path: 'a.ts', change_type: 'modified' },
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
void it('skips unknown PR ids', async () => {
|
|
168
|
+
const prs = [makePR({ id: 'pr-1', sequence: 1, files: [] })];
|
|
169
|
+
const unassigned = [cf('x.ts', 'added')];
|
|
170
|
+
let called = false;
|
|
171
|
+
const mockUpdater = async () => {
|
|
172
|
+
called = true;
|
|
173
|
+
};
|
|
174
|
+
const count = await applyAssignments([{ file: 'x.ts', pr_id: 'unknown-id' }], unassigned, prs, mockUpdater);
|
|
175
|
+
assert.strictEqual(count, 0);
|
|
176
|
+
assert.strictEqual(called, false);
|
|
177
|
+
});
|
|
178
|
+
void it('uses change_type from git, not a hardcoded default', async () => {
|
|
179
|
+
const prs = [makePR({ id: 'pr-1', sequence: 1, files: [] })];
|
|
180
|
+
const unassigned = [cf('new.ts', 'added')];
|
|
181
|
+
const updates = [];
|
|
182
|
+
const mockUpdater = async (_prId, u) => {
|
|
183
|
+
updates.push({ files: u.files });
|
|
184
|
+
};
|
|
185
|
+
await applyAssignments([{ file: 'new.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
|
|
186
|
+
assert.strictEqual(updates[0].files[0].change_type, 'added');
|
|
187
|
+
});
|
|
188
|
+
void it('distributes files to multiple PRs', async () => {
|
|
189
|
+
const prs = [
|
|
190
|
+
makePR({ id: 'pr-1', sequence: 1, files: [] }),
|
|
191
|
+
makePR({ id: 'pr-2', sequence: 2, files: [] }),
|
|
192
|
+
];
|
|
193
|
+
const unassigned = [
|
|
194
|
+
cf('a.ts', 'added'),
|
|
195
|
+
cf('b.ts', 'modified'),
|
|
196
|
+
];
|
|
197
|
+
const updates = [];
|
|
198
|
+
const mockUpdater = async (prId) => {
|
|
199
|
+
updates.push({ prId });
|
|
200
|
+
};
|
|
201
|
+
const count = await applyAssignments([
|
|
202
|
+
{ file: 'a.ts', pr_id: 'pr-1' },
|
|
203
|
+
{ file: 'b.ts', pr_id: 'pr-2' },
|
|
204
|
+
], unassigned, prs, mockUpdater);
|
|
205
|
+
assert.strictEqual(count, 2);
|
|
206
|
+
assert.deepStrictEqual(updates.map((u) => u.prId), ['pr-1', 'pr-2']);
|
|
207
|
+
});
|
|
208
|
+
void it('handles updater failure gracefully', async () => {
|
|
209
|
+
const prs = [makePR({ id: 'pr-1', sequence: 1, files: [] })];
|
|
210
|
+
const unassigned = [cf('a.ts')];
|
|
211
|
+
const mockUpdater = async () => {
|
|
212
|
+
throw new Error('DB error');
|
|
213
|
+
};
|
|
214
|
+
const count = await applyAssignments([{ file: 'a.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
|
|
215
|
+
assert.strictEqual(count, 0);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
// ============================================================
|
|
219
|
+
// removeDeletedFilesFromPRs
|
|
220
|
+
// ============================================================
|
|
221
|
+
void describe('removeDeletedFilesFromPRs', () => {
|
|
222
|
+
void it('removes deleted files from PR file lists', async () => {
|
|
223
|
+
const prs = [
|
|
224
|
+
makePR({
|
|
225
|
+
id: 'pr-1',
|
|
226
|
+
sequence: 1,
|
|
227
|
+
files: [
|
|
228
|
+
{ path: 'keep.ts', change_type: 'modified' },
|
|
229
|
+
{ path: 'gone.ts', change_type: 'added' },
|
|
230
|
+
],
|
|
231
|
+
}),
|
|
232
|
+
];
|
|
233
|
+
const changedFiles = [
|
|
234
|
+
cf('keep.ts'),
|
|
235
|
+
cf('gone.ts', 'deleted'),
|
|
236
|
+
];
|
|
237
|
+
const updates = [];
|
|
238
|
+
const mockUpdater = async (prId, u) => {
|
|
239
|
+
updates.push({ prId, files: u.files });
|
|
240
|
+
};
|
|
241
|
+
const count = await removeDeletedFilesFromPRs(changedFiles, prs, mockUpdater);
|
|
242
|
+
assert.strictEqual(count, 1);
|
|
243
|
+
assert.deepStrictEqual(updates[0].files, [
|
|
244
|
+
{ path: 'keep.ts', change_type: 'modified' },
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
void it('returns 0 when no files are deleted', async () => {
|
|
248
|
+
const prs = [
|
|
249
|
+
makePR({
|
|
250
|
+
id: 'pr-1',
|
|
251
|
+
sequence: 1,
|
|
252
|
+
files: [{ path: 'a.ts', change_type: 'modified' }],
|
|
253
|
+
}),
|
|
254
|
+
];
|
|
255
|
+
let called = false;
|
|
256
|
+
const mockUpdater = async () => {
|
|
257
|
+
called = true;
|
|
258
|
+
};
|
|
259
|
+
const count = await removeDeletedFilesFromPRs([cf('a.ts')], prs, mockUpdater);
|
|
260
|
+
assert.strictEqual(count, 0);
|
|
261
|
+
assert.strictEqual(called, false);
|
|
262
|
+
});
|
|
263
|
+
void it('skips PRs with no matching deleted files', async () => {
|
|
264
|
+
const prs = [
|
|
265
|
+
makePR({
|
|
266
|
+
id: 'pr-1',
|
|
267
|
+
sequence: 1,
|
|
268
|
+
files: [{ path: 'a.ts', change_type: 'modified' }],
|
|
269
|
+
}),
|
|
270
|
+
makePR({
|
|
271
|
+
id: 'pr-2',
|
|
272
|
+
sequence: 2,
|
|
273
|
+
files: [{ path: 'b.ts', change_type: 'added' }],
|
|
274
|
+
}),
|
|
275
|
+
];
|
|
276
|
+
const updates = [];
|
|
277
|
+
const mockUpdater = async (prId) => {
|
|
278
|
+
updates.push(prId);
|
|
279
|
+
};
|
|
280
|
+
await removeDeletedFilesFromPRs([cf('b.ts', 'deleted')], prs, mockUpdater);
|
|
281
|
+
// Only pr-2 should be updated
|
|
282
|
+
assert.deepStrictEqual(updates, ['pr-2']);
|
|
283
|
+
});
|
|
284
|
+
void it('handles PRs with null files', async () => {
|
|
285
|
+
const prs = [makePR({ id: 'pr-1', sequence: 1, files: null })];
|
|
286
|
+
let called = false;
|
|
287
|
+
const mockUpdater = async () => {
|
|
288
|
+
called = true;
|
|
289
|
+
};
|
|
290
|
+
const count = await removeDeletedFilesFromPRs([cf('a.ts', 'deleted')], prs, mockUpdater);
|
|
291
|
+
assert.strictEqual(count, 0);
|
|
292
|
+
assert.strictEqual(called, false);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -8,6 +8,10 @@ export interface GitHubConfigInfo {
|
|
|
8
8
|
repo?: string;
|
|
9
9
|
message?: string;
|
|
10
10
|
}
|
|
11
|
+
export interface ChangedFileInfo {
|
|
12
|
+
path: string;
|
|
13
|
+
change_type: 'added' | 'modified' | 'deleted' | 'renamed';
|
|
14
|
+
}
|
|
11
15
|
export interface PRExecutionContext {
|
|
12
16
|
feature: FeatureInfo;
|
|
13
17
|
pullRequests: PullRequest[];
|
|
@@ -18,7 +22,7 @@ export interface PRExecutionContext {
|
|
|
18
22
|
isIncrementalSync: boolean;
|
|
19
23
|
lastSyncedCommit: string | null;
|
|
20
24
|
diffStat: string;
|
|
21
|
-
changedFiles:
|
|
25
|
+
changedFiles: ChangedFileInfo[];
|
|
22
26
|
}
|
|
23
27
|
/**
|
|
24
28
|
* Fetch context for PR execution phase
|
|
@@ -32,22 +32,61 @@ function getDiffStat(baseRef, headRef) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* Map git diff --name-status letter to a readable change type
|
|
36
|
+
*/
|
|
37
|
+
function parseGitStatus(status) {
|
|
38
|
+
switch (status[0]) {
|
|
39
|
+
case 'A':
|
|
40
|
+
return 'added';
|
|
41
|
+
case 'D':
|
|
42
|
+
return 'deleted';
|
|
43
|
+
case 'R':
|
|
44
|
+
return 'renamed';
|
|
45
|
+
default:
|
|
46
|
+
return 'modified';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get list of changed files with their change types between two refs
|
|
36
51
|
*/
|
|
37
52
|
function getChangedFiles(baseRef, headRef) {
|
|
38
53
|
try {
|
|
39
|
-
const output = execSync(`git diff --name-
|
|
54
|
+
const output = execSync(`git diff --name-status ${baseRef}...${headRef}`, {
|
|
40
55
|
encoding: 'utf-8',
|
|
41
56
|
}).trim();
|
|
42
57
|
if (!output) {
|
|
43
58
|
return [];
|
|
44
59
|
}
|
|
45
|
-
return output
|
|
60
|
+
return output
|
|
61
|
+
.split('\n')
|
|
62
|
+
.filter((line) => line.length > 0)
|
|
63
|
+
.map((line) => {
|
|
64
|
+
const [status, ...pathParts] = line.split('\t');
|
|
65
|
+
// For renames (R100\told\tnew), use the new path
|
|
66
|
+
const path = pathParts.length > 1 ? pathParts[1] : pathParts[0];
|
|
67
|
+
return { path, change_type: parseGitStatus(status) };
|
|
68
|
+
});
|
|
46
69
|
}
|
|
47
70
|
catch {
|
|
48
71
|
return [];
|
|
49
72
|
}
|
|
50
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if a commit exists in the ancestry of a given ref.
|
|
76
|
+
* Returns false if the commit was orphaned (e.g. after a rebase).
|
|
77
|
+
*/
|
|
78
|
+
function isAncestor(commitSha, ref) {
|
|
79
|
+
try {
|
|
80
|
+
execSync(`git merge-base --is-ancestor ${commitSha} ${ref}`, {
|
|
81
|
+
encoding: 'utf-8',
|
|
82
|
+
stdio: 'pipe',
|
|
83
|
+
});
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
51
90
|
/**
|
|
52
91
|
* Determine the common last_synced_commit from existing PRs
|
|
53
92
|
* Returns null if any PR hasn't been synced yet (first run)
|
|
@@ -82,6 +121,27 @@ export async function fetchPRExecutionContext(featureId, verbose) {
|
|
|
82
121
|
getPullRequests({ featureId, verbose }),
|
|
83
122
|
getGitHubConfig(featureId, verbose),
|
|
84
123
|
]);
|
|
124
|
+
// Fetch latest remote refs and fast-forward local main
|
|
125
|
+
try {
|
|
126
|
+
const credArgs = buildCredentialArgs(githubConfig.token);
|
|
127
|
+
execFileSync('git', [...credArgs, 'fetch', 'origin'], {
|
|
128
|
+
encoding: 'utf-8',
|
|
129
|
+
stdio: 'pipe',
|
|
130
|
+
});
|
|
131
|
+
execSync('git checkout main', { encoding: 'utf-8', stdio: 'pipe' });
|
|
132
|
+
execSync('git merge --ff-only origin/main', {
|
|
133
|
+
encoding: 'utf-8',
|
|
134
|
+
stdio: 'pipe',
|
|
135
|
+
});
|
|
136
|
+
if (verbose) {
|
|
137
|
+
logInfo('✅ Local main synced with origin/main');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (verbose) {
|
|
142
|
+
logInfo(`⚠️ Could not sync main with origin: ${error instanceof Error ? error.message : String(error)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
85
145
|
// Verify dev branch exists
|
|
86
146
|
const localExists = branchExists(devBranchName);
|
|
87
147
|
const remoteExists = !localExists && remoteBranchExists(devBranchName, githubConfig.token);
|
|
@@ -95,10 +155,12 @@ export async function fetchPRExecutionContext(featureId, verbose) {
|
|
|
95
155
|
logInfo(`Fetching remote branch ${devBranchName}...`);
|
|
96
156
|
}
|
|
97
157
|
const credArgs = buildCredentialArgs(githubConfig.token);
|
|
98
|
-
execFileSync('git', [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
158
|
+
execFileSync('git', [
|
|
159
|
+
...credArgs,
|
|
160
|
+
'fetch',
|
|
161
|
+
'origin',
|
|
162
|
+
`${devBranchName}:refs/remotes/origin/${devBranchName}`,
|
|
163
|
+
], { encoding: 'utf-8', stdio: 'pipe' });
|
|
102
164
|
}
|
|
103
165
|
if (pullRequests.length === 0) {
|
|
104
166
|
throw new Error('No PR plan found. Run the pr-splitting phase first to create a PR plan.');
|
|
@@ -126,8 +188,18 @@ export async function fetchPRExecutionContext(featureId, verbose) {
|
|
|
126
188
|
// Determine sync mode
|
|
127
189
|
const devRef = localExists ? devBranchName : `origin/${devBranchName}`;
|
|
128
190
|
const devBranchHeadSha = getBranchHeadSha(devRef);
|
|
129
|
-
|
|
130
|
-
|
|
191
|
+
let lastSyncedCommit = getLastSyncedCommit(pullRequests);
|
|
192
|
+
let isIncrementalSync = lastSyncedCommit !== null;
|
|
193
|
+
// If the branch was rebased, lastSyncedCommit is no longer in the history.
|
|
194
|
+
// Treat this as a full re-execution so downstream logic uses the correct
|
|
195
|
+
// prompt and skips incremental file-assignment against an invalid base.
|
|
196
|
+
if (isIncrementalSync && !isAncestor(lastSyncedCommit, devRef)) {
|
|
197
|
+
if (verbose) {
|
|
198
|
+
logInfo(`⚠️ Last synced commit ${lastSyncedCommit} is no longer in branch history (likely rebased). Treating as full re-execution.`);
|
|
199
|
+
}
|
|
200
|
+
lastSyncedCommit = null;
|
|
201
|
+
isIncrementalSync = false;
|
|
202
|
+
}
|
|
131
203
|
// Get diff info: for incremental, diff from last sync; for first run, diff from main
|
|
132
204
|
const diffBase = isIncrementalSync ? lastSyncedCommit : 'main';
|
|
133
205
|
const diffStat = getDiffStat(diffBase, devRef);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File assignment for incremental sync.
|
|
3
|
+
*
|
|
4
|
+
* When new changes on the dev branch include files not covered by any
|
|
5
|
+
* existing PR's file list, this module asks an LLM to assign them to
|
|
6
|
+
* the most appropriate PR, then updates the PR records in the database.
|
|
7
|
+
*/
|
|
8
|
+
import { type PullRequest } from '../../services/pull-requests.js';
|
|
9
|
+
import type { ChangedFileInfo } from './context.js';
|
|
10
|
+
export interface FileAssignment {
|
|
11
|
+
file: string;
|
|
12
|
+
pr_id: string;
|
|
13
|
+
}
|
|
14
|
+
export interface FileAssignmentResult {
|
|
15
|
+
assignments: FileAssignment[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Find changed files that are not covered by any PR's file list.
|
|
19
|
+
*/
|
|
20
|
+
export declare function findUnassignedFiles(changedFiles: ChangedFileInfo[], pullRequests: PullRequest[]): ChangedFileInfo[];
|
|
21
|
+
/**
|
|
22
|
+
* Update PR records in the database with newly assigned files.
|
|
23
|
+
* Exported for testing via dependency injection of the updater function.
|
|
24
|
+
*/
|
|
25
|
+
export declare function applyAssignments(assignments: FileAssignment[], unassignedFiles: ChangedFileInfo[], pullRequests: PullRequest[], updater?: (prId: string, updates: {
|
|
26
|
+
files: {
|
|
27
|
+
path: string;
|
|
28
|
+
change_type: string;
|
|
29
|
+
}[];
|
|
30
|
+
}, verbose?: boolean) => Promise<unknown>, verbose?: boolean): Promise<number>;
|
|
31
|
+
/**
|
|
32
|
+
* Remove files that were deleted on the dev branch from PR file lists.
|
|
33
|
+
* Returns the number of PRs updated.
|
|
34
|
+
*/
|
|
35
|
+
export declare function removeDeletedFilesFromPRs(changedFiles: ChangedFileInfo[], pullRequests: PullRequest[], updater?: (prId: string, updates: {
|
|
36
|
+
files: {
|
|
37
|
+
path: string;
|
|
38
|
+
change_type: string;
|
|
39
|
+
}[];
|
|
40
|
+
}, verbose?: boolean) => Promise<unknown>, verbose?: boolean): Promise<number>;
|
|
41
|
+
/**
|
|
42
|
+
* Detect unassigned files and use LLM to assign them to existing PRs.
|
|
43
|
+
* Returns the number of files assigned, or 0 if none needed assignment.
|
|
44
|
+
*/
|
|
45
|
+
export declare function assignNewFilesToPRs(changedFiles: ChangedFileInfo[], pullRequests: PullRequest[], verbose?: boolean): Promise<number>;
|