reslop 0.2.0 → 0.2.3
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/CHANGELOG.md +27 -1
- package/README.md +13 -2
- package/lib/ansi.js +263 -6
- package/lib/api.js +2 -0
- package/lib/capabilities.js +6 -0
- package/lib/cli.js +13 -2
- package/lib/deps.js +43 -3
- package/lib/git-branches.js +228 -17
- package/lib/git-reword-todo.js +20 -0
- package/lib/git.js +20 -3
- package/lib/keys.js +10 -4
- package/lib/npm-commands.js +513 -0
- package/lib/render/branches.js +23 -58
- package/lib/render/chrome.js +156 -99
- package/lib/render/commits.js +643 -31
- package/lib/render/diff.js +4 -4
- package/lib/render/frame.js +51 -5
- package/lib/render/notes.js +22 -5
- package/lib/render/npm.js +228 -0
- package/lib/render/primitives.js +48 -34
- package/lib/render/render.js +3 -3
- package/lib/report-model.js +127 -0
- package/lib/report-parse.js +94 -0
- package/lib/report-render.js +87 -0
- package/lib/report-run.js +164 -0
- package/lib/report-test.js +506 -0
- package/lib/report-tools.js +311 -0
- package/lib/review-merge.js +270 -0
- package/lib/review.js +38 -6
- package/lib/session/actions.js +81 -5
- package/lib/session/commit.js +187 -19
- package/lib/session/compose.js +96 -32
- package/lib/session/dispatch.js +36 -2
- package/lib/session/files-pane.js +36 -6
- package/lib/session/lifecycle.js +116 -16
- package/lib/session/navigation.js +1 -0
- package/lib/session/npm.js +335 -0
- package/lib/session/pointer.js +12 -1
- package/lib/session/todos.js +29 -1
- package/lib/session/view.js +14 -3
- package/lib/session/watch.js +120 -8
- package/lib/session.js +11 -2
- package/package.json +7 -7
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parseAuditReport, parseOutdatedReport } = require('./deps.js');
|
|
4
|
+
const model = require('./report-model.js');
|
|
5
|
+
|
|
6
|
+
const { summary, problem, report, userTrace } = model;
|
|
7
|
+
|
|
8
|
+
const place = (file, line, column, root) =>
|
|
9
|
+
userTrace(`${file}:${line}:${column}`, root);
|
|
10
|
+
|
|
11
|
+
const parseEslintJson = (data, root) => {
|
|
12
|
+
const rows = Array.isArray(data) ? data : data && data.results;
|
|
13
|
+
if (!Array.isArray(rows) || !rows.length) return null;
|
|
14
|
+
if (!rows[0] || !Array.isArray(rows[0].messages)) return null;
|
|
15
|
+
const stats = summary();
|
|
16
|
+
let errors = 0;
|
|
17
|
+
let warnings = 0;
|
|
18
|
+
const problems = [];
|
|
19
|
+
for (const file of rows) {
|
|
20
|
+
const messages = file.messages || [];
|
|
21
|
+
for (const item of messages) {
|
|
22
|
+
const severity = item.severity === 1 ? 'warning' : 'error';
|
|
23
|
+
if (severity === 'warning') warnings += 1;
|
|
24
|
+
else errors += 1;
|
|
25
|
+
problems.push(
|
|
26
|
+
problem({
|
|
27
|
+
severity,
|
|
28
|
+
message: item.message || '',
|
|
29
|
+
code: item.ruleId || '',
|
|
30
|
+
trace: place(
|
|
31
|
+
file.filePath || '',
|
|
32
|
+
item.line || 0,
|
|
33
|
+
item.column || 0,
|
|
34
|
+
root,
|
|
35
|
+
),
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
stats.errors = errors;
|
|
41
|
+
stats.warnings = warnings;
|
|
42
|
+
return report('eslint', { summary: stats, problems });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const eslintLine = (line) => {
|
|
46
|
+
const match = /^\s*(\d+):(\d+)\s+(error|warning)\s+(.*)$/.exec(line);
|
|
47
|
+
if (!match) return null;
|
|
48
|
+
const rest = match[4];
|
|
49
|
+
const ruleAt = rest.lastIndexOf(' ');
|
|
50
|
+
const message = ruleAt < 0 ? rest : rest.slice(0, ruleAt);
|
|
51
|
+
const code = ruleAt < 0 ? '' : rest.slice(ruleAt + 2).trim();
|
|
52
|
+
return {
|
|
53
|
+
line: match[1],
|
|
54
|
+
column: match[2],
|
|
55
|
+
severity: match[3],
|
|
56
|
+
message: message.trim(),
|
|
57
|
+
code,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const isPathLine = (line) => {
|
|
62
|
+
const text = line.trim();
|
|
63
|
+
if (!text || text.startsWith('✖')) return false;
|
|
64
|
+
if (eslintLine(text)) return false;
|
|
65
|
+
return text.includes('/') || text.includes('\\') || /\.\w+$/.test(text);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const parseEslintStylish = (text, root) => {
|
|
69
|
+
const lines = `${text ?? ''}`.split(/\r?\n/);
|
|
70
|
+
const problems = [];
|
|
71
|
+
let file = '';
|
|
72
|
+
let errors = 0;
|
|
73
|
+
let warnings = 0;
|
|
74
|
+
let seen = false;
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const item = eslintLine(line);
|
|
77
|
+
if (item) {
|
|
78
|
+
seen = true;
|
|
79
|
+
if (item.severity === 'warning') warnings += 1;
|
|
80
|
+
else errors += 1;
|
|
81
|
+
problems.push(
|
|
82
|
+
problem({
|
|
83
|
+
severity: item.severity,
|
|
84
|
+
message: item.message,
|
|
85
|
+
code: item.code,
|
|
86
|
+
trace: place(file, item.line, item.column, root),
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (/✖\s+\d+\s+problems?\b/.test(line)) seen = true;
|
|
92
|
+
if (isPathLine(line)) file = line.trim();
|
|
93
|
+
}
|
|
94
|
+
if (!seen) return null;
|
|
95
|
+
const stats = summary();
|
|
96
|
+
stats.errors = errors;
|
|
97
|
+
stats.warnings = warnings;
|
|
98
|
+
return report('eslint', { summary: stats, problems });
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const tscLine = (line) => {
|
|
102
|
+
const match = /^(.*)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.*)$/.exec(
|
|
103
|
+
line.trim(),
|
|
104
|
+
);
|
|
105
|
+
if (!match) return null;
|
|
106
|
+
return {
|
|
107
|
+
file: match[1],
|
|
108
|
+
line: match[2],
|
|
109
|
+
column: match[3],
|
|
110
|
+
code: match[4],
|
|
111
|
+
message: match[5],
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const parseTsc = (text, root) => {
|
|
116
|
+
const problems = [];
|
|
117
|
+
for (const line of `${text ?? ''}`.split(/\r?\n/)) {
|
|
118
|
+
const item = tscLine(line);
|
|
119
|
+
if (!item) continue;
|
|
120
|
+
problems.push(
|
|
121
|
+
problem({
|
|
122
|
+
message: item.message,
|
|
123
|
+
code: item.code,
|
|
124
|
+
trace: place(item.file, item.line, item.column, root),
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (!problems.length) return null;
|
|
129
|
+
const stats = summary();
|
|
130
|
+
stats.errors = problems.length;
|
|
131
|
+
return report('tsc', { summary: stats, problems });
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const prettierFile = (line) => {
|
|
135
|
+
const warn = /^\[(?:warn|error)\]\s+(\S+)$/.exec(line.trim());
|
|
136
|
+
if (warn && !warn[1].startsWith('Code')) return warn[1];
|
|
137
|
+
return '';
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const parsePrettier = (text, root) => {
|
|
141
|
+
const lines = `${text ?? ''}`.split(/\r?\n/);
|
|
142
|
+
const problems = [];
|
|
143
|
+
let seen = false;
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
if (line.includes('Code style issues')) seen = true;
|
|
146
|
+
if (line.includes('All matched files use Prettier')) seen = true;
|
|
147
|
+
const file = prettierFile(line);
|
|
148
|
+
if (!file) continue;
|
|
149
|
+
seen = true;
|
|
150
|
+
problems.push(
|
|
151
|
+
problem({
|
|
152
|
+
message: 'format',
|
|
153
|
+
trace: userTrace(file, root),
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (!seen) return null;
|
|
158
|
+
const stats = summary();
|
|
159
|
+
stats.errors = problems.length;
|
|
160
|
+
return report('prettier', { summary: stats, problems });
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const auditProblems = (text) => {
|
|
164
|
+
const audit = parseAuditReport(text);
|
|
165
|
+
const problems = [];
|
|
166
|
+
for (const item of audit.values()) {
|
|
167
|
+
const extra = [];
|
|
168
|
+
if (item.range) extra.push(['range', item.range]);
|
|
169
|
+
if (item.fix) extra.push(['fix', item.fix]);
|
|
170
|
+
problems.push(
|
|
171
|
+
problem({
|
|
172
|
+
severity: item.severity || 'error',
|
|
173
|
+
name: item.name,
|
|
174
|
+
message: item.title || '',
|
|
175
|
+
extra,
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return problems;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const parseAudit = (text) => {
|
|
183
|
+
const problems = auditProblems(text);
|
|
184
|
+
if (!problems.length) return null;
|
|
185
|
+
const stats = summary();
|
|
186
|
+
stats.errors = problems.length;
|
|
187
|
+
return report('audit', { summary: stats, problems });
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const behind = (item) => {
|
|
191
|
+
if (!item.current) return true;
|
|
192
|
+
if (item.wanted && item.wanted !== item.current) return true;
|
|
193
|
+
if (item.latest && item.latest !== item.current) return true;
|
|
194
|
+
return false;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const parseOutdated = (text) => {
|
|
198
|
+
const outdated = parseOutdatedReport(text);
|
|
199
|
+
const problems = [];
|
|
200
|
+
for (const [name, item] of outdated) {
|
|
201
|
+
if (!behind(item)) continue;
|
|
202
|
+
problems.push(
|
|
203
|
+
problem({
|
|
204
|
+
severity: 'warning',
|
|
205
|
+
name,
|
|
206
|
+
message: `${item.current} ${item.wanted} ${item.latest}`.trim(),
|
|
207
|
+
extra: [
|
|
208
|
+
['current', item.current],
|
|
209
|
+
['wanted', item.wanted],
|
|
210
|
+
['latest', item.latest],
|
|
211
|
+
],
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (!problems.length) return null;
|
|
216
|
+
const stats = summary();
|
|
217
|
+
stats.warnings = problems.length;
|
|
218
|
+
return report('outdated', { summary: stats, problems });
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const pushLs = (problems, text) => {
|
|
222
|
+
const message = `${text ?? ''}`.trim();
|
|
223
|
+
if (!message) return;
|
|
224
|
+
problems.push(problem({ message }));
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const walkLs = (node, problems) => {
|
|
228
|
+
if (!node || typeof node !== 'object') return;
|
|
229
|
+
if (Array.isArray(node.problems)) {
|
|
230
|
+
for (const item of node.problems) pushLs(problems, item);
|
|
231
|
+
}
|
|
232
|
+
if (node.missing) {
|
|
233
|
+
pushLs(problems, `missing ${node.required || node.version || ''}`);
|
|
234
|
+
}
|
|
235
|
+
if (node.invalid) pushLs(problems, `invalid ${node.version || ''}`);
|
|
236
|
+
if (node.extraneous) pushLs(problems, `extraneous ${node.version || ''}`);
|
|
237
|
+
const deps = node.dependencies;
|
|
238
|
+
if (!deps || typeof deps !== 'object') return;
|
|
239
|
+
for (const name of Object.keys(deps)) walkLs(deps[name], problems);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const parseLs = (data) => {
|
|
243
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return null;
|
|
244
|
+
if (!data.dependencies && !Array.isArray(data.problems)) return null;
|
|
245
|
+
const problems = [];
|
|
246
|
+
walkLs(data, problems);
|
|
247
|
+
if (!problems.length && !Array.isArray(data.problems)) return null;
|
|
248
|
+
const stats = summary();
|
|
249
|
+
stats.errors = problems.length;
|
|
250
|
+
return report('ls', { summary: stats, problems });
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const parseToolJson = (data, root) => {
|
|
254
|
+
const eslint = parseEslintJson(data, root);
|
|
255
|
+
if (eslint) return [eslint];
|
|
256
|
+
const vulns = data && (data.vulnerabilities || data.advisories);
|
|
257
|
+
const audit = vulns ? parseAudit(JSON.stringify(data)) : null;
|
|
258
|
+
if (audit) return [audit];
|
|
259
|
+
const ls = parseLs(data);
|
|
260
|
+
if (ls) return [ls];
|
|
261
|
+
const outdated = parseOutdated(JSON.stringify(data));
|
|
262
|
+
if (outdated) return [outdated];
|
|
263
|
+
return null;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const parseAuditText = (text) => {
|
|
267
|
+
if (`${text ?? ''}`.trim().startsWith('{')) return null;
|
|
268
|
+
const lines = `${text ?? ''}`.split(/\r?\n/);
|
|
269
|
+
const problems = [];
|
|
270
|
+
for (let index = 0; index < lines.length; index++) {
|
|
271
|
+
const match = /^Severity:\s+(\w+)\s*$/.exec(lines[index].trim());
|
|
272
|
+
if (!match) continue;
|
|
273
|
+
const name = (lines[index - 1] || '').trim();
|
|
274
|
+
problems.push(
|
|
275
|
+
problem({
|
|
276
|
+
severity: match[1].toLowerCase(),
|
|
277
|
+
name,
|
|
278
|
+
message: name,
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (!problems.length) return null;
|
|
283
|
+
const stats = summary();
|
|
284
|
+
stats.errors = problems.length;
|
|
285
|
+
return report('audit', { summary: stats, problems });
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const parseToolText = (text, root) => {
|
|
289
|
+
const reports = [];
|
|
290
|
+
const eslint = parseEslintStylish(text, root);
|
|
291
|
+
if (eslint) reports.push(eslint);
|
|
292
|
+
const tsc = parseTsc(text, root);
|
|
293
|
+
if (tsc) reports.push(tsc);
|
|
294
|
+
const prettier = parsePrettier(text, root);
|
|
295
|
+
if (prettier) reports.push(prettier);
|
|
296
|
+
const audit = parseAuditText(text);
|
|
297
|
+
if (audit) reports.push(audit);
|
|
298
|
+
return reports;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
module.exports = {
|
|
302
|
+
parseEslintJson,
|
|
303
|
+
parseEslintStylish,
|
|
304
|
+
parseTsc,
|
|
305
|
+
parsePrettier,
|
|
306
|
+
parseAudit,
|
|
307
|
+
parseOutdated,
|
|
308
|
+
parseLs,
|
|
309
|
+
parseToolJson,
|
|
310
|
+
parseToolText,
|
|
311
|
+
};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { TODO_FILE } = require('./files.js');
|
|
4
|
+
|
|
5
|
+
const flatText = (text) => `${text ?? ''}`.trim().replace(/\n/g, ' ');
|
|
6
|
+
|
|
7
|
+
const snapTodo = (todo) => ({
|
|
8
|
+
file: todo.file ?? '',
|
|
9
|
+
text: flatText(todo.text),
|
|
10
|
+
done: todo.done === true,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const snapList = (todos) => {
|
|
14
|
+
const list = [];
|
|
15
|
+
for (const todo of todos ?? []) {
|
|
16
|
+
const snap = snapTodo(todo);
|
|
17
|
+
if (!snap.text) continue;
|
|
18
|
+
list.push(snap);
|
|
19
|
+
}
|
|
20
|
+
return list;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const copyMap = (map) => {
|
|
24
|
+
const next = new Map();
|
|
25
|
+
if (!map) return next;
|
|
26
|
+
for (const [key, note] of map) next.set(key, { ...note });
|
|
27
|
+
return next;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const snapshotSide = (store) => ({
|
|
31
|
+
status: store.status,
|
|
32
|
+
todos: snapList(store.todos),
|
|
33
|
+
feedback: copyMap(store.feedback),
|
|
34
|
+
code: copyMap(store.code),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const captureBaseline = (store) => {
|
|
38
|
+
store.baseline = snapshotSide(store);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const claim = (list, used, text, read) => {
|
|
42
|
+
for (let i = 0; i < list.length; i++) {
|
|
43
|
+
if (used[i]) continue;
|
|
44
|
+
if (read(list[i]) !== text) continue;
|
|
45
|
+
used[i] = true;
|
|
46
|
+
return i;
|
|
47
|
+
}
|
|
48
|
+
return -1;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const pickDone = (baseDone, ourDone, theirDone) => {
|
|
52
|
+
if (ourDone === theirDone) return ourDone;
|
|
53
|
+
if (theirDone === baseDone) return ourDone;
|
|
54
|
+
if (ourDone === baseDone) return theirDone;
|
|
55
|
+
return ourDone;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const withDone = (todo, done) => {
|
|
59
|
+
if (todo.done === done) return todo;
|
|
60
|
+
return { id: todo.id, file: todo.file, text: todo.text, done };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const liveText = (todo) => flatText(todo.text);
|
|
64
|
+
|
|
65
|
+
const snapText = (snap) => snap.text;
|
|
66
|
+
|
|
67
|
+
const flags = (n) => new Array(n).fill(false);
|
|
68
|
+
|
|
69
|
+
const adoptTodo = (store, snap) => {
|
|
70
|
+
const id = store.nextTodoId;
|
|
71
|
+
store.nextTodoId += 1;
|
|
72
|
+
const file = snap.file || TODO_FILE;
|
|
73
|
+
return { id, file, text: snap.text, done: snap.done };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const findMerged = (merged, alias, text) => {
|
|
77
|
+
const linked = alias.get(text);
|
|
78
|
+
if (linked) {
|
|
79
|
+
const at = merged.indexOf(linked);
|
|
80
|
+
if (at >= 0) return at;
|
|
81
|
+
}
|
|
82
|
+
for (let i = 0; i < merged.length; i++) {
|
|
83
|
+
if (liveText(merged[i]) === text) return i;
|
|
84
|
+
}
|
|
85
|
+
return -1;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const insertTheir = (store, merged, theirs, theirKeep, alias) => {
|
|
89
|
+
for (let i = 0; i < theirs.length; i++) {
|
|
90
|
+
if (!theirKeep[i]) continue;
|
|
91
|
+
let at = merged.length;
|
|
92
|
+
let placed = false;
|
|
93
|
+
for (let j = i - 1; j >= 0 && !placed; j--) {
|
|
94
|
+
const prev = findMerged(merged, alias, theirs[j].text);
|
|
95
|
+
if (prev < 0) continue;
|
|
96
|
+
at = prev + 1;
|
|
97
|
+
placed = true;
|
|
98
|
+
}
|
|
99
|
+
for (let j = i + 1; j < theirs.length && !placed; j++) {
|
|
100
|
+
const next = findMerged(merged, alias, theirs[j].text);
|
|
101
|
+
if (next < 0) continue;
|
|
102
|
+
at = next;
|
|
103
|
+
placed = true;
|
|
104
|
+
}
|
|
105
|
+
const item = adoptTodo(store, theirs[i]);
|
|
106
|
+
merged.splice(at, 0, item);
|
|
107
|
+
alias.set(theirs[i].text, item);
|
|
108
|
+
}
|
|
109
|
+
return merged;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const keepOurs = (ours, kept, alias, index, text) => {
|
|
113
|
+
kept[index] = true;
|
|
114
|
+
alias.set(text, ours[index]);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const mergeBaseTodo = (
|
|
118
|
+
base,
|
|
119
|
+
ours,
|
|
120
|
+
theirs,
|
|
121
|
+
used,
|
|
122
|
+
kept,
|
|
123
|
+
dropped,
|
|
124
|
+
theirKeep,
|
|
125
|
+
alias,
|
|
126
|
+
) => {
|
|
127
|
+
const text = base.text;
|
|
128
|
+
const oi = claim(ours, used.ours, text, liveText);
|
|
129
|
+
const ti = claim(theirs, used.theirs, text, snapText);
|
|
130
|
+
if (oi >= 0 && ti >= 0) {
|
|
131
|
+
const done = pickDone(base.done, ours[oi].done === true, theirs[ti].done);
|
|
132
|
+
ours[oi] = withDone(ours[oi], done);
|
|
133
|
+
keepOurs(ours, kept, alias, oi, text);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (oi >= 0) {
|
|
137
|
+
if (ours[oi].done === base.done) {
|
|
138
|
+
dropped[oi] = true;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
keepOurs(ours, kept, alias, oi, text);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (ti >= 0 && theirs[ti].done !== base.done) theirKeep[ti] = true;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const replaceTodo = (todo, snap) => ({
|
|
148
|
+
id: todo.id,
|
|
149
|
+
file: todo.file,
|
|
150
|
+
text: snap.text,
|
|
151
|
+
done: snap.done,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const fillGaps = (merged, gaps, theirs, theirKeep, alias) => {
|
|
155
|
+
let filled = 0;
|
|
156
|
+
for (let i = 0; i < theirs.length && filled < gaps.length; i++) {
|
|
157
|
+
if (!theirKeep[i]) continue;
|
|
158
|
+
const gap = gaps[filled];
|
|
159
|
+
const item = replaceTodo(gap.todo, theirs[i]);
|
|
160
|
+
merged.splice(gap.at, 0, item);
|
|
161
|
+
alias.set(theirs[i].text, item);
|
|
162
|
+
theirKeep[i] = false;
|
|
163
|
+
filled += 1;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const mergeFreshTodo = (ours, theirs, used, kept, alias, index) => {
|
|
168
|
+
const text = liveText(ours[index]);
|
|
169
|
+
const ti = claim(theirs, used.theirs, text, snapText);
|
|
170
|
+
if (ti < 0) {
|
|
171
|
+
kept[index] = true;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const done = ours[index].done === true || theirs[ti].done === true;
|
|
175
|
+
ours[index] = withDone(ours[index], done);
|
|
176
|
+
keepOurs(ours, kept, alias, index, text);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const mergeTodos = (store, base, theirs) => {
|
|
180
|
+
const ours = [];
|
|
181
|
+
for (const todo of store.todos) {
|
|
182
|
+
if (!liveText(todo)) continue;
|
|
183
|
+
ours.push(todo);
|
|
184
|
+
}
|
|
185
|
+
const used = { ours: flags(ours.length), theirs: flags(theirs.length) };
|
|
186
|
+
const kept = flags(ours.length);
|
|
187
|
+
const dropped = flags(ours.length);
|
|
188
|
+
const theirKeep = flags(theirs.length);
|
|
189
|
+
const alias = new Map();
|
|
190
|
+
for (const item of base) {
|
|
191
|
+
mergeBaseTodo(item, ours, theirs, used, kept, dropped, theirKeep, alias);
|
|
192
|
+
}
|
|
193
|
+
for (let i = 0; i < ours.length; i++) {
|
|
194
|
+
if (used.ours[i]) continue;
|
|
195
|
+
mergeFreshTodo(ours, theirs, used, kept, alias, i);
|
|
196
|
+
}
|
|
197
|
+
for (let i = 0; i < theirs.length; i++) {
|
|
198
|
+
if (!used.theirs[i]) theirKeep[i] = true;
|
|
199
|
+
}
|
|
200
|
+
const merged = [];
|
|
201
|
+
const gaps = [];
|
|
202
|
+
let cursor = 0;
|
|
203
|
+
for (let i = 0; i < ours.length; i++) {
|
|
204
|
+
if (kept[i]) {
|
|
205
|
+
merged.push(ours[i]);
|
|
206
|
+
cursor += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (!dropped[i]) continue;
|
|
210
|
+
gaps.push({ at: cursor, todo: ours[i] });
|
|
211
|
+
cursor += 1;
|
|
212
|
+
}
|
|
213
|
+
fillGaps(merged, gaps, theirs, theirKeep, alias);
|
|
214
|
+
store.todos = insertTheir(store, merged, theirs, theirKeep, alias);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const sameNote = (left, right) =>
|
|
218
|
+
left.text === right.text && !!left.done === !!right.done;
|
|
219
|
+
|
|
220
|
+
const cloneNote = (note, done) => {
|
|
221
|
+
if (!!note.done === done) return note;
|
|
222
|
+
return { ...note, done };
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const pickNote = (ours, base, theirs) => {
|
|
226
|
+
if (ours && theirs && ours.text === theirs.text) {
|
|
227
|
+
const baseDone = base ? base.done === true : ours.done === true;
|
|
228
|
+
const done = pickDone(baseDone, ours.done === true, theirs.done === true);
|
|
229
|
+
return cloneNote(ours, done);
|
|
230
|
+
}
|
|
231
|
+
if (!base) return ours || theirs || null;
|
|
232
|
+
if (!ours) {
|
|
233
|
+
if (!theirs || sameNote(theirs, base)) return null;
|
|
234
|
+
return theirs;
|
|
235
|
+
}
|
|
236
|
+
if (!theirs) {
|
|
237
|
+
if (sameNote(ours, base)) return null;
|
|
238
|
+
return ours;
|
|
239
|
+
}
|
|
240
|
+
if (sameNote(theirs, base)) return ours;
|
|
241
|
+
if (sameNote(ours, base)) return theirs;
|
|
242
|
+
return ours;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const mergeMap = (ours, base, theirs) => {
|
|
246
|
+
const keys = new Set();
|
|
247
|
+
for (const key of ours.keys()) keys.add(key);
|
|
248
|
+
for (const key of base.keys()) keys.add(key);
|
|
249
|
+
for (const key of theirs.keys()) keys.add(key);
|
|
250
|
+
for (const key of keys) {
|
|
251
|
+
const next = pickNote(ours.get(key), base.get(key), theirs.get(key));
|
|
252
|
+
if (!next) ours.delete(key);
|
|
253
|
+
else ours.set(key, next);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const mergeReview = (store, disk) => {
|
|
258
|
+
const base = store.baseline;
|
|
259
|
+
const baseTodos = base ? base.todos : [];
|
|
260
|
+
const baseStatus = base ? base.status : store.status;
|
|
261
|
+
mergeTodos(store, baseTodos, snapList(disk.todos));
|
|
262
|
+
const feedback = base ? base.feedback : new Map();
|
|
263
|
+
const code = base ? base.code : new Map();
|
|
264
|
+
mergeMap(store.feedback, feedback, disk.feedback);
|
|
265
|
+
mergeMap(store.code, code, disk.code);
|
|
266
|
+
if (store.status === baseStatus && disk.status) store.status = disk.status;
|
|
267
|
+
store.baseline = snapshotSide(disk);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
module.exports = { captureBaseline, mergeReview };
|
package/lib/review.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
6
|
const { TODO_FILE, REVIEW_DIR } = require('./files.js');
|
|
7
|
+
const { captureBaseline, mergeReview } = require('./review-merge.js');
|
|
7
8
|
|
|
8
9
|
const createStore = (reviewPath, templates = []) => ({
|
|
9
10
|
reviewPath,
|
|
@@ -14,6 +15,7 @@ const createStore = (reviewPath, templates = []) => ({
|
|
|
14
15
|
todos: [],
|
|
15
16
|
templates: [...templates],
|
|
16
17
|
status: 'editing',
|
|
18
|
+
baseline: null,
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
const noteCounts = (store) => {
|
|
@@ -119,6 +121,15 @@ const setTodoText = (store, id, text) => {
|
|
|
119
121
|
store.dirty = true;
|
|
120
122
|
};
|
|
121
123
|
|
|
124
|
+
const setTodoDone = (store, id, done) => {
|
|
125
|
+
const i = store.todos.findIndex((todo) => todo.id === id);
|
|
126
|
+
if (i < 0) return;
|
|
127
|
+
const prev = store.todos[i];
|
|
128
|
+
if (prev.done === done) return;
|
|
129
|
+
store.todos[i] = { id: prev.id, file: prev.file, text: prev.text, done };
|
|
130
|
+
store.dirty = true;
|
|
131
|
+
};
|
|
132
|
+
|
|
122
133
|
const checkLabel = (text, done) => {
|
|
123
134
|
const mark = done ? 'x' : ' ';
|
|
124
135
|
return `[${mark}] ${text}`;
|
|
@@ -222,12 +233,13 @@ const SECTION_LINE = {
|
|
|
222
233
|
};
|
|
223
234
|
|
|
224
235
|
const AGENT_INSTRUCTIONS = [
|
|
225
|
-
'Execute reviews with `status
|
|
226
|
-
'Do not start
|
|
227
|
-
'Work through unchecked items. After finishing one, mark it `[x]
|
|
228
|
-
'For `code` items, replace added lines at that location
|
|
229
|
-
'
|
|
230
|
-
|
|
236
|
+
'- Execute reviews with `status` `ready, partial, editing`',
|
|
237
|
+
'- Do not start or change `done` review files',
|
|
238
|
+
'- Work through unchecked items. After finishing one, mark it `[x]`',
|
|
239
|
+
'- For `code` items, replace added lines at that location',
|
|
240
|
+
'- If not `editing`, set `status` to `partial` if some remain, ' +
|
|
241
|
+
'or `done` if all are `[x]`',
|
|
242
|
+
'- Do not redo items already marked `[x]`',
|
|
231
243
|
].join('\n');
|
|
232
244
|
|
|
233
245
|
const toInt = (text) => parseInt(text, 10);
|
|
@@ -622,6 +634,7 @@ const parseReview = (text, reviewPath, templates = []) => {
|
|
|
622
634
|
flushPendingCode(builder, pendingCode);
|
|
623
635
|
flushPendingFeedback(builder, pending);
|
|
624
636
|
builder.dirty = false;
|
|
637
|
+
captureBaseline(builder);
|
|
625
638
|
return builder;
|
|
626
639
|
};
|
|
627
640
|
|
|
@@ -733,8 +746,25 @@ const loadTemplates = (dir) => {
|
|
|
733
746
|
}
|
|
734
747
|
};
|
|
735
748
|
|
|
749
|
+
const readReviewText = (reviewPath) => {
|
|
750
|
+
try {
|
|
751
|
+
return fs.readFileSync(reviewPath, 'utf8');
|
|
752
|
+
} catch (error) {
|
|
753
|
+
if (error.code === 'ENOENT') return '';
|
|
754
|
+
throw error;
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const mergeDiskReview = (notes) => {
|
|
759
|
+
const text = readReviewText(notes.reviewPath);
|
|
760
|
+
if (!text.startsWith('---')) return;
|
|
761
|
+
const disk = parseReview(text, notes.reviewPath, notes.templates);
|
|
762
|
+
mergeReview(notes, disk);
|
|
763
|
+
};
|
|
764
|
+
|
|
736
765
|
const flushReview = (notes, force = false) => {
|
|
737
766
|
if (!notes || !notes.dirty) return false;
|
|
767
|
+
mergeDiskReview(notes);
|
|
738
768
|
if (!hasNotes(notes) && !force) {
|
|
739
769
|
notes.dirty = false;
|
|
740
770
|
return false;
|
|
@@ -746,6 +776,7 @@ const flushReview = (notes, force = false) => {
|
|
|
746
776
|
const templatesFile = path.join(top, REVIEW_DIR, TEMPLATES_FILE);
|
|
747
777
|
const body = `${JSON.stringify(notes.templates, null, 2)}\n`;
|
|
748
778
|
fs.writeFileSync(templatesFile, body, 'utf8');
|
|
779
|
+
captureBaseline(notes);
|
|
749
780
|
notes.dirty = false;
|
|
750
781
|
return true;
|
|
751
782
|
};
|
|
@@ -770,6 +801,7 @@ module.exports = {
|
|
|
770
801
|
addTodo,
|
|
771
802
|
removeTodo,
|
|
772
803
|
setTodoText,
|
|
804
|
+
setTodoDone,
|
|
773
805
|
checkLabel,
|
|
774
806
|
feedbackKey,
|
|
775
807
|
applyImportedNotes,
|