reslop 0.1.9 → 0.2.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/CHANGELOG.md +12 -1
- package/README.md +2 -1
- package/bin/reslop.js +2 -2
- package/lib/ansi.js +4 -2
- package/lib/api.js +2 -2
- package/lib/capabilities.js +7 -14
- package/lib/cli.js +28 -47
- package/lib/clipboard.js +1 -1
- package/lib/{deps/changes.js → deps-diff.js} +309 -19
- package/lib/deps.js +498 -29
- package/lib/detect.js +1 -7
- package/lib/diff/align.js +7 -10
- package/lib/diff/diff.js +53 -0
- package/lib/diff/display.js +5 -4
- package/lib/diff/inline.js +26 -21
- package/lib/diff/items.js +6 -4
- package/lib/diff/patch.js +1 -3
- package/lib/diff/unit.js +392 -0
- package/lib/files.js +5 -8
- package/lib/{git/branches.js → git-branches.js} +6 -5
- package/lib/{git/dependencies.js → git-deps.js} +111 -38
- package/lib/git.js +589 -68
- package/lib/github.js +28 -197
- package/lib/gitlab.js +24 -195
- package/lib/highlight/bash.js +1 -3
- package/lib/highlight/css.js +1 -3
- package/lib/highlight/js.js +11 -14
- package/lib/highlight.js +6 -9
- package/lib/keys.js +14 -8
- package/lib/manifest.js +330 -0
- package/lib/remote.js +374 -0
- package/lib/render/branches.js +14 -68
- package/lib/render/chrome.js +24 -9
- package/lib/render/commits.js +6 -59
- package/lib/render/diff.js +16 -16
- package/lib/render/files.js +6 -34
- package/lib/render/frame.js +23 -12
- package/lib/render/notes.js +4 -3
- package/lib/render/primitives.js +45 -0
- package/lib/{render.js → render/render.js} +5 -5
- package/lib/render/unit.js +41 -0
- package/lib/review.js +750 -14
- package/lib/select.js +5 -10
- package/lib/session/accessors.js +9 -1
- package/lib/session/actions.js +32 -16
- package/lib/session/branches.js +116 -128
- package/lib/session/change-actions.js +202 -123
- package/lib/session/commit.js +105 -112
- package/lib/session/compose.js +158 -13
- package/lib/session/dispatch.js +104 -60
- package/lib/session/files-pane.js +142 -148
- package/lib/session/items.js +22 -30
- package/lib/session/lifecycle.js +44 -44
- package/lib/session/load.js +19 -33
- package/lib/session/navigation.js +12 -1
- package/lib/session/ops.js +28 -41
- package/lib/session/pointer.js +54 -57
- package/lib/session/review.js +23 -37
- package/lib/session/terminal.js +6 -12
- package/lib/session/todos.js +8 -12
- package/lib/session/unit.js +208 -0
- package/lib/session/update.js +33 -46
- package/lib/session/view.js +78 -9
- package/lib/session/watch.js +11 -19
- package/lib/session.js +21 -246
- package/lib/source.js +3 -3
- package/lib/update.js +40 -75
- package/lib/{git/proc.js → utilities.js} +75 -11
- package/lib/wrap.js +1 -3
- package/package.json +1 -1
- package/lib/deps/items.js +0 -134
- package/lib/deps/manifest.js +0 -152
- package/lib/deps/merge.js +0 -120
- package/lib/deps/proposals.js +0 -164
- package/lib/deps/reports.js +0 -180
- package/lib/deps/rows.js +0 -176
- package/lib/deps/scan.js +0 -138
- package/lib/deps/usage.js +0 -68
- package/lib/deps/versions.js +0 -119
- package/lib/diff.js +0 -33
- package/lib/git/changes.js +0 -164
- package/lib/git/read.js +0 -253
- package/lib/git/run.js +0 -100
- package/lib/npm/commands.js +0 -100
- package/lib/remote/changes.js +0 -37
- package/lib/remote/headers.js +0 -36
- package/lib/remote/http.js +0 -122
- package/lib/remote/notes.js +0 -94
- package/lib/review/format.js +0 -434
- package/lib/review/storage.js +0 -150
- package/lib/review/store.js +0 -178
- package/lib/review/templates.js +0 -49
- package/lib/session/origins.js +0 -7
- package/lib/sys.js +0 -84
package/lib/remote/headers.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const headerValue = (headers, name) => {
|
|
4
|
-
if (!headers) return '';
|
|
5
|
-
if (typeof headers.get === 'function') {
|
|
6
|
-
return headers.get(name) || '';
|
|
7
|
-
}
|
|
8
|
-
const wanted = name.toLowerCase();
|
|
9
|
-
for (const key of Object.keys(headers)) {
|
|
10
|
-
const value = headers[key];
|
|
11
|
-
if (key.toLowerCase() === wanted) return `${value}`;
|
|
12
|
-
}
|
|
13
|
-
return '';
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const parseLinkNext = (headers) => {
|
|
17
|
-
const link = headerValue(headers, 'link');
|
|
18
|
-
if (!link) return '';
|
|
19
|
-
for (const part of link.split(',')) {
|
|
20
|
-
const match = /<([^>]+)>\s*;\s*rel="?next"?/i.exec(part.trim());
|
|
21
|
-
if (match) return match[1];
|
|
22
|
-
}
|
|
23
|
-
return '';
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const parseNextPage = (headers, currentUrl) => {
|
|
27
|
-
const fromLink = parseLinkNext(headers);
|
|
28
|
-
if (fromLink) return fromLink;
|
|
29
|
-
const page = headerValue(headers, 'x-next-page');
|
|
30
|
-
if (!page) return '';
|
|
31
|
-
const url = new URL(currentUrl);
|
|
32
|
-
url.searchParams.set('page', page);
|
|
33
|
-
return url.toString();
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
module.exports = { headerValue, parseLinkNext, parseNextPage };
|
package/lib/remote/http.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { delay, isHashObject } = require('metautil');
|
|
4
|
-
|
|
5
|
-
const RETRY_ATTEMPTS = 3;
|
|
6
|
-
const RETRY_DELAY_MS = 200;
|
|
7
|
-
const TRANSIENT_STATUS = [429, 502, 503, 504];
|
|
8
|
-
|
|
9
|
-
const isRecord = isHashObject;
|
|
10
|
-
|
|
11
|
-
const oneLine = (text) => {
|
|
12
|
-
const value = `${text ?? ''}`.trim();
|
|
13
|
-
if (!value) return '';
|
|
14
|
-
return value.split('\n')[0];
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const abortError = (signal) => {
|
|
18
|
-
if (signal && signal.reason !== undefined) return signal.reason;
|
|
19
|
-
const error = new Error('This operation was aborted');
|
|
20
|
-
error.name = 'AbortError';
|
|
21
|
-
return error;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const isAbortError = (error, signal) => {
|
|
25
|
-
if (signal && signal.aborted) return true;
|
|
26
|
-
if (!error || typeof error !== 'object') return false;
|
|
27
|
-
if (error.name === 'AbortError') return true;
|
|
28
|
-
if (error.code === 'ABORT') return true;
|
|
29
|
-
return false;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const throwIfAborted = (signal) => {
|
|
33
|
-
if (signal && signal.aborted) throw abortError(signal);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const createTransport = (deps = {}) => {
|
|
37
|
-
const fetchImpl = deps.fetch;
|
|
38
|
-
const rawDelay = deps.delay ?? delay;
|
|
39
|
-
const delayFn = async (ms, abortSignal) => {
|
|
40
|
-
try {
|
|
41
|
-
await rawDelay(ms, abortSignal);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
throwIfAborted(abortSignal);
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
const signal = deps.signal;
|
|
48
|
-
|
|
49
|
-
const retry = async (run, options = {}) => {
|
|
50
|
-
const attempts = options.attempts ?? RETRY_ATTEMPTS;
|
|
51
|
-
const delayMs = options.delayMs ?? RETRY_DELAY_MS;
|
|
52
|
-
const isRetryable = options.isRetryable;
|
|
53
|
-
for (let i = 0; ; i++) {
|
|
54
|
-
try {
|
|
55
|
-
return await run();
|
|
56
|
-
} catch (error) {
|
|
57
|
-
if (isAbortError(error, signal)) throw error;
|
|
58
|
-
const isLast = i === attempts - 1;
|
|
59
|
-
if (isLast || !isRetryable(error)) throw error;
|
|
60
|
-
throwIfAborted(signal);
|
|
61
|
-
await delayFn(delayMs * (i + 1), signal);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const request = async (url, options = {}) => {
|
|
67
|
-
throwIfAborted(signal);
|
|
68
|
-
if (typeof fetchImpl !== 'function') {
|
|
69
|
-
throw options.missingFetch();
|
|
70
|
-
}
|
|
71
|
-
const headers = options.headers;
|
|
72
|
-
const method = options.method || 'GET';
|
|
73
|
-
const body = options.body;
|
|
74
|
-
const wrapNetwork = options.wrapNetwork;
|
|
75
|
-
const toHttpError = options.toHttpError;
|
|
76
|
-
const retryOpts = options.retry ?? {};
|
|
77
|
-
return retry(
|
|
78
|
-
async () => {
|
|
79
|
-
let response;
|
|
80
|
-
try {
|
|
81
|
-
const init = { method, headers };
|
|
82
|
-
if (body !== undefined) init.body = body;
|
|
83
|
-
if (signal) init.signal = signal;
|
|
84
|
-
response = await fetchImpl(url, init);
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (isAbortError(error, signal)) throw error;
|
|
87
|
-
throw wrapNetwork(error);
|
|
88
|
-
}
|
|
89
|
-
const text = await response.text();
|
|
90
|
-
if (!response.ok) throw toHttpError(response.status, text);
|
|
91
|
-
return { body: text, headers: response.headers };
|
|
92
|
-
},
|
|
93
|
-
{ ...retryOpts, isRetryable: options.isRetryable },
|
|
94
|
-
);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const listPages = async (url, options = {}) => {
|
|
98
|
-
const items = [];
|
|
99
|
-
let next = url;
|
|
100
|
-
const parsePage = options.parsePage;
|
|
101
|
-
const nextPage = options.nextPage;
|
|
102
|
-
while (next) {
|
|
103
|
-
const page = await request(next, options);
|
|
104
|
-
const entries = parsePage(page.body);
|
|
105
|
-
for (const entry of entries) items.push(entry);
|
|
106
|
-
next = nextPage(page.headers, next);
|
|
107
|
-
}
|
|
108
|
-
return items;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
return { retry, request, listPages };
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
module.exports = {
|
|
115
|
-
RETRY_ATTEMPTS,
|
|
116
|
-
RETRY_DELAY_MS,
|
|
117
|
-
TRANSIENT_STATUS,
|
|
118
|
-
isRecord,
|
|
119
|
-
oneLine,
|
|
120
|
-
isAbortError,
|
|
121
|
-
createTransport,
|
|
122
|
-
};
|
package/lib/remote/notes.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { isCtxType } = require('../diff.js');
|
|
4
|
-
const { itemPath } = require('../files.js');
|
|
5
|
-
|
|
6
|
-
const sideLineNumber = (type, oldNo, newNo, side) => {
|
|
7
|
-
if (side === 'LEFT') {
|
|
8
|
-
if (type === 'add') return 0;
|
|
9
|
-
return oldNo;
|
|
10
|
-
}
|
|
11
|
-
if (type === 'del') return 0;
|
|
12
|
-
return newNo;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const locateLineInItem = (item, rel, line, side) => {
|
|
16
|
-
const path = itemPath(item);
|
|
17
|
-
if (path !== rel) return null;
|
|
18
|
-
const hunk = item.hunk;
|
|
19
|
-
if (!hunk || !line) return null;
|
|
20
|
-
let oldNo = hunk.oldStart;
|
|
21
|
-
let newNo = hunk.newStart;
|
|
22
|
-
for (const entry of hunk.lines) {
|
|
23
|
-
const at = sideLineNumber(entry.type, oldNo, newNo, side);
|
|
24
|
-
if (at === line) {
|
|
25
|
-
const inBlock = entry.blockId === item.blockId;
|
|
26
|
-
return { inBlock };
|
|
27
|
-
}
|
|
28
|
-
if (isCtxType(entry.type) || entry.type === 'del') {
|
|
29
|
-
oldNo += 1;
|
|
30
|
-
}
|
|
31
|
-
if (isCtxType(entry.type) || entry.type === 'add') {
|
|
32
|
-
newNo += 1;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const findItemForLine = (items, rel, line, side) => {
|
|
39
|
-
let fallback = null;
|
|
40
|
-
for (const item of items) {
|
|
41
|
-
const hit = locateLineInItem(item, rel, line, side);
|
|
42
|
-
if (!hit) continue;
|
|
43
|
-
if (hit.inBlock) return item;
|
|
44
|
-
if (!fallback) fallback = item;
|
|
45
|
-
}
|
|
46
|
-
return fallback;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const importedText = (entry, host) => {
|
|
50
|
-
const reviewer = entry.reviewer || 'unknown';
|
|
51
|
-
const body = `${entry.body ?? ''}`.trim();
|
|
52
|
-
return `@${reviewer} review at ${host}: ${body}`;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const importedTodo = (file, entry, host) => ({
|
|
56
|
-
kind: 'todo',
|
|
57
|
-
file,
|
|
58
|
-
text: importedText(entry, host),
|
|
59
|
-
done: entry.resolved === true,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const importedFeedback = (item, path, entry, host) => {
|
|
63
|
-
const hunk = item.hunk;
|
|
64
|
-
return {
|
|
65
|
-
kind: 'feedback',
|
|
66
|
-
file: itemPath(item) || path,
|
|
67
|
-
oldStart: hunk ? hunk.oldStart : 0,
|
|
68
|
-
newStart: hunk ? hunk.newStart : 0,
|
|
69
|
-
blockId: item.blockId ?? 0,
|
|
70
|
-
origin: item.origin ?? 'pr',
|
|
71
|
-
header: hunk ? hunk.header : '',
|
|
72
|
-
text: importedText(entry, host),
|
|
73
|
-
done: entry.resolved === true,
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const noteFromLocation = (items, loc, entry, host, fallbackFile) => {
|
|
78
|
-
const path = loc.path || '';
|
|
79
|
-
const line = loc.line;
|
|
80
|
-
const side = loc.side;
|
|
81
|
-
if (line && path) {
|
|
82
|
-
const item = findItemForLine(items, path, line, side);
|
|
83
|
-
if (item) return importedFeedback(item, path, entry, host);
|
|
84
|
-
}
|
|
85
|
-
if (path) return importedTodo(path, entry, host);
|
|
86
|
-
return importedTodo(fallbackFile, entry, host);
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
module.exports = {
|
|
90
|
-
findItemForLine,
|
|
91
|
-
importedText,
|
|
92
|
-
importedTodo,
|
|
93
|
-
noteFromLocation,
|
|
94
|
-
};
|
package/lib/review/format.js
DELETED
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
|
|
5
|
-
const { TODO_FILE } = require('../files.js');
|
|
6
|
-
const store = require('./store.js');
|
|
7
|
-
const { createStore, feedbackKey } = store;
|
|
8
|
-
|
|
9
|
-
const REVIEW_STATUSES = ['editing', 'ready', 'partial', 'done'];
|
|
10
|
-
const FEED_HEAD = /^### Feedback `([^`]+)` \+(\d+) \((.*)\)\s*$/;
|
|
11
|
-
const FEED_BLOCK = /^(.*), block (\d+)$/;
|
|
12
|
-
const FEED_BLOCK_ONLY = /^block (\d+)$/;
|
|
13
|
-
const FEED_META = /^<!-- reslop:(.*) -->\s*$/;
|
|
14
|
-
const FEED_SUFFIX = /^(.*) - (.+):(\d+):(\d+):(\d+)$/;
|
|
15
|
-
const CHECK_ITEM = /^- \[([ xX])\] (.*)$/;
|
|
16
|
-
const CODE_HEAD = /^- \[([ xX])\] code `([^`]+)`\s*$/;
|
|
17
|
-
const FENCE_LINE = /^(`{3,})\s*$/;
|
|
18
|
-
const FILE_QUOTE = /^> (.+)$/;
|
|
19
|
-
const STATUS_LINE = /^status:\s*(\S+)\s*$/;
|
|
20
|
-
const GIT_ORIGINS = ['unstaged', 'staged', 'untracked', 'commit', 'pr'];
|
|
21
|
-
const SECTION_LINE = {
|
|
22
|
-
'### Todo': 'todo',
|
|
23
|
-
'### Feedback': 'feedback',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const AGENT_INSTRUCTIONS = [
|
|
27
|
-
'Execute reviews with `status: ready` or `status: partial`.',
|
|
28
|
-
'Do not start `editing` or `done` review files.',
|
|
29
|
-
'Work through unchecked items. After finishing one, mark it `[x]`.',
|
|
30
|
-
'For `code` items, replace added lines at that location with the fence.',
|
|
31
|
-
'Set `status` to `partial` if some remain, or `done` if all are `[x]`.',
|
|
32
|
-
'Do not redo items already marked `[x]`.',
|
|
33
|
-
].join('\n');
|
|
34
|
-
|
|
35
|
-
const toInt = (text) => parseInt(text, 10);
|
|
36
|
-
|
|
37
|
-
const canonicalStatus = (raw) => {
|
|
38
|
-
if (!raw) return '';
|
|
39
|
-
const name = raw === 'pending' ? 'ready' : raw;
|
|
40
|
-
if (REVIEW_STATUSES.includes(name)) return name;
|
|
41
|
-
return '';
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const parseFrontmatterStatus = (text) => {
|
|
45
|
-
if (!text.startsWith('---')) return '';
|
|
46
|
-
const nl = text.indexOf('\n');
|
|
47
|
-
if (nl < 0) return '';
|
|
48
|
-
const rest = text.slice(nl + 1);
|
|
49
|
-
const end = rest.indexOf('\n---');
|
|
50
|
-
if (end < 0) return '';
|
|
51
|
-
const matter = rest.slice(0, end);
|
|
52
|
-
for (const line of matter.split('\n')) {
|
|
53
|
-
const match = STATUS_LINE.exec(line);
|
|
54
|
-
if (match) return canonicalStatus(match[1]);
|
|
55
|
-
}
|
|
56
|
-
return '';
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const isDoneMark = (mark) => mark === 'x' || mark === 'X';
|
|
60
|
-
|
|
61
|
-
const pushTodo = (builder, file, text, done) => {
|
|
62
|
-
const id = builder.nextTodoId;
|
|
63
|
-
builder.nextTodoId += 1;
|
|
64
|
-
builder.todos.push({ id, file, text, done });
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const checkboxLine = (text, done) => {
|
|
68
|
-
const flat = text.trim().replace(/\n/g, ' ');
|
|
69
|
-
const mark = done ? 'x' : ' ';
|
|
70
|
-
return `- [${mark}] ${flat}`;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const storeFeedback = (builder, note, key) => {
|
|
74
|
-
const next = {
|
|
75
|
-
file: note.file,
|
|
76
|
-
oldStart: note.oldStart ?? 0,
|
|
77
|
-
newStart: note.newStart ?? 0,
|
|
78
|
-
blockId: note.blockId ?? 0,
|
|
79
|
-
origin: note.origin ?? '',
|
|
80
|
-
header: note.header ?? '',
|
|
81
|
-
text: note.text,
|
|
82
|
-
done: note.done ?? false,
|
|
83
|
-
};
|
|
84
|
-
builder.feedback.set(key || feedbackKey(next), next);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const storeCode = (builder, note, key) => {
|
|
88
|
-
const next = {
|
|
89
|
-
file: note.file,
|
|
90
|
-
oldStart: note.oldStart ?? 0,
|
|
91
|
-
newStart: note.newStart ?? 0,
|
|
92
|
-
blockId: note.blockId ?? 0,
|
|
93
|
-
origin: note.origin ?? '',
|
|
94
|
-
header: note.header ?? '',
|
|
95
|
-
text: note.text,
|
|
96
|
-
done: note.done ?? false,
|
|
97
|
-
};
|
|
98
|
-
builder.code.set(key || feedbackKey(next), next);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const feedbackLine = (note) => {
|
|
102
|
-
const loc = feedbackKey(note);
|
|
103
|
-
return `${checkboxLine(note.text, note.done)} - ${loc}`;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const parseLoc = (raw) => {
|
|
107
|
-
const parts = raw.split(':');
|
|
108
|
-
const n = parts.length;
|
|
109
|
-
if (n < 4) return null;
|
|
110
|
-
const blockId = toInt(parts[n - 1]);
|
|
111
|
-
const newStart = toInt(parts[n - 2]);
|
|
112
|
-
const oldStart = toInt(parts[n - 3]);
|
|
113
|
-
if (!Number.isFinite(blockId + newStart + oldStart)) return null;
|
|
114
|
-
let fileParts = parts.slice(0, n - 3);
|
|
115
|
-
let origin = '';
|
|
116
|
-
if (fileParts.length > 1 && GIT_ORIGINS.includes(fileParts[0])) {
|
|
117
|
-
origin = fileParts[0];
|
|
118
|
-
fileParts = fileParts.slice(1);
|
|
119
|
-
}
|
|
120
|
-
if (!fileParts.length) return null;
|
|
121
|
-
const file = fileParts.join(':');
|
|
122
|
-
return { origin, file, oldStart, newStart, blockId };
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const parseFeedSuffix = (textItem) => {
|
|
126
|
-
const match = FEED_SUFFIX.exec(textItem);
|
|
127
|
-
if (!match) return null;
|
|
128
|
-
const text = match[1];
|
|
129
|
-
const file = match[2];
|
|
130
|
-
const oldStart = toInt(match[3]);
|
|
131
|
-
const newStart = toInt(match[4]);
|
|
132
|
-
const blockId = toInt(match[5]);
|
|
133
|
-
return { text, file, oldStart, newStart, blockId };
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const fileGroup = (groups, file) => {
|
|
137
|
-
let group = groups.get(file);
|
|
138
|
-
if (!group) {
|
|
139
|
-
group = { todos: [], feedback: [], code: [] };
|
|
140
|
-
groups.set(file, group);
|
|
141
|
-
}
|
|
142
|
-
return group;
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const groupReviewNotes = (notes) => {
|
|
146
|
-
const groups = new Map();
|
|
147
|
-
for (const todo of notes.todos) {
|
|
148
|
-
if (!todo.text.trim()) continue;
|
|
149
|
-
fileGroup(groups, TODO_FILE).todos.push(todo);
|
|
150
|
-
}
|
|
151
|
-
for (const key of notes.feedback.keys()) {
|
|
152
|
-
const note = notes.feedback.get(key);
|
|
153
|
-
if (!note.text.trim()) continue;
|
|
154
|
-
fileGroup(groups, note.file).feedback.push({ key, note });
|
|
155
|
-
}
|
|
156
|
-
for (const key of notes.code.keys()) {
|
|
157
|
-
const note = notes.code.get(key);
|
|
158
|
-
fileGroup(groups, note.file).code.push({ key, note });
|
|
159
|
-
}
|
|
160
|
-
return groups;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const reviewHeader = (notes) => {
|
|
164
|
-
const name = path.basename(notes.reviewPath, '.md');
|
|
165
|
-
const status = canonicalStatus(notes.status) || 'editing';
|
|
166
|
-
return [
|
|
167
|
-
'---',
|
|
168
|
-
`status: ${status}`,
|
|
169
|
-
'---',
|
|
170
|
-
'',
|
|
171
|
-
`# reslop review ${name}`,
|
|
172
|
-
'',
|
|
173
|
-
'## Agent instructions',
|
|
174
|
-
'',
|
|
175
|
-
AGENT_INSTRUCTIONS,
|
|
176
|
-
'',
|
|
177
|
-
];
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const fenceFor = (text) => {
|
|
181
|
-
let n = 3;
|
|
182
|
-
for (const line of text.split('\n')) {
|
|
183
|
-
const match = /^(`{3,})/.exec(line);
|
|
184
|
-
if (!match) continue;
|
|
185
|
-
if (match[1].length >= n) n = match[1].length + 1;
|
|
186
|
-
}
|
|
187
|
-
return '`'.repeat(n);
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const emitCodeItem = (lines, note) => {
|
|
191
|
-
const loc = feedbackKey(note);
|
|
192
|
-
const mark = note.done ? 'x' : ' ';
|
|
193
|
-
const fence = fenceFor(note.text);
|
|
194
|
-
lines.push(`- [${mark}] code \`${loc}\``);
|
|
195
|
-
lines.push(fence);
|
|
196
|
-
if (note.text !== '') lines.push(note.text);
|
|
197
|
-
lines.push(fence);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const emitReviewGroups = (lines, groups) => {
|
|
201
|
-
for (const file of groups.keys()) {
|
|
202
|
-
const group = groups.get(file);
|
|
203
|
-
lines.push(`## ${file}`);
|
|
204
|
-
lines.push('');
|
|
205
|
-
for (const todo of group.todos) {
|
|
206
|
-
lines.push(checkboxLine(todo.text, todo.done));
|
|
207
|
-
}
|
|
208
|
-
for (const item of group.feedback) {
|
|
209
|
-
lines.push(feedbackLine(item.note));
|
|
210
|
-
}
|
|
211
|
-
for (const item of group.code) {
|
|
212
|
-
emitCodeItem(lines, item.note);
|
|
213
|
-
}
|
|
214
|
-
lines.push('');
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
const serializeReview = (notes) => {
|
|
219
|
-
const lines = reviewHeader(notes);
|
|
220
|
-
emitReviewGroups(lines, groupReviewNotes(notes));
|
|
221
|
-
return `${lines.join('\n')}\n`;
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
const parseFeedbackHeading = (line) => {
|
|
225
|
-
const match = FEED_HEAD.exec(line);
|
|
226
|
-
if (!match) return null;
|
|
227
|
-
const file = match[1];
|
|
228
|
-
const newStart = toInt(match[2]);
|
|
229
|
-
const rest = match[3];
|
|
230
|
-
const only = FEED_BLOCK_ONLY.exec(rest);
|
|
231
|
-
if (only) {
|
|
232
|
-
return { file, newStart, header: '', blockId: toInt(only[1]) };
|
|
233
|
-
}
|
|
234
|
-
const withHead = FEED_BLOCK.exec(rest);
|
|
235
|
-
if (!withHead) {
|
|
236
|
-
return { file, newStart, header: rest, blockId: 0 };
|
|
237
|
-
}
|
|
238
|
-
const header = withHead[1];
|
|
239
|
-
const blockId = toInt(withHead[2]);
|
|
240
|
-
return { file, newStart, header, blockId };
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const flushPendingFeedback = (builder, pending) => {
|
|
244
|
-
if (!pending || !(pending.text ?? '').trim()) return null;
|
|
245
|
-
storeFeedback(builder, pending, pending.key);
|
|
246
|
-
return null;
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const headingFile = (line) => {
|
|
250
|
-
const quoted = FILE_QUOTE.exec(line);
|
|
251
|
-
if (quoted) return quoted[1];
|
|
252
|
-
if (line.startsWith('## ') && line !== '## Agent instructions') {
|
|
253
|
-
return line.slice(3);
|
|
254
|
-
}
|
|
255
|
-
return null;
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const startFeedbackPending = (heading, file) => ({
|
|
259
|
-
file: heading.file || file,
|
|
260
|
-
newStart: heading.newStart,
|
|
261
|
-
header: heading.header,
|
|
262
|
-
blockId: heading.blockId,
|
|
263
|
-
oldStart: heading.newStart,
|
|
264
|
-
origin: '',
|
|
265
|
-
key: '',
|
|
266
|
-
text: '',
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const applyFeedMeta = (pending, raw) => {
|
|
270
|
-
const loc = parseLoc(raw);
|
|
271
|
-
if (!loc) return pending;
|
|
272
|
-
pending.origin = loc.origin;
|
|
273
|
-
pending.file = loc.file;
|
|
274
|
-
pending.oldStart = loc.oldStart;
|
|
275
|
-
pending.newStart = loc.newStart;
|
|
276
|
-
pending.blockId = loc.blockId;
|
|
277
|
-
pending.key = feedbackKey(loc);
|
|
278
|
-
return pending;
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
const applyCheckItem = (builder, file, section, pending, textItem, done) => {
|
|
282
|
-
if (section === 'todo' && file) {
|
|
283
|
-
pushTodo(builder, file, textItem, done);
|
|
284
|
-
return pending;
|
|
285
|
-
}
|
|
286
|
-
const parsed = parseFeedSuffix(textItem);
|
|
287
|
-
if (parsed) {
|
|
288
|
-
flushPendingFeedback(builder, pending);
|
|
289
|
-
storeFeedback(builder, {
|
|
290
|
-
file: parsed.file || file,
|
|
291
|
-
oldStart: parsed.oldStart,
|
|
292
|
-
newStart: parsed.newStart,
|
|
293
|
-
blockId: parsed.blockId,
|
|
294
|
-
origin: '',
|
|
295
|
-
header: '',
|
|
296
|
-
text: parsed.text,
|
|
297
|
-
done,
|
|
298
|
-
});
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
if (pending) {
|
|
302
|
-
pending.text = textItem;
|
|
303
|
-
pending.done = done;
|
|
304
|
-
return flushPendingFeedback(builder, pending);
|
|
305
|
-
}
|
|
306
|
-
if (file && section !== 'feedback') {
|
|
307
|
-
pushTodo(builder, file, textItem, done);
|
|
308
|
-
}
|
|
309
|
-
return pending;
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
const applyReviewLine = (builder, file, section, pending, line) => {
|
|
313
|
-
const nextFile = headingFile(line);
|
|
314
|
-
if (nextFile !== null) {
|
|
315
|
-
pending = flushPendingFeedback(builder, pending);
|
|
316
|
-
return { file: nextFile, section: '', pending };
|
|
317
|
-
}
|
|
318
|
-
const nextSection = SECTION_LINE[line];
|
|
319
|
-
if (nextSection) {
|
|
320
|
-
pending = flushPendingFeedback(builder, pending);
|
|
321
|
-
return { file, section: nextSection, pending };
|
|
322
|
-
}
|
|
323
|
-
const heading = parseFeedbackHeading(line);
|
|
324
|
-
if (heading) {
|
|
325
|
-
flushPendingFeedback(builder, pending);
|
|
326
|
-
pending = startFeedbackPending(heading, file);
|
|
327
|
-
return { file, section: 'feedback', pending };
|
|
328
|
-
}
|
|
329
|
-
const meta = FEED_META.exec(line);
|
|
330
|
-
if (meta && pending) {
|
|
331
|
-
pending = applyFeedMeta(pending, meta[1]);
|
|
332
|
-
return { file, section, pending };
|
|
333
|
-
}
|
|
334
|
-
const item = CHECK_ITEM.exec(line);
|
|
335
|
-
if (!item) return { file, section, pending };
|
|
336
|
-
const done = isDoneMark(item[1]);
|
|
337
|
-
pending = applyCheckItem(builder, file, section, pending, item[2], done);
|
|
338
|
-
return { file, section, pending };
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const startCodePending = (line) => {
|
|
342
|
-
const match = CODE_HEAD.exec(line);
|
|
343
|
-
if (!match) return null;
|
|
344
|
-
const loc = parseLoc(match[2]);
|
|
345
|
-
if (!loc) return null;
|
|
346
|
-
return {
|
|
347
|
-
done: isDoneMark(match[1]),
|
|
348
|
-
origin: loc.origin,
|
|
349
|
-
file: loc.file,
|
|
350
|
-
oldStart: loc.oldStart,
|
|
351
|
-
newStart: loc.newStart,
|
|
352
|
-
blockId: loc.blockId,
|
|
353
|
-
open: '',
|
|
354
|
-
lines: [],
|
|
355
|
-
};
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
const storeCodeNote = (builder, pending, text) => {
|
|
359
|
-
storeCode(builder, {
|
|
360
|
-
file: pending.file,
|
|
361
|
-
oldStart: pending.oldStart,
|
|
362
|
-
newStart: pending.newStart,
|
|
363
|
-
blockId: pending.blockId,
|
|
364
|
-
origin: pending.origin,
|
|
365
|
-
header: '',
|
|
366
|
-
text,
|
|
367
|
-
done: pending.done,
|
|
368
|
-
});
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
const flushPendingCode = (builder, pending) => {
|
|
372
|
-
if (!pending) return;
|
|
373
|
-
const text = pending.open ? pending.lines.join('\n') : '';
|
|
374
|
-
storeCodeNote(builder, pending, text);
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const takeCodeLine = (builder, pending, line) => {
|
|
378
|
-
const fence = FENCE_LINE.exec(line);
|
|
379
|
-
if (!pending.open) {
|
|
380
|
-
if (fence) {
|
|
381
|
-
pending.open = fence[1];
|
|
382
|
-
return pending;
|
|
383
|
-
}
|
|
384
|
-
if (!line.trim()) return pending;
|
|
385
|
-
storeCodeNote(builder, pending, '');
|
|
386
|
-
return 'fallthrough';
|
|
387
|
-
}
|
|
388
|
-
if (fence && fence[1].length >= pending.open.length) {
|
|
389
|
-
storeCodeNote(builder, pending, pending.lines.join('\n'));
|
|
390
|
-
return null;
|
|
391
|
-
}
|
|
392
|
-
pending.lines.push(line);
|
|
393
|
-
return pending;
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
const parseReview = (text, reviewPath, templates = []) => {
|
|
397
|
-
const builder = createStore(reviewPath, templates);
|
|
398
|
-
builder.status = parseFrontmatterStatus(text) || 'editing';
|
|
399
|
-
let file = '';
|
|
400
|
-
let section = '';
|
|
401
|
-
let pending = null;
|
|
402
|
-
let pendingCode = null;
|
|
403
|
-
for (const line of text.split('\n')) {
|
|
404
|
-
if (pendingCode) {
|
|
405
|
-
const nextCode = takeCodeLine(builder, pendingCode, line);
|
|
406
|
-
if (nextCode === 'fallthrough') {
|
|
407
|
-
pendingCode = null;
|
|
408
|
-
} else {
|
|
409
|
-
pendingCode = nextCode;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
const started = startCodePending(line);
|
|
414
|
-
if (started) {
|
|
415
|
-
pending = flushPendingFeedback(builder, pending);
|
|
416
|
-
pendingCode = started;
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
const next = applyReviewLine(builder, file, section, pending, line);
|
|
420
|
-
file = next.file;
|
|
421
|
-
section = next.section;
|
|
422
|
-
pending = next.pending;
|
|
423
|
-
}
|
|
424
|
-
flushPendingCode(builder, pendingCode);
|
|
425
|
-
flushPendingFeedback(builder, pending);
|
|
426
|
-
builder.dirty = false;
|
|
427
|
-
return builder;
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
module.exports = {
|
|
431
|
-
parseFrontmatterStatus,
|
|
432
|
-
serializeReview,
|
|
433
|
-
parseReview,
|
|
434
|
-
};
|