diffsplain 0.5.0 → 0.6.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 +17 -12
- package/dist/assets/{angular-html-CoQle5-T.js → angular-html-vBvPnd6a.js} +1 -1
- package/dist/assets/{angular-ts-BB_A1Lpy.js → angular-ts-CDzF1Z6a.js} +1 -1
- package/dist/assets/{apl-B7_4_FNq.js → apl-Bm9bxvP7.js} +1 -1
- package/dist/assets/{astro-CXVo4VE4.js → astro-RNrNWCYO.js} +1 -1
- package/dist/assets/{blade-BVX41dq7.js → blade-On-NBYUv.js} +1 -1
- package/dist/assets/{c-Dhyi8Nd2.js → c-hRmhFgD0.js} +1 -1
- package/dist/assets/{cobol-CU5oLeZj.js → cobol-CIqc3k2t.js} +1 -1
- package/dist/assets/{coffee-D_FU6gC_.js → coffee-Cd5EyvvS.js} +1 -1
- package/dist/assets/{cpp-CxoYLSi3.js → cpp-zXFAAp_S.js} +1 -1
- package/dist/assets/{crystal-BdgqBN3z.js → crystal-DRYY4cSw.js} +1 -1
- package/dist/assets/{css-wLk1vVwJ.js → css-C4op_MsD.js} +1 -1
- package/dist/assets/{edge-DMylWoGl.js → edge-d3rJkErf.js} +1 -1
- package/dist/assets/{elixir-Djmgto_Z.js → elixir-DPqlJhYM.js} +1 -1
- package/dist/assets/{elm-D7B-eBLA.js → elm-DgU37I8a.js} +1 -1
- package/dist/assets/{erb-g1sso1GW.js → erb-CZVB_ntC.js} +1 -1
- package/dist/assets/{git-rebase-DEAhxutZ.js → git-rebase-hUkltHZG.js} +1 -1
- package/dist/assets/{glimmer-js-CHvGFGMp.js → glimmer-js-5XOT9GQ8.js} +1 -1
- package/dist/assets/{glimmer-ts-DvhL705p.js → glimmer-ts-DT7D0Kfq.js} +1 -1
- package/dist/assets/{glsl-C8mS_t8I.js → glsl-Br4_Eh5O.js} +1 -1
- package/dist/assets/{graphql-DW2pnvqi.js → graphql--BmZ_AYP.js} +1 -1
- package/dist/assets/{hack-DYc1d3he.js → hack-CMdqGDoA.js} +1 -1
- package/dist/assets/{haml-_5OmzH31.js → haml-DLEOHGmg.js} +1 -1
- package/dist/assets/{handlebars-rN-CaXUf.js → handlebars-BfUftoV0.js} +1 -1
- package/dist/assets/{html-derivative-BovOubry.js → html-derivative-D5cZE6C4.js} +1 -1
- package/dist/assets/{html-DQnaJUjl.js → html-uOgx2iYC.js} +1 -1
- package/dist/assets/{http-BKTgwOYc.js → http-cW9c66J_.js} +1 -1
- package/dist/assets/{hurl-ChS3_IgD.js → hurl-BztURO2n.js} +1 -1
- package/dist/assets/{index-BLtRBc2n.css → index-C2p_1iAk.css} +1 -1
- package/dist/assets/{index-CEglkSs-.js → index-DZFM4-3L.js} +11 -11
- package/dist/assets/{java-Dgnyk--z.js → java-Cy4mnKaj.js} +1 -1
- package/dist/assets/{javascript-BfnrwkRN.js → javascript-_Z02t_9Q.js} +1 -1
- package/dist/assets/{jinja-CD2AIvOi.js → jinja-BppBm5gt.js} +1 -1
- package/dist/assets/{jison-DmKNw3Pl.js → jison-Dzns436M.js} +1 -1
- package/dist/assets/{json-CKB5a8aj.js → json-BYkfLOry.js} +1 -1
- package/dist/assets/{jsx-jJXrDRBM.js → jsx-DtAm8NUb.js} +1 -1
- package/dist/assets/{julia-G8vVBW-t.js → julia-CLIMRuh0.js} +1 -1
- package/dist/assets/{just-DMXLrLZL.js → just-DMg47an6.js} +1 -1
- package/dist/assets/{latex-DS3xuxld.js → latex-D05ztCOx.js} +1 -1
- package/dist/assets/{liquid-COQnD_C4.js → liquid-DpImQR4t.js} +1 -1
- package/dist/assets/{lua-BwMKwosd.js → lua--ZHUYQUU.js} +1 -1
- package/dist/assets/{marko-cQCK_WIk.js → marko-Dh3Xi08i.js} +1 -1
- package/dist/assets/{mdc-DQUjY13N.js → mdc-Br6Pt78u.js} +1 -1
- package/dist/assets/{nginx-C4Kv3DJi.js → nginx-CwSU6U6d.js} +1 -1
- package/dist/assets/{nim-ChzLzuYD.js → nim-CBbTwyeq.js} +1 -1
- package/dist/assets/{perl-z5xUiWAY.js → perl-D3jQu3LN.js} +1 -1
- package/dist/assets/{php-BHqm-Xeh.js → php-BMeXBzlU.js} +1 -1
- package/dist/assets/{pug-C9C_Llft.js → pug-BGdY1g5E.js} +1 -1
- package/dist/assets/{qml-dtpuwXJZ.js → qml-DsT5biza.js} +1 -1
- package/dist/assets/{r-BicQMs_c.js → r-CbS9PfN5.js} +1 -1
- package/dist/assets/{razor-D5h00YTZ.js → razor-Bwm8roHi.js} +1 -1
- package/dist/assets/{regexp-CTa8S86v.js → regexp-Bc_9DyAu.js} +1 -1
- package/dist/assets/{rst-Bg-b-7HK.js → rst-Cj1Aii91.js} +1 -1
- package/dist/assets/{ruby-BiWYm056.js → ruby-BPMtFZnc.js} +1 -1
- package/dist/assets/{sas-bJTdfgUz.js → sas-CoOau1t2.js} +1 -1
- package/dist/assets/{scss-0qkNyHr1.js → scss-CvfaI808.js} +1 -1
- package/dist/assets/{shellscript-3sDkDE9z.js → shellscript-B0C-TdLE.js} +1 -1
- package/dist/assets/{shellsession-D4EBhEmz.js → shellsession-UhmLiqcA.js} +1 -1
- package/dist/assets/{soy-DG9hUwLL.js → soy-D419u0RL.js} +1 -1
- package/dist/assets/{sql-B0UoKHkx.js → sql-Do9k0rRn.js} +1 -1
- package/dist/assets/{stata-CsQbuXQp.js → stata-x47J6BC4.js} +1 -1
- package/dist/assets/{surrealql-D63VLxos.js → surrealql-BERQ6pHu.js} +1 -1
- package/dist/assets/{svelte-BApdnAma.js → svelte-dRWkyqKW.js} +1 -1
- package/dist/assets/{templ-B64pN6Lz.js → templ-CTq-KoWN.js} +1 -1
- package/dist/assets/{tex-DR2UVIPF.js → tex-CdRGjyYI.js} +1 -1
- package/dist/assets/{ts-tags-IwhomvQv.js → ts-tags-CgaMqyVb.js} +1 -1
- package/dist/assets/{tsx-DROdxqzS.js → tsx-cpBQKcPG.js} +1 -1
- package/dist/assets/{twig-BtbUqpR4.js → twig-vbs_PM4-.js} +1 -1
- package/dist/assets/{typescript-CHpSjYZL.js → typescript-Cc--nwIo.js} +1 -1
- package/dist/assets/{vue-DCvEu_ug.js → vue-CgHwdBmB.js} +1 -1
- package/dist/assets/{vue-html-BgL4Rf-L.js → vue-html-BHfoMw47.js} +1 -1
- package/dist/assets/{vue-vine-CRNdhPTn.js → vue-vine-_lVRp_NK.js} +1 -1
- package/dist/assets/{xml-CkfPxZuz.js → xml-G4NVmTde.js} +1 -1
- package/dist/assets/{xsl-CteaoFSV.js → xsl-s0so2M1i.js} +1 -1
- package/dist/assets/{yaml-CUr3TDRN.js → yaml-CpsmW05t.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +49 -23
- package/scripts/access-token.mjs +21 -0
- package/scripts/build-diff-data.mjs +260 -39
- package/scripts/cache.mjs +301 -0
- package/scripts/cli-args.mjs +206 -57
- package/scripts/coding-agents.mjs +279 -162
- package/scripts/dev.mjs +218 -0
- package/scripts/doctor.mjs +265 -46
- package/scripts/generate-summaries.mjs +760 -260
- package/scripts/mock-agent.mjs +165 -0
- package/scripts/present.mjs +394 -85
- package/scripts/presenter-runtime.mjs +148 -0
- package/scripts/serve-built.mjs +343 -71
- package/scripts/summary-path.mjs +24 -7
- package/scripts/support-record.mjs +242 -0
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
existsSync,
|
|
7
7
|
mkdtempSync,
|
|
8
8
|
readFileSync,
|
|
9
|
-
renameSync,
|
|
10
9
|
rmSync,
|
|
11
10
|
writeFileSync,
|
|
12
11
|
} from 'node:fs';
|
|
@@ -15,12 +14,25 @@ import { dirname, relative, resolve } from 'node:path';
|
|
|
15
14
|
import { fileURLToPath } from 'node:url';
|
|
16
15
|
import {
|
|
17
16
|
agentCommand,
|
|
17
|
+
assertReasoningSupported,
|
|
18
18
|
codingAgentBinary,
|
|
19
19
|
commandAvailable,
|
|
20
20
|
parseAgentResponse,
|
|
21
21
|
selectCodingAgent,
|
|
22
22
|
} from './coding-agents.mjs';
|
|
23
23
|
import { summaryPath } from './summary-path.mjs';
|
|
24
|
+
import {
|
|
25
|
+
acquireLease,
|
|
26
|
+
publishLeaseFile,
|
|
27
|
+
refreshLease,
|
|
28
|
+
releaseLease,
|
|
29
|
+
} from './cache.mjs';
|
|
30
|
+
import {
|
|
31
|
+
createSupportRecorder,
|
|
32
|
+
formatSupportRecord,
|
|
33
|
+
safeCommandVersion,
|
|
34
|
+
writeSupportRecord,
|
|
35
|
+
} from './support-record.mjs';
|
|
24
36
|
|
|
25
37
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
38
|
const callerDirectory = process.cwd();
|
|
@@ -43,8 +55,14 @@ const valueFlags = new Set([
|
|
|
43
55
|
'--batch-size',
|
|
44
56
|
'--jobs',
|
|
45
57
|
'--snapshot',
|
|
58
|
+
'--support-record-file',
|
|
59
|
+
]);
|
|
60
|
+
const booleanFlags = new Set([
|
|
61
|
+
'--checkout',
|
|
62
|
+
'--force',
|
|
63
|
+
'--support-record',
|
|
64
|
+
'--worktree',
|
|
46
65
|
]);
|
|
47
|
-
const booleanFlags = new Set(['--checkout', '--force', '--worktree']);
|
|
48
66
|
|
|
49
67
|
function fail(message) {
|
|
50
68
|
console.error(message);
|
|
@@ -86,12 +104,16 @@ Options:
|
|
|
86
104
|
--summaries FILE Agent note file
|
|
87
105
|
--output FILE Rebuilt Diffsplain JSON
|
|
88
106
|
--cache-dir PATH Bare cache for fetched Git objects
|
|
89
|
-
--agent NAME Use codex, claude, copilot,
|
|
107
|
+
--agent NAME Use codex, claude, copilot, or opencode
|
|
108
|
+
Cursor is disabled because it cannot meet the review boundary
|
|
90
109
|
--codex-bin FILE Codex CLI path (default: codex)
|
|
91
110
|
--model NAME Model passed to the coding agent
|
|
92
111
|
--reasoning LEVEL Agent reasoning effort when supported
|
|
93
112
|
--batch-size COUNT Maximum files per agent pass (default: 12)
|
|
94
113
|
--jobs COUNT Agent passes to run at once (default: 3)
|
|
114
|
+
--support-record Print a safe record if this run fails
|
|
115
|
+
--support-record-file FILE
|
|
116
|
+
Write a safe record if this run fails
|
|
95
117
|
--force Regenerate all notes instead of using cached notes`);
|
|
96
118
|
process.exit(0);
|
|
97
119
|
}
|
|
@@ -101,18 +123,22 @@ const outputPath = resolve(
|
|
|
101
123
|
callerDirectory,
|
|
102
124
|
option('--output') || resolve(root, '.cache/diff-data.json'),
|
|
103
125
|
);
|
|
126
|
+
const printSupportRecord = rawArgs.includes('--support-record');
|
|
127
|
+
const supportRecordFile = option('--support-record-file');
|
|
128
|
+
if (printSupportRecord && supportRecordFile) {
|
|
129
|
+
fail('Pass either --support-record or --support-record-file, not both');
|
|
130
|
+
}
|
|
131
|
+
const supportRecordPath = supportRecordFile
|
|
132
|
+
? resolve(callerDirectory, supportRecordFile)
|
|
133
|
+
: undefined;
|
|
104
134
|
const codexBin = option('--codex-bin') || process.env.CODEX_BIN;
|
|
135
|
+
const requestedAgent = option('--agent');
|
|
136
|
+
const supportRecorder =
|
|
137
|
+
printSupportRecord || supportRecordPath
|
|
138
|
+
? createSupportRecorder()
|
|
139
|
+
: undefined;
|
|
105
140
|
let selectedAgent;
|
|
106
|
-
|
|
107
|
-
selectedAgent = await selectCodingAgent(
|
|
108
|
-
option('--agent'),
|
|
109
|
-
(agent) =>
|
|
110
|
-
commandAvailable(codingAgentBinary(agent, { codexBin })),
|
|
111
|
-
);
|
|
112
|
-
} catch (error) {
|
|
113
|
-
fail(error.message);
|
|
114
|
-
}
|
|
115
|
-
const agentBinary = codingAgentBinary(selectedAgent, { codexBin });
|
|
141
|
+
let agentBinary;
|
|
116
142
|
const model = option('--model');
|
|
117
143
|
const reasoning = option('--reasoning');
|
|
118
144
|
const batchSizeValue = option('--batch-size') || '12';
|
|
@@ -126,11 +152,27 @@ const reasoningLevels = new Set([
|
|
|
126
152
|
if (reasoning && !reasoningLevels.has(reasoning)) {
|
|
127
153
|
fail('--reasoning must be minimal, low, medium, high, or xhigh');
|
|
128
154
|
}
|
|
155
|
+
if (requestedAgent) {
|
|
156
|
+
try {
|
|
157
|
+
await selectCodingAgent(requestedAgent, async () => true);
|
|
158
|
+
assertReasoningSupported(requestedAgent, reasoning);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
129
163
|
if (!/^[1-9]\d*$/.test(batchSizeValue) || Number(batchSizeValue) > 50) {
|
|
130
164
|
fail('--batch-size must be a number from 1 to 50');
|
|
131
165
|
}
|
|
132
166
|
const batchSize = Number(batchSizeValue);
|
|
133
167
|
const batchByteLimit = 180_000;
|
|
168
|
+
const softFileByteLimit = 180_000;
|
|
169
|
+
const hardInputByteLimit = 2_000_000;
|
|
170
|
+
const priorNoteByteLimit = 250_000;
|
|
171
|
+
const titleCodePointLimit = 160;
|
|
172
|
+
const proseCodePointLimit = 1_200;
|
|
173
|
+
const detailItemLimit = 4;
|
|
174
|
+
const riskItemLimit = 3;
|
|
175
|
+
const listItemCodePointLimit = 500;
|
|
134
176
|
const jobsValue = option('--jobs') || '3';
|
|
135
177
|
if (!/^[1-9]\d*$/.test(jobsValue) || Number(jobsValue) > 8) {
|
|
136
178
|
fail('--jobs must be a number from 1 to 8');
|
|
@@ -148,10 +190,37 @@ const snapshotPath = option('--snapshot');
|
|
|
148
190
|
const activeAgentProcesses = new Set();
|
|
149
191
|
let interrupted = false;
|
|
150
192
|
|
|
151
|
-
|
|
193
|
+
function recordSyncStage(name, action) {
|
|
194
|
+
const finishStage = supportRecorder?.startStage(name);
|
|
195
|
+
try {
|
|
196
|
+
const result = action();
|
|
197
|
+
finishStage?.();
|
|
198
|
+
return result;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
finishStage?.('failed');
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function recordAsyncStage(name, action) {
|
|
206
|
+
const finishStage = supportRecorder?.startStage(name);
|
|
207
|
+
try {
|
|
208
|
+
const result = await action();
|
|
209
|
+
finishStage?.();
|
|
210
|
+
return result;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
finishStage?.('failed');
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function interrupt() {
|
|
152
218
|
interrupted = true;
|
|
153
219
|
for (const child of activeAgentProcesses) child.kill('SIGTERM');
|
|
154
|
-
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
process.once('SIGINT', interrupt);
|
|
223
|
+
process.once('SIGTERM', interrupt);
|
|
155
224
|
|
|
156
225
|
if (range && (base || head)) {
|
|
157
226
|
fail('--range cannot be used with --base or --head');
|
|
@@ -192,8 +261,21 @@ const summariesPath = summaryPath({
|
|
|
192
261
|
head: selectedHead,
|
|
193
262
|
remote,
|
|
194
263
|
});
|
|
264
|
+
const ownershipPath = `${summariesPath}.lock`;
|
|
265
|
+
let ownership;
|
|
266
|
+
let ownershipHeartbeat;
|
|
267
|
+
function acquireOwnership() {
|
|
268
|
+
ownership = acquireLease(ownershipPath);
|
|
269
|
+
ownershipHeartbeat = setInterval(() => {
|
|
270
|
+
try { refreshLease(ownership); } catch { clearInterval(ownershipHeartbeat); }
|
|
271
|
+
}, 30_000);
|
|
272
|
+
ownershipHeartbeat.unref();
|
|
273
|
+
}
|
|
195
274
|
if (selectedBase) targetArgs.push('--base', selectedBase);
|
|
196
275
|
if (selectedHead) targetArgs.push('--head', selectedHead);
|
|
276
|
+
if (supportRecordPath) {
|
|
277
|
+
targetArgs.push('--exclude-output', supportRecordPath);
|
|
278
|
+
}
|
|
197
279
|
const cacheDirectory = option('--cache-dir');
|
|
198
280
|
if (cacheDirectory) {
|
|
199
281
|
targetArgs.push('--cache-dir', resolve(callerDirectory, cacheDirectory));
|
|
@@ -233,15 +315,17 @@ function pathInsideRepo(file) {
|
|
|
233
315
|
}
|
|
234
316
|
|
|
235
317
|
function cleanSnapshot(snapshot) {
|
|
318
|
+
const summaryFile = pathInsideRepo(summariesPath);
|
|
236
319
|
const excluded = new Set(
|
|
237
|
-
[
|
|
320
|
+
[summaryFile, summaryFile && `${summaryFile}.lock`, pathInsideRepo(outputPath)].filter(Boolean),
|
|
238
321
|
);
|
|
239
322
|
const files = snapshot.files
|
|
240
323
|
.filter((file) => !excluded.has(file.path))
|
|
241
324
|
.map((file) => {
|
|
242
325
|
const fullPatch = typeof file.patch === 'string' ? file.patch : '';
|
|
243
|
-
const useSnippet =
|
|
244
|
-
|
|
326
|
+
const useSnippet =
|
|
327
|
+
Buffer.byteLength(fullPatch) > softFileByteLimit;
|
|
328
|
+
const summaryFile = {
|
|
245
329
|
path: file.path,
|
|
246
330
|
...(file.oldPath ? { oldPath: file.oldPath } : {}),
|
|
247
331
|
status: file.status,
|
|
@@ -251,6 +335,16 @@ function cleanSnapshot(snapshot) {
|
|
|
251
335
|
patch: useSnippet ? file.snippet : fullPatch,
|
|
252
336
|
patchIsExcerpt: useSnippet,
|
|
253
337
|
};
|
|
338
|
+
const inputBytes = Buffer.byteLength(JSON.stringify(summaryFile));
|
|
339
|
+
return {
|
|
340
|
+
...summaryFile,
|
|
341
|
+
...(inputBytes > hardInputByteLimit
|
|
342
|
+
? {
|
|
343
|
+
summaryFailure:
|
|
344
|
+
`The file input is ${inputBytes} bytes after using its patch excerpt; the hard limit is ${hardInputByteLimit} bytes.`,
|
|
345
|
+
}
|
|
346
|
+
: {}),
|
|
347
|
+
};
|
|
254
348
|
});
|
|
255
349
|
|
|
256
350
|
return {
|
|
@@ -273,11 +367,35 @@ function cleanSnapshot(snapshot) {
|
|
|
273
367
|
};
|
|
274
368
|
}
|
|
275
369
|
|
|
370
|
+
// fallow-ignore-next-line complexity
|
|
371
|
+
function compactExistingFileNotes(snapshot, existingFiles, selected, baseBytes) {
|
|
372
|
+
const existingFileNotes = {};
|
|
373
|
+
const propertyBytes = Buffer.byteLength(',"existingFileNotes":');
|
|
374
|
+
const maximumBytes = Math.min(
|
|
375
|
+
priorNoteByteLimit,
|
|
376
|
+
hardInputByteLimit - baseBytes - propertyBytes,
|
|
377
|
+
);
|
|
378
|
+
let contextBytes = Buffer.byteLength('{}');
|
|
379
|
+
let hasEntries = false;
|
|
380
|
+
for (const file of snapshot.files) {
|
|
381
|
+
const path = file.path;
|
|
382
|
+
const note = existingFiles[path];
|
|
383
|
+
if (selected.has(path) || !note) continue;
|
|
384
|
+
const compactNote = { title: note.title, what: note.what, risks: note.risks };
|
|
385
|
+
const entryBytes = Buffer.byteLength(
|
|
386
|
+
`${hasEntries ? ',' : ''}${JSON.stringify(path)}:${JSON.stringify(compactNote)}`,
|
|
387
|
+
);
|
|
388
|
+
if (contextBytes + entryBytes <= maximumBytes) {
|
|
389
|
+
existingFileNotes[path] = compactNote;
|
|
390
|
+
contextBytes += entryBytes;
|
|
391
|
+
hasEntries = true;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return existingFileNotes;
|
|
395
|
+
}
|
|
396
|
+
|
|
276
397
|
function batchInput(snapshot, rawSnapshot, paths, existingFiles) {
|
|
277
398
|
const selected = new Set(paths);
|
|
278
|
-
const existingFileNotes = Object.fromEntries(
|
|
279
|
-
Object.entries(existingFiles).filter(([path]) => !selected.has(path)),
|
|
280
|
-
);
|
|
281
399
|
const result = {
|
|
282
400
|
repo: snapshot.repo,
|
|
283
401
|
change: snapshot.change,
|
|
@@ -291,37 +409,59 @@ function batchInput(snapshot, rawSnapshot, paths, existingFiles) {
|
|
|
291
409
|
})),
|
|
292
410
|
files: snapshot.files
|
|
293
411
|
.filter((file) => selected.has(file.path))
|
|
294
|
-
.map((
|
|
295
|
-
...(Object.keys(existingFileNotes).length ? { existingFileNotes } : {}),
|
|
412
|
+
.map(({ summaryFailure: _summaryFailure, ...file }) => file),
|
|
296
413
|
};
|
|
297
414
|
|
|
298
|
-
let
|
|
299
|
-
if (Buffer.byteLength(
|
|
415
|
+
let baseInput = JSON.stringify(result);
|
|
416
|
+
if (Buffer.byteLength(baseInput) > hardInputByteLimit) {
|
|
300
417
|
for (const file of result.files) {
|
|
301
418
|
const source = rawSnapshot.files.find((item) => item.path === file.path);
|
|
302
419
|
file.patch = source?.snippet || '';
|
|
303
420
|
file.patchIsExcerpt = true;
|
|
304
421
|
}
|
|
305
|
-
|
|
422
|
+
baseInput = JSON.stringify(result);
|
|
306
423
|
}
|
|
307
|
-
|
|
424
|
+
const baseBytes = Buffer.byteLength(baseInput);
|
|
425
|
+
if (baseBytes > hardInputByteLimit) {
|
|
308
426
|
throw new Error(
|
|
309
|
-
`The
|
|
427
|
+
`The agent input containing ${paths.join(', ')} is larger than the ${hardInputByteLimit}-byte hard limit`,
|
|
310
428
|
);
|
|
311
429
|
}
|
|
312
|
-
|
|
430
|
+
const existingFileNotes = compactExistingFileNotes(
|
|
431
|
+
snapshot,
|
|
432
|
+
existingFiles,
|
|
433
|
+
selected,
|
|
434
|
+
baseBytes,
|
|
435
|
+
);
|
|
436
|
+
return Object.keys(existingFileNotes).length
|
|
437
|
+
? `${baseInput.slice(0, -1)},"existingFileNotes":${JSON.stringify(existingFileNotes)}}`
|
|
438
|
+
: baseInput;
|
|
313
439
|
}
|
|
314
440
|
|
|
315
|
-
|
|
316
|
-
|
|
441
|
+
function textSchema(maxLength) {
|
|
442
|
+
return { type: 'string', minLength: 1, maxLength };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function listSchema(maxItems) {
|
|
446
|
+
return {
|
|
447
|
+
type: 'array',
|
|
448
|
+
maxItems,
|
|
449
|
+
items: textSchema(listItemCodePointLimit),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const title = textSchema(titleCodePointLimit);
|
|
454
|
+
const prose = textSchema(proseCodePointLimit);
|
|
455
|
+
const details = listSchema(detailItemLimit);
|
|
456
|
+
const risks = listSchema(riskItemLimit);
|
|
317
457
|
const fileNote = {
|
|
318
458
|
type: 'object',
|
|
319
459
|
properties: {
|
|
320
|
-
title
|
|
321
|
-
what:
|
|
322
|
-
why:
|
|
323
|
-
details
|
|
324
|
-
risks
|
|
460
|
+
title,
|
|
461
|
+
what: prose,
|
|
462
|
+
why: prose,
|
|
463
|
+
details,
|
|
464
|
+
risks,
|
|
325
465
|
},
|
|
326
466
|
required: ['title', 'what', 'why', 'details', 'risks'],
|
|
327
467
|
additionalProperties: false,
|
|
@@ -333,11 +473,11 @@ function outputSchema(paths, { includeChange = true } = {}) {
|
|
|
333
473
|
properties.change = {
|
|
334
474
|
type: 'object',
|
|
335
475
|
properties: {
|
|
336
|
-
title
|
|
337
|
-
summary:
|
|
338
|
-
why:
|
|
339
|
-
highlights:
|
|
340
|
-
risks
|
|
476
|
+
title,
|
|
477
|
+
summary: prose,
|
|
478
|
+
why: prose,
|
|
479
|
+
highlights: details,
|
|
480
|
+
risks,
|
|
341
481
|
},
|
|
342
482
|
required: ['title', 'summary', 'why', 'highlights', 'risks'],
|
|
343
483
|
additionalProperties: false,
|
|
@@ -392,18 +532,34 @@ risks to at most three concrete items. Use an empty list when there is no useful
|
|
|
392
532
|
detail or risk. For binary files, describe only the change shown by the metadata.`;
|
|
393
533
|
}
|
|
394
534
|
|
|
395
|
-
function normalizedText(value, field) {
|
|
535
|
+
function normalizedText(value, field, limit) {
|
|
396
536
|
if (typeof value !== 'string' || !value.trim()) {
|
|
397
537
|
throw new Error(`${field} must be a non-empty string`);
|
|
398
538
|
}
|
|
399
|
-
|
|
539
|
+
const text = value.trim();
|
|
540
|
+
if (Array.from(text).length > limit) {
|
|
541
|
+
throw new Error(`${field} must be at most ${limit} Unicode code points`);
|
|
542
|
+
}
|
|
543
|
+
return text;
|
|
400
544
|
}
|
|
401
545
|
|
|
402
|
-
|
|
546
|
+
// fallow-ignore-next-line complexity
|
|
547
|
+
function normalizedList(value, field, maxItems) {
|
|
403
548
|
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
404
549
|
throw new Error(`${field} must be a list of strings`);
|
|
405
550
|
}
|
|
406
|
-
|
|
551
|
+
if (value.length > maxItems) {
|
|
552
|
+
throw new Error(`${field} must contain at most ${maxItems} items`);
|
|
553
|
+
}
|
|
554
|
+
const list = value.map((item) => item.trim()).filter(Boolean);
|
|
555
|
+
for (const item of list) {
|
|
556
|
+
if (Array.from(item).length > listItemCodePointLimit) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
`${field} items must be at most ${listItemCodePointLimit} Unicode code points`,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return list;
|
|
407
563
|
}
|
|
408
564
|
|
|
409
565
|
function exactFields(value, fields, label) {
|
|
@@ -425,104 +581,229 @@ function exactFields(value, fields, label) {
|
|
|
425
581
|
}
|
|
426
582
|
}
|
|
427
583
|
|
|
428
|
-
function
|
|
429
|
-
value
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
584
|
+
function isObject(value) {
|
|
585
|
+
return Boolean(value) &&
|
|
586
|
+
typeof value === 'object' &&
|
|
587
|
+
!Array.isArray(value);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function responseObject(value) {
|
|
591
|
+
if (!isObject(value)) {
|
|
434
592
|
throw new Error('Agent response must be an object');
|
|
435
593
|
}
|
|
436
|
-
|
|
437
|
-
|
|
594
|
+
return value;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function unsupportedResponseFields(value) {
|
|
598
|
+
return Object.keys(value).filter(
|
|
599
|
+
(field) => !['change', 'files'].includes(field),
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function normalizeChangeResponse(value) {
|
|
604
|
+
const response = responseObject(value);
|
|
605
|
+
if (unsupportedResponseFields(response).length) {
|
|
438
606
|
throw new Error('Agent response has unsupported fields');
|
|
439
607
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
608
|
+
exactFields(
|
|
609
|
+
response.change,
|
|
610
|
+
['title', 'summary', 'why', 'highlights', 'risks'],
|
|
611
|
+
'Change note',
|
|
612
|
+
);
|
|
613
|
+
return {
|
|
614
|
+
title: normalizedText(response.change.title, 'change.title', titleCodePointLimit),
|
|
615
|
+
summary: normalizedText(response.change.summary, 'change.summary', proseCodePointLimit),
|
|
616
|
+
why: normalizedText(response.change.why, 'change.why', proseCodePointLimit),
|
|
617
|
+
highlights: normalizedList(
|
|
618
|
+
response.change.highlights,
|
|
619
|
+
'change.highlights', detailItemLimit,
|
|
620
|
+
),
|
|
621
|
+
risks: normalizedList(response.change.risks, 'change.risks', riskItemLimit),
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function arrayFileEntry(note) {
|
|
626
|
+
if (!isObject(note)) {
|
|
627
|
+
return { error: 'Agent response has a malformed file note' };
|
|
628
|
+
}
|
|
629
|
+
const path = typeof note.path === 'string' ? note.path.trim() : '';
|
|
630
|
+
return path
|
|
631
|
+
? { entry: { path, note, arrayForm: true } }
|
|
632
|
+
: { error: 'Agent response has a file note without a path' };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function arrayFileEntries(notes) {
|
|
636
|
+
const result = { entries: [], errors: [] };
|
|
637
|
+
for (const note of notes) {
|
|
638
|
+
const item = arrayFileEntry(note);
|
|
639
|
+
if (item.entry) result.entries.push(item.entry);
|
|
640
|
+
if (item.error) result.errors.push(item.error);
|
|
446
641
|
}
|
|
447
|
-
|
|
448
|
-
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function fileResponseEntries(value) {
|
|
646
|
+
const response = responseObject(value);
|
|
647
|
+
const errors = unsupportedResponseFields(response).map(
|
|
648
|
+
(field) => `Agent response has unsupported field: ${field}`,
|
|
649
|
+
);
|
|
650
|
+
if (Array.isArray(response.files)) {
|
|
651
|
+
const arrayResult = arrayFileEntries(response.files);
|
|
449
652
|
return {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
change: {
|
|
453
|
-
title: normalizedText(value.change.title, 'change.title'),
|
|
454
|
-
summary: normalizedText(
|
|
455
|
-
value.change.summary,
|
|
456
|
-
'change.summary',
|
|
457
|
-
),
|
|
458
|
-
why: normalizedText(value.change.why, 'change.why'),
|
|
459
|
-
highlights: normalizedList(
|
|
460
|
-
value.change.highlights,
|
|
461
|
-
'change.highlights',
|
|
462
|
-
),
|
|
463
|
-
risks: normalizedList(value.change.risks, 'change.risks'),
|
|
464
|
-
},
|
|
465
|
-
}
|
|
466
|
-
: {}),
|
|
467
|
-
files: {},
|
|
653
|
+
entries: arrayResult.entries,
|
|
654
|
+
errors: [...errors, ...arrayResult.errors],
|
|
468
655
|
};
|
|
469
656
|
}
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
657
|
+
if (!isObject(response.files)) {
|
|
658
|
+
throw new Error('Agent response has no file notes object');
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
entries: Object.entries(response.files).map(([path, note]) => ({
|
|
662
|
+
path,
|
|
663
|
+
note,
|
|
664
|
+
arrayForm: false,
|
|
665
|
+
})),
|
|
666
|
+
errors,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function normalizeFileNote(note, path, arrayForm) {
|
|
671
|
+
exactFields(
|
|
672
|
+
note,
|
|
673
|
+
[
|
|
674
|
+
...(arrayForm ? ['path'] : []),
|
|
675
|
+
'title',
|
|
676
|
+
'what',
|
|
677
|
+
'why',
|
|
678
|
+
'details',
|
|
679
|
+
'risks',
|
|
680
|
+
],
|
|
681
|
+
path,
|
|
682
|
+
);
|
|
683
|
+
return {
|
|
684
|
+
title: normalizedText(note.title, `${path}.title`, titleCodePointLimit),
|
|
685
|
+
what: normalizedText(note.what, `${path}.what`, proseCodePointLimit),
|
|
686
|
+
why: normalizedText(note.why, `${path}.why`, proseCodePointLimit),
|
|
687
|
+
details: normalizedList(note.details, `${path}.details`, detailItemLimit),
|
|
688
|
+
risks: normalizedList(note.risks, `${path}.risks`, riskItemLimit),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function indexFileEntries(entries, expected) {
|
|
693
|
+
const byPath = new Map();
|
|
694
|
+
const failedFiles = [];
|
|
695
|
+
for (const entry of entries) {
|
|
696
|
+
if (!expected.has(entry.path)) {
|
|
697
|
+
failedFiles.push({
|
|
698
|
+
path: entry.path,
|
|
699
|
+
reason: 'Agent output included a file outside this batch.',
|
|
700
|
+
});
|
|
701
|
+
continue;
|
|
481
702
|
}
|
|
482
|
-
|
|
483
|
-
|
|
703
|
+
const values = byPath.get(entry.path) || [];
|
|
704
|
+
byPath.set(entry.path, [...values, entry]);
|
|
705
|
+
}
|
|
706
|
+
return { byPath, failedFiles };
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function normalizeFileEntry(path, values) {
|
|
710
|
+
if (values.length !== 1) {
|
|
711
|
+
return {
|
|
712
|
+
failure: {
|
|
713
|
+
path,
|
|
714
|
+
reason: values.length
|
|
715
|
+
? 'Agent output repeated this file.'
|
|
716
|
+
: 'Agent output omitted this file.',
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
return {
|
|
722
|
+
note: normalizeFileNote(
|
|
723
|
+
values[0].note,
|
|
724
|
+
path,
|
|
725
|
+
values[0].arrayForm,
|
|
726
|
+
),
|
|
727
|
+
};
|
|
728
|
+
} catch (error) {
|
|
729
|
+
return {
|
|
730
|
+
failure: {
|
|
731
|
+
path,
|
|
732
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
733
|
+
},
|
|
734
|
+
};
|
|
484
735
|
}
|
|
485
|
-
|
|
736
|
+
}
|
|
486
737
|
|
|
738
|
+
function normalizeFileResponse(value, paths) {
|
|
739
|
+
const expected = new Set(paths);
|
|
487
740
|
const files = {};
|
|
741
|
+
const { entries, errors } = fileResponseEntries(value);
|
|
742
|
+
const indexed = indexFileEntries(entries, expected);
|
|
488
743
|
for (const path of paths) {
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
files[path] =
|
|
494
|
-
|
|
495
|
-
what: normalizedText(note.what, `${path}.what`),
|
|
496
|
-
why: normalizedText(note.why, `${path}.why`),
|
|
497
|
-
details: normalizedList(note.details, `${path}.details`),
|
|
498
|
-
risks: normalizedList(note.risks, `${path}.risks`),
|
|
499
|
-
};
|
|
744
|
+
const result = normalizeFileEntry(
|
|
745
|
+
path,
|
|
746
|
+
indexed.byPath.get(path) || [],
|
|
747
|
+
);
|
|
748
|
+
if (result.note) files[path] = result.note;
|
|
749
|
+
if (result.failure) indexed.failedFiles.push(result.failure);
|
|
500
750
|
}
|
|
751
|
+
return { files, failedFiles: indexed.failedFiles, errors };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function writeJsonAtomic(file, value, options) {
|
|
755
|
+
publishLeaseFile(
|
|
756
|
+
ownership,
|
|
757
|
+
file,
|
|
758
|
+
`${JSON.stringify(value, null, 2)}\n`,
|
|
759
|
+
options,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
501
762
|
|
|
763
|
+
function metadataList(value) {
|
|
764
|
+
return Array.isArray(value) ? value : [];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function summaryFailureState(snapshot, summaries) {
|
|
768
|
+
const failedFiles = metadataList(summaries.meta?.failedFiles);
|
|
769
|
+
const errors = metadataList(summaries.meta?.errors);
|
|
770
|
+
const emptyReviewComplete =
|
|
771
|
+
snapshot.files.length === 0 &&
|
|
772
|
+
summaries.meta?.status === 'complete' &&
|
|
773
|
+
summaries.meta?.reviewFingerprint ===
|
|
774
|
+
snapshot.notes?.reviewFingerprint;
|
|
775
|
+
const complete = [
|
|
776
|
+
failedFiles.length === 0,
|
|
777
|
+
errors.length === 0,
|
|
778
|
+
emptyReviewComplete ||
|
|
779
|
+
(completeChangeNote(summaries.change) &&
|
|
780
|
+
snapshot.files.every((file) =>
|
|
781
|
+
completeFileNote(summaries.files?.[file.path]),
|
|
782
|
+
)),
|
|
783
|
+
].every(Boolean);
|
|
784
|
+
return { failedFiles, errors, complete };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function snapshotFileWithNote(file, summaries, failureByPath) {
|
|
788
|
+
const nextFile = { ...file };
|
|
789
|
+
delete nextFile.noteFailure;
|
|
790
|
+
const note = summaries.files?.[file.path];
|
|
791
|
+
if (completeFileNote(note)) {
|
|
792
|
+
return { ...nextFile, summary: note, noteReady: true };
|
|
793
|
+
}
|
|
794
|
+
const failure = failureByPath.get(file.path);
|
|
502
795
|
return {
|
|
503
|
-
...
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
title: normalizedText(value.change.title, 'change.title'),
|
|
507
|
-
summary: normalizedText(value.change.summary, 'change.summary'),
|
|
508
|
-
why: normalizedText(value.change.why, 'change.why'),
|
|
509
|
-
highlights: normalizedList(
|
|
510
|
-
value.change.highlights,
|
|
511
|
-
'change.highlights',
|
|
512
|
-
),
|
|
513
|
-
risks: normalizedList(value.change.risks, 'change.risks'),
|
|
514
|
-
},
|
|
515
|
-
}
|
|
516
|
-
: {}),
|
|
517
|
-
files,
|
|
796
|
+
...nextFile,
|
|
797
|
+
noteReady: false,
|
|
798
|
+
...(failure ? { noteFailure: failure } : {}),
|
|
518
799
|
};
|
|
519
800
|
}
|
|
520
801
|
|
|
521
|
-
function
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
802
|
+
function notesWithoutFailures(notes = {}) {
|
|
803
|
+
const nextNotes = { ...notes };
|
|
804
|
+
delete nextNotes.failedFiles;
|
|
805
|
+
delete nextNotes.errors;
|
|
806
|
+
return nextNotes;
|
|
526
807
|
}
|
|
527
808
|
|
|
528
809
|
function publishSnapshot(snapshot, summaries) {
|
|
@@ -536,17 +817,18 @@ function publishSnapshot(snapshot, summaries) {
|
|
|
536
817
|
throw new Error('The diff changed while agent notes were being written');
|
|
537
818
|
}
|
|
538
819
|
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
820
|
+
const lockPath = pathInsideRepo(summariesPath);
|
|
821
|
+
const publishedFiles = snapshot.files.filter(
|
|
822
|
+
(file) => file.path !== `${lockPath}.lock`,
|
|
823
|
+
);
|
|
824
|
+
const publishedSnapshot = { ...snapshot, files: publishedFiles };
|
|
825
|
+
const state = summaryFailureState(publishedSnapshot, summaries);
|
|
826
|
+
const failureByPath = new Map(
|
|
827
|
+
state.failedFiles.map((failure) => [failure.path, failure.reason]),
|
|
828
|
+
);
|
|
829
|
+
const files = publishedFiles.map((file) =>
|
|
830
|
+
snapshotFileWithNote(file, summaries, failureByPath),
|
|
831
|
+
);
|
|
550
832
|
const content = {
|
|
551
833
|
...snapshot,
|
|
552
834
|
...(completeChangeNote(summaries.change)
|
|
@@ -554,15 +836,20 @@ function publishSnapshot(snapshot, summaries) {
|
|
|
554
836
|
: {}),
|
|
555
837
|
files,
|
|
556
838
|
notes: {
|
|
557
|
-
...snapshot.notes,
|
|
839
|
+
...notesWithoutFailures(snapshot.notes),
|
|
840
|
+
agent: selectedAgent,
|
|
558
841
|
generatedFor: reviewFingerprint,
|
|
559
842
|
fresh: true,
|
|
560
|
-
complete,
|
|
561
|
-
status: complete
|
|
843
|
+
complete: state.complete,
|
|
844
|
+
status: state.complete
|
|
562
845
|
? 'complete'
|
|
563
846
|
: summaries.meta?.status || 'generating',
|
|
564
847
|
completedFiles: files.filter((file) => file.noteReady).length,
|
|
565
848
|
totalFiles: files.length,
|
|
849
|
+
...(state.failedFiles.length
|
|
850
|
+
? { failedFiles: state.failedFiles }
|
|
851
|
+
: {}),
|
|
852
|
+
...(state.errors.length ? { errors: state.errors } : {}),
|
|
566
853
|
...(model ? { model } : {}),
|
|
567
854
|
...(reasoning ? { reasoning } : {}),
|
|
568
855
|
},
|
|
@@ -573,11 +860,15 @@ function publishSnapshot(snapshot, summaries) {
|
|
|
573
860
|
.update(JSON.stringify(content))
|
|
574
861
|
.digest('hex')
|
|
575
862
|
.slice(0, 12);
|
|
576
|
-
writeJsonAtomic(
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
863
|
+
writeJsonAtomic(
|
|
864
|
+
outputPath,
|
|
865
|
+
{
|
|
866
|
+
version,
|
|
867
|
+
generatedAt: new Date().toISOString(),
|
|
868
|
+
...content,
|
|
869
|
+
},
|
|
870
|
+
{ privateFile: false },
|
|
871
|
+
);
|
|
581
872
|
}
|
|
582
873
|
|
|
583
874
|
function publish(snapshot, summaries) {
|
|
@@ -588,6 +879,30 @@ function publish(snapshot, summaries) {
|
|
|
588
879
|
}
|
|
589
880
|
}
|
|
590
881
|
|
|
882
|
+
function storeProgress(snapshot, summaries) {
|
|
883
|
+
recordSyncStage('publish', () => {
|
|
884
|
+
writeJsonAtomic(summariesPath, summaries);
|
|
885
|
+
publish(snapshot, summaries);
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function emitFailedSupportRecord(code = 1) {
|
|
890
|
+
if (!supportRecorder) return;
|
|
891
|
+
const record = supportRecorder.failure(code);
|
|
892
|
+
try {
|
|
893
|
+
if (supportRecordPath) {
|
|
894
|
+
writeSupportRecord(supportRecordPath, record);
|
|
895
|
+
console.error(`Wrote support record to ${supportRecordPath}`);
|
|
896
|
+
} else {
|
|
897
|
+
process.stderr.write(
|
|
898
|
+
`Diffsplain support record:\n${formatSupportRecord(record)}`,
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
} catch {
|
|
902
|
+
console.error('Could not write the support record.');
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
591
906
|
function readJson(file, fallback) {
|
|
592
907
|
try {
|
|
593
908
|
return JSON.parse(readFileSync(file, 'utf8'));
|
|
@@ -596,14 +911,73 @@ function readJson(file, fallback) {
|
|
|
596
911
|
}
|
|
597
912
|
}
|
|
598
913
|
|
|
914
|
+
function readSummaryState(file) {
|
|
915
|
+
try {
|
|
916
|
+
const value = JSON.parse(readFileSync(file, 'utf8'));
|
|
917
|
+
const valid =
|
|
918
|
+
value &&
|
|
919
|
+
typeof value === 'object' &&
|
|
920
|
+
!Array.isArray(value);
|
|
921
|
+
return valid
|
|
922
|
+
? { value, damaged: false }
|
|
923
|
+
: { value: {}, damaged: true };
|
|
924
|
+
} catch {
|
|
925
|
+
return { value: {}, damaged: existsSync(file) };
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async function selectAgentForNotes() {
|
|
930
|
+
try {
|
|
931
|
+
selectedAgent = await selectCodingAgent(
|
|
932
|
+
requestedAgent,
|
|
933
|
+
(agent) => commandAvailable(codingAgentBinary(agent, { codexBin })),
|
|
934
|
+
);
|
|
935
|
+
assertReasoningSupported(selectedAgent, reasoning);
|
|
936
|
+
agentBinary = codingAgentBinary(selectedAgent, { codexBin });
|
|
937
|
+
supportRecorder?.setProvider(
|
|
938
|
+
selectedAgent,
|
|
939
|
+
safeCommandVersion(selectedAgent, agentBinary),
|
|
940
|
+
);
|
|
941
|
+
} catch (error) {
|
|
942
|
+
supportRecorder?.setProvider(requestedAgent || 'unknown');
|
|
943
|
+
if (error instanceof Error) error.exitCode = 2;
|
|
944
|
+
throw error;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function addFailures(summaries, failedFiles = [], errors = []) {
|
|
949
|
+
const priorFailedFiles = metadataList(summaries.meta?.failedFiles);
|
|
950
|
+
const priorErrors = metadataList(summaries.meta?.errors);
|
|
951
|
+
const uniqueFailures = new Map(
|
|
952
|
+
[...priorFailedFiles, ...failedFiles].map((failure) => [
|
|
953
|
+
`${failure.path}\0${failure.reason}`,
|
|
954
|
+
failure,
|
|
955
|
+
]),
|
|
956
|
+
);
|
|
957
|
+
const nextErrors = [...new Set([...priorErrors, ...errors])];
|
|
958
|
+
return {
|
|
959
|
+
...summaries,
|
|
960
|
+
meta: {
|
|
961
|
+
...summaries.meta,
|
|
962
|
+
...(uniqueFailures.size
|
|
963
|
+
? { failedFiles: [...uniqueFailures.values()] }
|
|
964
|
+
: {}),
|
|
965
|
+
...(nextErrors.length ? { errors: nextErrors } : {}),
|
|
966
|
+
},
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function failureReason(error) {
|
|
971
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
972
|
+
return message.split('\n').find((line) => line.trim())?.trim() ||
|
|
973
|
+
'Agent note generation failed.';
|
|
974
|
+
}
|
|
975
|
+
|
|
599
976
|
function runAgent(invocation, input) {
|
|
600
977
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
601
978
|
const child = spawn(invocation.command, invocation.args, {
|
|
602
979
|
cwd: invocation.cwd || root,
|
|
603
|
-
env:
|
|
604
|
-
...process.env,
|
|
605
|
-
...invocation.env,
|
|
606
|
-
},
|
|
980
|
+
env: invocation.env || process.env,
|
|
607
981
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
608
982
|
});
|
|
609
983
|
activeAgentProcesses.add(child);
|
|
@@ -613,6 +987,7 @@ function runAgent(invocation, input) {
|
|
|
613
987
|
const maxBuffer = 10 * 1024 * 1024;
|
|
614
988
|
const collect = (chunks, chunk) => {
|
|
615
989
|
outputBytes += chunk.length;
|
|
990
|
+
supportRecorder?.addBytes('agentOutput', chunk.length);
|
|
616
991
|
if (outputBytes > maxBuffer) {
|
|
617
992
|
child.kill('SIGTERM');
|
|
618
993
|
rejectPromise(new Error(`${selectedAgent} returned too much output`));
|
|
@@ -622,6 +997,9 @@ function runAgent(invocation, input) {
|
|
|
622
997
|
};
|
|
623
998
|
child.stdout.on('data', (chunk) => collect(stdout, chunk));
|
|
624
999
|
child.stderr.on('data', (chunk) => collect(stderr, chunk));
|
|
1000
|
+
child.stdin.on('error', (error) => {
|
|
1001
|
+
if (error.code !== 'EPIPE') rejectPromise(error);
|
|
1002
|
+
});
|
|
625
1003
|
child.once('error', (error) => {
|
|
626
1004
|
activeAgentProcesses.delete(child);
|
|
627
1005
|
rejectPromise(error);
|
|
@@ -653,13 +1031,26 @@ function runAgent(invocation, input) {
|
|
|
653
1031
|
);
|
|
654
1032
|
return;
|
|
655
1033
|
}
|
|
656
|
-
resolvePromise(stdoutText);
|
|
1034
|
+
resolvePromise({ stdout: stdoutText, stderr: stderrText });
|
|
657
1035
|
});
|
|
658
1036
|
if (invocation.input === 'stdin') child.stdin.end(input);
|
|
659
1037
|
else child.stdin.end();
|
|
660
1038
|
});
|
|
661
1039
|
}
|
|
662
1040
|
|
|
1041
|
+
async function requestAgent(invocation, input, normalize) {
|
|
1042
|
+
supportRecorder?.addBytes('agentInput', Buffer.byteLength(input));
|
|
1043
|
+
return recordAsyncStage('agent', async () => {
|
|
1044
|
+
const result = await runAgent(invocation, input);
|
|
1045
|
+
if (result.stderr.trim()) {
|
|
1046
|
+
console.error(
|
|
1047
|
+
`${selectedAgent} wrote diagnostic output:\n${result.stderr.trim()}`,
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
return normalize(result.stdout);
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
663
1054
|
function completeText(value) {
|
|
664
1055
|
return typeof value === 'string' && Boolean(value.trim());
|
|
665
1056
|
}
|
|
@@ -711,6 +1102,18 @@ function fileFingerprint(file) {
|
|
|
711
1102
|
.digest('hex');
|
|
712
1103
|
}
|
|
713
1104
|
|
|
1105
|
+
function generationSettingsMatch(meta, generationSettings) {
|
|
1106
|
+
if (!meta || typeof meta !== 'object' || typeof meta.agent !== 'string') {
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
const previousSettings = {
|
|
1110
|
+
agent: meta.agent,
|
|
1111
|
+
model: Object.hasOwn(meta, 'model') ? meta.model : null,
|
|
1112
|
+
reasoning: Object.hasOwn(meta, 'reasoning') ? meta.reasoning : null,
|
|
1113
|
+
};
|
|
1114
|
+
return JSON.stringify(previousSettings) === JSON.stringify(generationSettings);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
714
1117
|
const temporaryDirectory = mkdtempSync(
|
|
715
1118
|
resolve(tmpdir(), 'diffsplain-agent-'),
|
|
716
1119
|
);
|
|
@@ -718,22 +1121,36 @@ let workingSummaries;
|
|
|
718
1121
|
let workingSnapshot;
|
|
719
1122
|
|
|
720
1123
|
try {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1124
|
+
recordSyncStage('cache', acquireOwnership);
|
|
1125
|
+
const { rawSnapshot, snapshot } = recordSyncStage('snapshot', () => {
|
|
1126
|
+
const rawSnapshotPath = resolve(temporaryDirectory, 'diff-data.json');
|
|
1127
|
+
if (!snapshotPath) runBuilder(rawSnapshotPath, true);
|
|
1128
|
+
const rawSnapshotText = readFileSync(
|
|
1129
|
+
snapshotPath || rawSnapshotPath,
|
|
1130
|
+
'utf8',
|
|
1131
|
+
);
|
|
1132
|
+
supportRecorder?.addBytes(
|
|
1133
|
+
'snapshot',
|
|
1134
|
+
Buffer.byteLength(rawSnapshotText),
|
|
1135
|
+
);
|
|
1136
|
+
const value = JSON.parse(rawSnapshotText);
|
|
1137
|
+
return { rawSnapshot: value, snapshot: cleanSnapshot(value) };
|
|
1138
|
+
});
|
|
727
1139
|
const paths = snapshot.files.map((file) => file.path);
|
|
728
|
-
const
|
|
1140
|
+
const previousState = readSummaryState(summariesPath);
|
|
1141
|
+
const previousSummaries = previousState.value;
|
|
1142
|
+
if (previousState.damaged) {
|
|
1143
|
+
console.error(
|
|
1144
|
+
`Saved notes at ${summariesPath} are damaged. Rebuilding them from the current review.`,
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
729
1147
|
if (paths.length === 0) {
|
|
1148
|
+
if (requestedAgent) await selectAgentForNotes();
|
|
730
1149
|
workingSnapshot = rawSnapshot;
|
|
731
1150
|
workingSummaries = {
|
|
732
|
-
...(completeChangeNote(previousSummaries.change)
|
|
733
|
-
? { change: previousSummaries.change }
|
|
734
|
-
: {}),
|
|
735
1151
|
files: {},
|
|
736
1152
|
meta: {
|
|
1153
|
+
agent: selectedAgent,
|
|
737
1154
|
reviewFingerprint: rawSnapshot.notes.reviewFingerprint,
|
|
738
1155
|
fileFingerprints: {},
|
|
739
1156
|
status: 'complete',
|
|
@@ -742,10 +1159,15 @@ try {
|
|
|
742
1159
|
...(reasoning ? { reasoning } : {}),
|
|
743
1160
|
},
|
|
744
1161
|
};
|
|
745
|
-
|
|
746
|
-
publish(rawSnapshot, workingSummaries);
|
|
1162
|
+
storeProgress(rawSnapshot, workingSummaries);
|
|
747
1163
|
console.log('No changed files to summarize.');
|
|
748
1164
|
} else {
|
|
1165
|
+
await selectAgentForNotes();
|
|
1166
|
+
const generationSettings = {
|
|
1167
|
+
agent: selectedAgent,
|
|
1168
|
+
model: model || null,
|
|
1169
|
+
reasoning: reasoning || null,
|
|
1170
|
+
};
|
|
749
1171
|
const startedAt = new Date().toISOString();
|
|
750
1172
|
const previousFiles =
|
|
751
1173
|
previousSummaries.files &&
|
|
@@ -765,45 +1187,67 @@ try {
|
|
|
765
1187
|
const fileFingerprints = Object.fromEntries(
|
|
766
1188
|
paths.map((path) => [path, fileFingerprint(rawFiles.get(path))]),
|
|
767
1189
|
);
|
|
1190
|
+
const settingsMatch = generationSettingsMatch(
|
|
1191
|
+
previousSummaries.meta,
|
|
1192
|
+
generationSettings,
|
|
1193
|
+
);
|
|
1194
|
+
const summaryFiles = new Map(
|
|
1195
|
+
snapshot.files.map((file) => [file.path, file]),
|
|
1196
|
+
);
|
|
768
1197
|
const reusableFiles = {};
|
|
769
1198
|
const changedPaths = [];
|
|
1199
|
+
const inputFailures = [];
|
|
770
1200
|
for (const path of paths) {
|
|
771
1201
|
if (
|
|
772
1202
|
!force &&
|
|
1203
|
+
settingsMatch &&
|
|
773
1204
|
previousFingerprints[path] === fileFingerprints[path] &&
|
|
774
1205
|
completeFileNote(previousFiles[path])
|
|
775
1206
|
) {
|
|
776
1207
|
reusableFiles[path] = previousFiles[path];
|
|
1208
|
+
} else if (summaryFiles.get(path)?.summaryFailure) {
|
|
1209
|
+
inputFailures.push({
|
|
1210
|
+
path,
|
|
1211
|
+
reason: summaryFiles.get(path).summaryFailure,
|
|
1212
|
+
});
|
|
777
1213
|
} else {
|
|
778
1214
|
changedPaths.push(path);
|
|
779
1215
|
}
|
|
780
1216
|
}
|
|
781
1217
|
const changeNeedsRefresh =
|
|
782
1218
|
force ||
|
|
1219
|
+
!settingsMatch ||
|
|
783
1220
|
previousSummaries.meta?.reviewFingerprint !==
|
|
784
1221
|
rawSnapshot.notes.reviewFingerprint ||
|
|
785
1222
|
!completeChangeNote(previousSummaries.change);
|
|
786
1223
|
const needsGeneration = changedPaths.length > 0 || changeNeedsRefresh;
|
|
787
1224
|
|
|
788
1225
|
workingSnapshot = rawSnapshot;
|
|
789
|
-
workingSummaries =
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
? {
|
|
799
|
-
:
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1226
|
+
workingSummaries = addFailures(
|
|
1227
|
+
{
|
|
1228
|
+
...(!changeNeedsRefresh ? { change: previousSummaries.change } : {}),
|
|
1229
|
+
files: reusableFiles,
|
|
1230
|
+
meta: {
|
|
1231
|
+
agent: selectedAgent,
|
|
1232
|
+
reviewFingerprint: rawSnapshot.notes.reviewFingerprint,
|
|
1233
|
+
fileFingerprints,
|
|
1234
|
+
...(needsGeneration
|
|
1235
|
+
? { startedAt }
|
|
1236
|
+
: previousSummaries.meta?.generatedAt
|
|
1237
|
+
? { generatedAt: previousSummaries.meta.generatedAt }
|
|
1238
|
+
: {}),
|
|
1239
|
+
status: needsGeneration
|
|
1240
|
+
? 'generating'
|
|
1241
|
+
: inputFailures.length
|
|
1242
|
+
? 'failed'
|
|
1243
|
+
: 'complete',
|
|
1244
|
+
...(model ? { model } : {}),
|
|
1245
|
+
...(reasoning ? { reasoning } : {}),
|
|
1246
|
+
},
|
|
803
1247
|
},
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1248
|
+
inputFailures,
|
|
1249
|
+
);
|
|
1250
|
+
storeProgress(rawSnapshot, workingSummaries);
|
|
807
1251
|
|
|
808
1252
|
const batches = [];
|
|
809
1253
|
let batch = [];
|
|
@@ -825,8 +1269,7 @@ try {
|
|
|
825
1269
|
}
|
|
826
1270
|
if (batch.length) batches.push(batch);
|
|
827
1271
|
let nextBatch = 0;
|
|
828
|
-
const
|
|
829
|
-
const batchPaths = batches[index];
|
|
1272
|
+
const requestBatch = async (index, batchPaths) => {
|
|
830
1273
|
const schemaPath = resolve(
|
|
831
1274
|
temporaryDirectory,
|
|
832
1275
|
`summary-schema-${index + 1}.json`,
|
|
@@ -866,18 +1309,40 @@ try {
|
|
|
866
1309
|
console.error(
|
|
867
1310
|
`Asking ${selectedAgent} for batch ${index + 1} of ${batches.length} (${batchPaths.length} changed files)...`,
|
|
868
1311
|
);
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1312
|
+
return requestAgent(
|
|
1313
|
+
invocation,
|
|
1314
|
+
input,
|
|
1315
|
+
(stdout) =>
|
|
1316
|
+
normalizeFileResponse(
|
|
1317
|
+
parseAgentResponse(selectedAgent, stdout),
|
|
1318
|
+
batchPaths,
|
|
1319
|
+
),
|
|
1320
|
+
);
|
|
1321
|
+
};
|
|
1322
|
+
const runBatch = async (index) => {
|
|
1323
|
+
const batchPaths = batches[index];
|
|
1324
|
+
let outcome;
|
|
1325
|
+
try {
|
|
1326
|
+
outcome = await requestBatch(index, batchPaths);
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
if (interrupted) throw error;
|
|
1329
|
+
const reason = failureReason(error);
|
|
1330
|
+
console.error(
|
|
1331
|
+
error instanceof Error ? error.message : String(error),
|
|
1332
|
+
);
|
|
1333
|
+
outcome = {
|
|
1334
|
+
files: {},
|
|
1335
|
+
failedFiles: batchPaths.map((path) => ({ path, reason })),
|
|
1336
|
+
errors: [],
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
874
1339
|
workingSummaries = {
|
|
875
1340
|
...(workingSummaries.change
|
|
876
1341
|
? { change: workingSummaries.change }
|
|
877
1342
|
: {}),
|
|
878
1343
|
files: {
|
|
879
1344
|
...workingSummaries.files,
|
|
880
|
-
...
|
|
1345
|
+
...outcome.files,
|
|
881
1346
|
},
|
|
882
1347
|
meta: {
|
|
883
1348
|
...workingSummaries.meta,
|
|
@@ -885,8 +1350,12 @@ try {
|
|
|
885
1350
|
generatedAt: new Date().toISOString(),
|
|
886
1351
|
},
|
|
887
1352
|
};
|
|
888
|
-
|
|
889
|
-
|
|
1353
|
+
workingSummaries = addFailures(
|
|
1354
|
+
workingSummaries,
|
|
1355
|
+
outcome.failedFiles,
|
|
1356
|
+
outcome.errors,
|
|
1357
|
+
);
|
|
1358
|
+
storeProgress(rawSnapshot, workingSummaries);
|
|
890
1359
|
if (batchPaths.length) {
|
|
891
1360
|
console.log(
|
|
892
1361
|
`Wrote ${Object.keys(workingSummaries.files).length} of ${paths.length} agent notes to ${summariesPath}`,
|
|
@@ -896,7 +1365,7 @@ try {
|
|
|
896
1365
|
const workers = Array.from(
|
|
897
1366
|
{ length: Math.min(jobs, batches.length) },
|
|
898
1367
|
async () => {
|
|
899
|
-
while (nextBatch < batches.length) {
|
|
1368
|
+
while (!interrupted && nextBatch < batches.length) {
|
|
900
1369
|
const index = nextBatch;
|
|
901
1370
|
nextBatch += 1;
|
|
902
1371
|
await runBatch(index);
|
|
@@ -905,91 +1374,122 @@ try {
|
|
|
905
1374
|
);
|
|
906
1375
|
await Promise.all(workers);
|
|
907
1376
|
if (changeNeedsRefresh) {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
966
|
-
writeJsonAtomic(summariesPath, workingSummaries);
|
|
967
|
-
publish(rawSnapshot, workingSummaries);
|
|
1377
|
+
try {
|
|
1378
|
+
const schemaPath = resolve(
|
|
1379
|
+
temporaryDirectory,
|
|
1380
|
+
'change-summary-schema.json',
|
|
1381
|
+
);
|
|
1382
|
+
const schema = outputSchema([]);
|
|
1383
|
+
writeFileSync(
|
|
1384
|
+
schemaPath,
|
|
1385
|
+
`${JSON.stringify(schema, null, 2)}\n`,
|
|
1386
|
+
);
|
|
1387
|
+
const input = batchInput(
|
|
1388
|
+
snapshot,
|
|
1389
|
+
rawSnapshot,
|
|
1390
|
+
[],
|
|
1391
|
+
workingSummaries.files,
|
|
1392
|
+
);
|
|
1393
|
+
const inputPath = resolve(
|
|
1394
|
+
temporaryDirectory,
|
|
1395
|
+
'change-summary-input.json',
|
|
1396
|
+
);
|
|
1397
|
+
writeFileSync(inputPath, input);
|
|
1398
|
+
const invocation = agentCommand({
|
|
1399
|
+
agent: selectedAgent,
|
|
1400
|
+
binary: agentBinary,
|
|
1401
|
+
model,
|
|
1402
|
+
reasoning,
|
|
1403
|
+
prompt: promptFor([]),
|
|
1404
|
+
schema,
|
|
1405
|
+
schemaPath,
|
|
1406
|
+
inputPath,
|
|
1407
|
+
workingDirectory: root,
|
|
1408
|
+
});
|
|
1409
|
+
console.error(`Asking ${selectedAgent} for the change note...`);
|
|
1410
|
+
const normalized = await requestAgent(
|
|
1411
|
+
invocation,
|
|
1412
|
+
input,
|
|
1413
|
+
(stdout) =>
|
|
1414
|
+
normalizeChangeResponse(
|
|
1415
|
+
parseAgentResponse(selectedAgent, stdout),
|
|
1416
|
+
),
|
|
1417
|
+
);
|
|
1418
|
+
workingSummaries = {
|
|
1419
|
+
change: normalized,
|
|
1420
|
+
files: workingSummaries.files,
|
|
1421
|
+
meta: workingSummaries.meta,
|
|
1422
|
+
};
|
|
1423
|
+
console.log(`Updated the change note in ${summariesPath}`);
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
if (interrupted) throw error;
|
|
1426
|
+
console.error(
|
|
1427
|
+
error instanceof Error ? error.message : String(error),
|
|
1428
|
+
);
|
|
1429
|
+
workingSummaries = addFailures(
|
|
1430
|
+
workingSummaries,
|
|
1431
|
+
[],
|
|
1432
|
+
[`Change note: ${failureReason(error)}`],
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
968
1435
|
}
|
|
969
|
-
|
|
1436
|
+
const failedFiles = workingSummaries.meta.failedFiles || [];
|
|
1437
|
+
const generationErrors = workingSummaries.meta.errors || [];
|
|
1438
|
+
const complete =
|
|
1439
|
+
failedFiles.length === 0 &&
|
|
1440
|
+
generationErrors.length === 0 &&
|
|
1441
|
+
completeChangeNote(workingSummaries.change) &&
|
|
1442
|
+
paths.every((path) =>
|
|
1443
|
+
completeFileNote(workingSummaries.files[path]),
|
|
1444
|
+
);
|
|
1445
|
+
workingSummaries = {
|
|
1446
|
+
...workingSummaries,
|
|
1447
|
+
meta: {
|
|
1448
|
+
...workingSummaries.meta,
|
|
1449
|
+
status: complete ? 'complete' : 'failed',
|
|
1450
|
+
generatedAt: new Date().toISOString(),
|
|
1451
|
+
},
|
|
1452
|
+
};
|
|
1453
|
+
storeProgress(rawSnapshot, workingSummaries);
|
|
1454
|
+
if (batches.length === 0 && inputFailures.length === 0) {
|
|
970
1455
|
console.log('No file summaries changed.');
|
|
971
1456
|
}
|
|
1457
|
+
for (const failure of failedFiles) {
|
|
1458
|
+
console.error(`${failure.path}: ${failure.reason}`);
|
|
1459
|
+
}
|
|
1460
|
+
for (const error of generationErrors) console.error(error);
|
|
1461
|
+
if (!complete) {
|
|
1462
|
+
process.exitCode = 1;
|
|
1463
|
+
emitFailedSupportRecord(1);
|
|
1464
|
+
}
|
|
972
1465
|
console.log(`Rebuilt ${outputPath}`);
|
|
973
1466
|
}
|
|
974
1467
|
} catch (error) {
|
|
975
1468
|
if (!interrupted && workingSummaries && workingSnapshot) {
|
|
976
1469
|
try {
|
|
977
|
-
workingSummaries =
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1470
|
+
workingSummaries = addFailures(
|
|
1471
|
+
{
|
|
1472
|
+
...workingSummaries,
|
|
1473
|
+
meta: {
|
|
1474
|
+
...workingSummaries.meta,
|
|
1475
|
+
status: 'failed',
|
|
1476
|
+
generatedAt: new Date().toISOString(),
|
|
1477
|
+
},
|
|
983
1478
|
},
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1479
|
+
[],
|
|
1480
|
+
[failureReason(error)],
|
|
1481
|
+
);
|
|
1482
|
+
storeProgress(workingSnapshot, workingSummaries);
|
|
987
1483
|
} catch {}
|
|
988
1484
|
}
|
|
989
1485
|
if (!interrupted) {
|
|
990
1486
|
console.error(error instanceof Error ? error.message : String(error));
|
|
991
|
-
process.exitCode =
|
|
1487
|
+
process.exitCode =
|
|
1488
|
+
error instanceof Error && error.exitCode === 2 ? 2 : 1;
|
|
1489
|
+
emitFailedSupportRecord(process.exitCode);
|
|
992
1490
|
}
|
|
993
1491
|
} finally {
|
|
1492
|
+
if (ownershipHeartbeat) clearInterval(ownershipHeartbeat);
|
|
1493
|
+
if (ownership) { try { releaseLease(ownership); } catch {} }
|
|
994
1494
|
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
995
1495
|
}
|