@sealant/mend 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -3
- package/dist/dashboard.js +546 -0
- package/dist/main.js +409 -90
- package/dist/review-model.js +212 -0
- package/dist/review-workflow.js +15 -0
- package/dist/review.js +559 -0
- package/dist/shared.js +48 -0
- package/dist/terminal.js +8 -0
- package/dist/tui-theme.js +25 -0
- package/package.json +10 -3
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
const FILETYPE_BY_EXTENSION = {
|
|
3
|
+
c: "c",
|
|
4
|
+
cc: "cpp",
|
|
5
|
+
cpp: "cpp",
|
|
6
|
+
css: "css",
|
|
7
|
+
go: "go",
|
|
8
|
+
h: "c",
|
|
9
|
+
hpp: "cpp",
|
|
10
|
+
html: "html",
|
|
11
|
+
java: "java",
|
|
12
|
+
js: "javascript",
|
|
13
|
+
json: "json",
|
|
14
|
+
jsx: "javascript",
|
|
15
|
+
md: "markdown",
|
|
16
|
+
mjs: "javascript",
|
|
17
|
+
py: "python",
|
|
18
|
+
rb: "ruby",
|
|
19
|
+
rs: "rust",
|
|
20
|
+
sh: "bash",
|
|
21
|
+
sql: "sql",
|
|
22
|
+
swift: "swift",
|
|
23
|
+
toml: "toml",
|
|
24
|
+
ts: "typescript",
|
|
25
|
+
tsx: "tsx",
|
|
26
|
+
yaml: "yaml",
|
|
27
|
+
yml: "yaml",
|
|
28
|
+
zsh: "bash",
|
|
29
|
+
};
|
|
30
|
+
const filetypeOf = (name) => {
|
|
31
|
+
const extension = path.extname(name).slice(1).toLowerCase();
|
|
32
|
+
return extension === "" ? undefined : (FILETYPE_BY_EXTENSION[extension] ?? extension);
|
|
33
|
+
};
|
|
34
|
+
const statusOf = (patch) => {
|
|
35
|
+
if (/^new file mode /m.test(patch))
|
|
36
|
+
return "added";
|
|
37
|
+
if (/^deleted file mode /m.test(patch))
|
|
38
|
+
return "deleted";
|
|
39
|
+
if (/^rename (?:from|to) /m.test(patch))
|
|
40
|
+
return "renamed";
|
|
41
|
+
return "modified";
|
|
42
|
+
};
|
|
43
|
+
const isLikelyGenerated = (name) => /(^|\/)(?:dist|build|generated)(?:\/|$)/i.test(name) ||
|
|
44
|
+
/(?:\.generated\.|\.min\.(?:js|css)$|(?:^|\/)(?:pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$)/i.test(name);
|
|
45
|
+
/** A legible terminal whitespace mode that preserves git's control prefixes. */
|
|
46
|
+
export const visibleWhitespace = (patch) => patch
|
|
47
|
+
.split("\n")
|
|
48
|
+
.map((line) => {
|
|
49
|
+
const hunkContent = (line.startsWith("+") && !line.startsWith("+++")) ||
|
|
50
|
+
(line.startsWith("-") && !line.startsWith("---")) ||
|
|
51
|
+
line.startsWith(" ");
|
|
52
|
+
if (!hunkContent)
|
|
53
|
+
return line;
|
|
54
|
+
const prefix = line.slice(0, 1);
|
|
55
|
+
const content = line.slice(1).replaceAll("\t", "→····").replaceAll(" ", "·");
|
|
56
|
+
return `${prefix}${content}`;
|
|
57
|
+
})
|
|
58
|
+
.join("\n");
|
|
59
|
+
/**
|
|
60
|
+
* Git is the source of this wire value, so a file begins only at a column-zero
|
|
61
|
+
* `diff --git` header. A changed source line is prefixed with +/− and cannot
|
|
62
|
+
* accidentally split the patch.
|
|
63
|
+
*/
|
|
64
|
+
const splitFilePatches = (diff) => {
|
|
65
|
+
const starts = [...diff.matchAll(/^diff --git /gm)].map((match) => match.index);
|
|
66
|
+
return starts.map((start, index) => {
|
|
67
|
+
const end = starts[index + 1] ?? diff.length;
|
|
68
|
+
const patch = diff.slice(start, end);
|
|
69
|
+
return patch.endsWith("\n") ? patch : `${patch}\n`;
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
const parseRows = (patch) => {
|
|
73
|
+
const patchLines = patch.split("\n");
|
|
74
|
+
const lines = [];
|
|
75
|
+
const hunks = [];
|
|
76
|
+
let unifiedRow = 0;
|
|
77
|
+
let splitRow = 0;
|
|
78
|
+
let hunkIndex = -1;
|
|
79
|
+
let oldLine = 0;
|
|
80
|
+
let newLine = 0;
|
|
81
|
+
for (let index = 0; index < patchLines.length; index += 1) {
|
|
82
|
+
const header = patchLines[index] ?? "";
|
|
83
|
+
const hunkMatch = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(header);
|
|
84
|
+
if (hunkMatch === null)
|
|
85
|
+
continue;
|
|
86
|
+
hunkIndex += 1;
|
|
87
|
+
oldLine = Number(hunkMatch[1]);
|
|
88
|
+
newLine = Number(hunkMatch[2]);
|
|
89
|
+
hunks.push({ index: hunkIndex, header, unifiedRow, splitRow });
|
|
90
|
+
const body = [];
|
|
91
|
+
let bodyIndex = index + 1;
|
|
92
|
+
while (bodyIndex < patchLines.length) {
|
|
93
|
+
const bodyLine = patchLines[bodyIndex] ?? "";
|
|
94
|
+
if (bodyLine.startsWith("@@ "))
|
|
95
|
+
break;
|
|
96
|
+
if (bodyLine.startsWith("+") && !bodyLine.startsWith("+++")) {
|
|
97
|
+
body.push({ kind: "addition" });
|
|
98
|
+
}
|
|
99
|
+
else if (bodyLine.startsWith("-") && !bodyLine.startsWith("---")) {
|
|
100
|
+
body.push({ kind: "deletion" });
|
|
101
|
+
}
|
|
102
|
+
else if (bodyLine.startsWith(" ")) {
|
|
103
|
+
body.push({ kind: "context" });
|
|
104
|
+
}
|
|
105
|
+
bodyIndex += 1;
|
|
106
|
+
}
|
|
107
|
+
let cursor = 0;
|
|
108
|
+
while (cursor < body.length) {
|
|
109
|
+
const current = body[cursor];
|
|
110
|
+
if (current?.kind === "context") {
|
|
111
|
+
lines.push({
|
|
112
|
+
kind: "context",
|
|
113
|
+
oldLine,
|
|
114
|
+
newLine,
|
|
115
|
+
unifiedRow,
|
|
116
|
+
splitRow,
|
|
117
|
+
hunk: hunkIndex,
|
|
118
|
+
commentable: true,
|
|
119
|
+
});
|
|
120
|
+
oldLine += 1;
|
|
121
|
+
newLine += 1;
|
|
122
|
+
unifiedRow += 1;
|
|
123
|
+
splitRow += 1;
|
|
124
|
+
cursor += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Git groups a replacement as a run of deletions/additions. Unified
|
|
128
|
+
// stacks every row; split aligns the Nth deletion with the Nth addition.
|
|
129
|
+
const groupStart = cursor;
|
|
130
|
+
while (cursor < body.length && body[cursor]?.kind !== "context")
|
|
131
|
+
cursor += 1;
|
|
132
|
+
const group = body.slice(groupStart, cursor);
|
|
133
|
+
let deletionOffset = 0;
|
|
134
|
+
let additionOffset = 0;
|
|
135
|
+
for (const entry of group) {
|
|
136
|
+
if (entry.kind === "deletion") {
|
|
137
|
+
lines.push({
|
|
138
|
+
kind: entry.kind,
|
|
139
|
+
oldLine,
|
|
140
|
+
newLine: null,
|
|
141
|
+
unifiedRow,
|
|
142
|
+
splitRow: splitRow + deletionOffset,
|
|
143
|
+
hunk: hunkIndex,
|
|
144
|
+
// ReviewComment persists a line number but no old/new side. Like
|
|
145
|
+
// the web review, deletions therefore abstain instead of storing
|
|
146
|
+
// a confidently wrong new-file anchor.
|
|
147
|
+
commentable: false,
|
|
148
|
+
});
|
|
149
|
+
oldLine += 1;
|
|
150
|
+
deletionOffset += 1;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
lines.push({
|
|
154
|
+
kind: entry.kind,
|
|
155
|
+
oldLine: null,
|
|
156
|
+
newLine,
|
|
157
|
+
unifiedRow,
|
|
158
|
+
splitRow: splitRow + additionOffset,
|
|
159
|
+
hunk: hunkIndex,
|
|
160
|
+
commentable: true,
|
|
161
|
+
});
|
|
162
|
+
newLine += 1;
|
|
163
|
+
additionOffset += 1;
|
|
164
|
+
}
|
|
165
|
+
unifiedRow += 1;
|
|
166
|
+
}
|
|
167
|
+
splitRow += Math.max(deletionOffset, additionOffset);
|
|
168
|
+
}
|
|
169
|
+
index = bodyIndex - 1;
|
|
170
|
+
}
|
|
171
|
+
return { lines, hunks, unifiedRows: unifiedRow, splitRows: splitRow };
|
|
172
|
+
};
|
|
173
|
+
/** Public review seam: the API's live git diff becomes the terminal's files, hunks, and anchors. */
|
|
174
|
+
export const buildReviewFiles = (diff, stats) => {
|
|
175
|
+
const patches = splitFilePatches(diff);
|
|
176
|
+
return stats.map((stat, index) => {
|
|
177
|
+
const patch = patches[index] ?? "";
|
|
178
|
+
const rows = parseRows(patch);
|
|
179
|
+
return {
|
|
180
|
+
...stat,
|
|
181
|
+
patch,
|
|
182
|
+
filetype: filetypeOf(stat.path),
|
|
183
|
+
status: statusOf(patch),
|
|
184
|
+
binary: /^Binary files |^GIT binary patch$/m.test(patch),
|
|
185
|
+
likelyGenerated: isLikelyGenerated(stat.path),
|
|
186
|
+
...rows,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
/** The mechanically assembled draft remains editable before the API receives it. */
|
|
191
|
+
export const assembleReviewInstruction = (change, comments) => {
|
|
192
|
+
const points = comments
|
|
193
|
+
.map((comment, index) => {
|
|
194
|
+
const anchor = comment.file === null
|
|
195
|
+
? "the change as a whole"
|
|
196
|
+
: `${comment.file}${comment.line === null ? "" : `:${comment.line}${comment.endLine === null || comment.endLine === comment.line ? "" : `-${comment.endLine}`}`}`;
|
|
197
|
+
const proposal = comment.suggestion === null
|
|
198
|
+
? ""
|
|
199
|
+
: `\n Proposed replacement:\n${comment.suggestion
|
|
200
|
+
.split("\n")
|
|
201
|
+
.map((line) => ` ${line}`)
|
|
202
|
+
.join("\n")}`;
|
|
203
|
+
return `${index + 1}. ${anchor} — ${comment.body}${proposal}`;
|
|
204
|
+
})
|
|
205
|
+
.join("\n");
|
|
206
|
+
return `This is a follow-up on your earlier work in this worktree, responding to review feedback. The branch ${change.branch} is already checked out with your work on it: keep working there, and do not start a new branch or reset it.
|
|
207
|
+
|
|
208
|
+
Review comments:
|
|
209
|
+
${points}
|
|
210
|
+
|
|
211
|
+
Address each point using your own judgment about how. Check your work with the repository's own build, tests, or typecheck; a check that still fails is something to report, not to force green. Report honestly what you changed and what you did not — if a point cannot or should not be done, say so plainly.`;
|
|
212
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CONTINUE_COMMANDS } from "./shared.js";
|
|
2
|
+
export const reviewTargetForSession = (session, annotation, projectName) => annotation?.changeId === null || annotation?.changeId === undefined
|
|
3
|
+
? null
|
|
4
|
+
: { session, changeId: annotation.changeId, projectName };
|
|
5
|
+
export const commentRange = (start, current) => start === null
|
|
6
|
+
? { line: current, endLine: null }
|
|
7
|
+
: { line: Math.min(start, current), endLine: Math.max(start, current) };
|
|
8
|
+
/** Close the review loop without changing session identity or worktree. */
|
|
9
|
+
export const deliverReview = async (api, session, instruction) => {
|
|
10
|
+
const build = CONTINUE_COMMANDS[session.harness];
|
|
11
|
+
if (build === undefined)
|
|
12
|
+
throw new Error(`Harness “${session.harness}” has no known relaunch command`);
|
|
13
|
+
await api("POST", `/sessions/${session.id}/follow-up/deliver`, {});
|
|
14
|
+
await api("POST", `/sessions/${session.id}/launch`, { argv: build(instruction) });
|
|
15
|
+
};
|