comprehende 0.3.0 → 0.4.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 +9 -2
- package/dist/api/live.js +61 -15
- package/dist/api/paths.js +18 -0
- package/dist/git/diff.js +101 -2
- package/dist/git/log.js +18 -13
- package/dist/git/name-status.js +73 -0
- package/dist/review/coverage.js +5 -1
- package/dist/schema/lockfile.js +38 -0
- package/dist/schema/parse.js +18 -9
- package/dist/schema/skill-paths.js +2 -2
- package/dist/schema/skill-sync.js +63 -26
- package/dist/ui/assets/{index-D4bY-beP.js → index-CcecgzMY.js} +52 -52
- package/dist/ui/assets/index-CukSNLbz.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/skills/comprehende/SKILL.md +39 -9
- package/skills/comprehende/references/example.md +13 -3
- package/skills/comprehende/references/review.schema.json +20 -5
- package/dist/ui/assets/index-BxIxxAYx.css +0 -1
package/README.md
CHANGED
|
@@ -25,18 +25,25 @@ pnpm typecheck
|
|
|
25
25
|
pnpm build
|
|
26
26
|
pnpm pack:smoke
|
|
27
27
|
pnpm sync:skill
|
|
28
|
+
pnpm release:skill
|
|
28
29
|
pnpm dev -- --help
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
`pnpm build` emits `dist/cli` and `dist/ui`. `prepack` runs that build, so `pnpm pack` / `pnpm publish` always ship the UI.
|
|
32
33
|
|
|
34
|
+
`pnpm sync:skill` copies the JSON Schema into `skills-next/comprehende/`, pins `npx comprehende@<version>` there, and mirrors that tree into `.agents/skills/comprehende` so agents in this checkout use the next skill. It does not touch `skills/comprehende/`.
|
|
35
|
+
|
|
33
36
|
`comprehende serve` and `comprehende export` share one UI and one git payload layer. Serve computes those payloads on each request. Export writes the same JSON (and image bytes) next to the UI so any static file server can host the review.
|
|
34
37
|
|
|
35
|
-
`pnpm dev` and `pnpm exec` run with this package as cwd, so they only make sense when _this_ repo is the one under review. To review a different project from a checkout, `cd` into it and run `npx comprehende@0.
|
|
38
|
+
`pnpm dev` and `pnpm exec` run with this package as cwd, so they only make sense when _this_ repo is the one under review. To review a different project from a checkout, `cd` into it and run `npx comprehende@0.4.1` (or `node /path/to/comprehende/dist/cli/main.js` after `pnpm build`).
|
|
36
39
|
|
|
37
40
|
## Release
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
Edit the skill in `skills-next/comprehende/`. `npx skills add` reads `skills/comprehende/` only.
|
|
43
|
+
|
|
44
|
+
Bump `version` in `package.json` when the CLI or UI changes, then run `pnpm sync:skill` so the next skill pin matches. Pre-commit and `pnpm test` fail if the staged package version and next skill pin differ. Do not bump for skill-only edits.
|
|
45
|
+
|
|
46
|
+
When that next skill should ship with `npx skills add`, run `pnpm release:skill`. That copies `skills-next/comprehende/` onto `skills/comprehende/`. Run it in the same change that publishes a new CLI. Then `npx skills add` installs instructions that match the package they pin.
|
|
40
47
|
|
|
41
48
|
Push to `main`. CI packs and tests the tarball on every change. If the version is not on npm yet, CI publishes it. Skill-only commits keep the same version, so they do not publish.
|
|
42
49
|
|
package/dist/api/live.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { loadDocument } from "../cli/commands.js";
|
|
2
2
|
import { blameFile } from "../git/blame.js";
|
|
3
3
|
import { readImageBlob } from "../git/blob.js";
|
|
4
|
-
import { fileLanguage, filePatchFromGit, resolveSource, toHunkRef } from "../git/diff.js";
|
|
4
|
+
import { fileLanguage, filePatchFromGit, readPathDiff, resolveSource, toHunkRef } from "../git/diff.js";
|
|
5
5
|
import { GitError } from "../git/exec.js";
|
|
6
6
|
import { listCommits } from "../git/log.js";
|
|
7
7
|
import { mergeBase } from "../git/repo.js";
|
|
8
8
|
import { showFile } from "../git/show.js";
|
|
9
9
|
import { coverReview } from "../review/coverage.js";
|
|
10
|
+
import { isLockfilePath } from "../schema/lockfile.js";
|
|
10
11
|
import { ApiError } from "./error.js";
|
|
11
12
|
export async function openReview(cwd, dataPath) {
|
|
12
13
|
const document = await loadDocument(dataPath);
|
|
@@ -34,6 +35,7 @@ export async function openReview(cwd, dataPath) {
|
|
|
34
35
|
}
|
|
35
36
|
export function reviewPayload(ctx) {
|
|
36
37
|
const { document, resolved, files, coverage, commits } = ctx;
|
|
38
|
+
const lockfiles = lockfileFiles(files);
|
|
37
39
|
return {
|
|
38
40
|
document,
|
|
39
41
|
resolved,
|
|
@@ -49,6 +51,7 @@ export function reviewPayload(ctx) {
|
|
|
49
51
|
.map((group) => ({
|
|
50
52
|
id: group.group.id,
|
|
51
53
|
title: group.group.title,
|
|
54
|
+
why: group.group.why,
|
|
52
55
|
summary: group.group.summary,
|
|
53
56
|
lookFor: group.group.lookFor ?? [],
|
|
54
57
|
dependsOn: group.group.dependsOn ?? [],
|
|
@@ -62,6 +65,10 @@ export function reviewPayload(ctx) {
|
|
|
62
65
|
hunkCount: coverage.unassigned.length,
|
|
63
66
|
files: uniquePaths(coverage.unassigned),
|
|
64
67
|
},
|
|
68
|
+
lockfiles: {
|
|
69
|
+
fileCount: lockfiles.length,
|
|
70
|
+
files: lockfiles.map((file) => file.path),
|
|
71
|
+
},
|
|
65
72
|
stale: coverage.stale,
|
|
66
73
|
files: files.map((file) => {
|
|
67
74
|
const entry = {
|
|
@@ -87,6 +94,9 @@ export function hunksPayload(ctx, groupId) {
|
|
|
87
94
|
if (groupId === "unassigned") {
|
|
88
95
|
return serializeLayer(ctx.files, ctx.coverage.unassigned);
|
|
89
96
|
}
|
|
97
|
+
if (groupId === "lockfiles") {
|
|
98
|
+
return serializeLockfiles(ctx.files);
|
|
99
|
+
}
|
|
90
100
|
const group = ctx.coverage.groups.find((item) => item.group.id === groupId);
|
|
91
101
|
if (group === undefined) {
|
|
92
102
|
throw new ApiError(404, `unknown group "${groupId}"`);
|
|
@@ -153,10 +163,16 @@ export async function renderResource(ctx, resource) {
|
|
|
153
163
|
return { encoding: "json", body: await blamePayload(ctx, resource.path, resource.side) };
|
|
154
164
|
case "image":
|
|
155
165
|
return imagePayload(ctx, resource.path, resource.side);
|
|
166
|
+
case "patch":
|
|
167
|
+
return { encoding: "json", body: await patchPayload(ctx, resource.path) };
|
|
156
168
|
}
|
|
157
169
|
}
|
|
158
170
|
export function listResources(ctx) {
|
|
159
|
-
const resources = [
|
|
171
|
+
const resources = [
|
|
172
|
+
{ kind: "review" },
|
|
173
|
+
{ kind: "hunks", group: "unassigned" },
|
|
174
|
+
{ kind: "hunks", group: "lockfiles" },
|
|
175
|
+
];
|
|
160
176
|
for (const group of ctx.document.groups) {
|
|
161
177
|
resources.push({ kind: "hunks", group: group.id });
|
|
162
178
|
}
|
|
@@ -167,6 +183,9 @@ export function listResources(ctx) {
|
|
|
167
183
|
}
|
|
168
184
|
continue;
|
|
169
185
|
}
|
|
186
|
+
if (isLockfilePath(file.path) && !file.binary) {
|
|
187
|
+
resources.push({ kind: "patch", path: file.path });
|
|
188
|
+
}
|
|
170
189
|
for (const side of sidesFor(file)) {
|
|
171
190
|
resources.push({ kind: "file", path: file.path, side });
|
|
172
191
|
resources.push({ kind: "blame", path: file.path, side });
|
|
@@ -197,21 +216,48 @@ function serializeLayer(files, hunks) {
|
|
|
197
216
|
}
|
|
198
217
|
groups.push({ file, hunks: [hunk] });
|
|
199
218
|
}
|
|
200
|
-
const serializedFiles = groups.map(({ file, hunks: fileHunks }) =>
|
|
201
|
-
const next = {
|
|
202
|
-
path: file.path,
|
|
203
|
-
kind: file.image ? "image" : "text",
|
|
204
|
-
status: file.status,
|
|
205
|
-
patch: file.image ? file.headerPatch : filePatchFromGit(file, fileHunks),
|
|
206
|
-
hunks: fileHunks.map(serializeHunk),
|
|
207
|
-
};
|
|
208
|
-
if (file.oldPath !== undefined) {
|
|
209
|
-
next.oldPath = file.oldPath;
|
|
210
|
-
}
|
|
211
|
-
return next;
|
|
212
|
-
});
|
|
219
|
+
const serializedFiles = groups.map(({ file, hunks: fileHunks }) => serializeLayerFile(file, fileHunks, true));
|
|
213
220
|
return { hunks: hunks.map(serializeHunk), files: serializedFiles };
|
|
214
221
|
}
|
|
222
|
+
function serializeLockfiles(files) {
|
|
223
|
+
const serializedFiles = lockfileFiles(files).map((file) => serializeLayerFile(file, [], true));
|
|
224
|
+
return { hunks: [], files: serializedFiles };
|
|
225
|
+
}
|
|
226
|
+
function lockfileFiles(files) {
|
|
227
|
+
return files.filter((file) => isLockfilePath(file.path) && !file.binary && !file.image);
|
|
228
|
+
}
|
|
229
|
+
async function patchPayload(ctx, path) {
|
|
230
|
+
const file = findFile(ctx.files, path);
|
|
231
|
+
if (!isLockfilePath(file.path) || file.binary || file.image) {
|
|
232
|
+
throw new ApiError(404, "path is not a deferred lockfile");
|
|
233
|
+
}
|
|
234
|
+
const live = await readPathDiff(ctx.cwd, ctx.document.source.baseRef, ctx.document.source.headRef, file.path);
|
|
235
|
+
if (live === undefined) {
|
|
236
|
+
throw new ApiError(404, `no live diff for ${path}`);
|
|
237
|
+
}
|
|
238
|
+
return serializeLayerFile(live, live.hunks, false);
|
|
239
|
+
}
|
|
240
|
+
function serializeLayerFile(file, fileHunks, deferLockfile) {
|
|
241
|
+
const lockfile = isLockfilePath(file.path) && !file.binary && !file.image;
|
|
242
|
+
const deferred = deferLockfile && lockfile;
|
|
243
|
+
const next = {
|
|
244
|
+
path: file.path,
|
|
245
|
+
kind: file.image ? "image" : lockfile ? "lockfile" : "text",
|
|
246
|
+
status: file.status,
|
|
247
|
+
patch: deferred ? "" : file.image ? file.headerPatch : filePatchFromGit(file, fileHunks),
|
|
248
|
+
hunks: fileHunks.map(serializeHunk),
|
|
249
|
+
};
|
|
250
|
+
if (file.oldPath !== undefined) {
|
|
251
|
+
next.oldPath = file.oldPath;
|
|
252
|
+
}
|
|
253
|
+
if (file.added !== undefined) {
|
|
254
|
+
next.added = file.added;
|
|
255
|
+
}
|
|
256
|
+
if (file.removed !== undefined) {
|
|
257
|
+
next.removed = file.removed;
|
|
258
|
+
}
|
|
259
|
+
return next;
|
|
260
|
+
}
|
|
215
261
|
function serializeHunk(hunk) {
|
|
216
262
|
return {
|
|
217
263
|
...toHunkRef(hunk),
|
package/dist/api/paths.js
CHANGED
|
@@ -10,6 +10,8 @@ export function apiHref(resource) {
|
|
|
10
10
|
return `api/blame/${resource.side}/${encodeFilePath(resource.path)}.json`;
|
|
11
11
|
case "image":
|
|
12
12
|
return `api/images/${resource.side}/${encodeFilePath(resource.path)}`;
|
|
13
|
+
case "patch":
|
|
14
|
+
return `api/patches/${encodeFilePath(resource.path)}.json`;
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
/** Decoded relative path under the site root. Static hosts map encoded request URLs onto this. */
|
|
@@ -25,6 +27,8 @@ export function apiFsRel(resource) {
|
|
|
25
27
|
return `api/blame/${resource.side}/${resource.path}.json`;
|
|
26
28
|
case "image":
|
|
27
29
|
return `api/images/${resource.side}/${resource.path}`;
|
|
30
|
+
case "patch":
|
|
31
|
+
return `api/patches/${resource.path}.json`;
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
export function parseApiPath(pathname) {
|
|
@@ -53,6 +57,20 @@ export function parseApiPath(pathname) {
|
|
|
53
57
|
}
|
|
54
58
|
return { kind: "image", path, side };
|
|
55
59
|
}
|
|
60
|
+
if (parts[1] === "patches" && parts.length >= 3) {
|
|
61
|
+
const rest = parts.slice(2);
|
|
62
|
+
const last = rest.at(-1);
|
|
63
|
+
const stem = last === undefined ? undefined : jsonStem(last);
|
|
64
|
+
if (stem === undefined) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
rest[rest.length - 1] = stem;
|
|
68
|
+
const path = rest.join("/");
|
|
69
|
+
if (!isRepoPath(path)) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return { kind: "patch", path };
|
|
73
|
+
}
|
|
56
74
|
if ((parts[1] === "files" || parts[1] === "blame") && parts.length >= 4 && parts[2] !== undefined) {
|
|
57
75
|
const side = parts[2];
|
|
58
76
|
if (side !== "old" && side !== "new") {
|
package/dist/git/diff.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isImagePath, isLfsPointerText } from "../schema/image.js";
|
|
2
|
+
import { isLockfilePath } from "../schema/lockfile.js";
|
|
2
3
|
import { git } from "./exec.js";
|
|
3
|
-
import {
|
|
4
|
+
import { parseNameStatus, parseNumstat } from "./name-status.js";
|
|
5
|
+
import { assertSafePath, rangeLabel, resolveCommit } from "./repo.js";
|
|
4
6
|
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
5
7
|
export async function resolveSource(cwd, baseRef, headRef) {
|
|
6
8
|
const baseSha = await resolveCommit(cwd, baseRef);
|
|
@@ -12,6 +14,53 @@ export async function resolveSource(cwd, baseRef, headRef) {
|
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
export async function readDiff(cwd, baseRef, headRef) {
|
|
17
|
+
await resolveCommit(cwd, baseRef);
|
|
18
|
+
await resolveCommit(cwd, headRef);
|
|
19
|
+
const range = `${baseRef}...${headRef}`;
|
|
20
|
+
const entries = parseNameStatus(await git(cwd, ["diff", "--find-renames", "--find-copies", "--name-status", "-z", "--end-of-options", range]));
|
|
21
|
+
const lockEntries = entries.filter((entry) => isLockfilePath(entry.path));
|
|
22
|
+
const diffArgs = [
|
|
23
|
+
"diff",
|
|
24
|
+
"--find-renames",
|
|
25
|
+
"--find-copies",
|
|
26
|
+
"-U3",
|
|
27
|
+
"--no-color",
|
|
28
|
+
"--no-ext-diff",
|
|
29
|
+
"--end-of-options",
|
|
30
|
+
range,
|
|
31
|
+
];
|
|
32
|
+
if (lockEntries.length > 0) {
|
|
33
|
+
const excludes = [
|
|
34
|
+
...new Set(lockEntries.flatMap((entry) => {
|
|
35
|
+
const paths = [`:(exclude)${entry.path}`];
|
|
36
|
+
if (entry.oldPath !== undefined) {
|
|
37
|
+
paths.push(`:(exclude)${entry.oldPath}`);
|
|
38
|
+
}
|
|
39
|
+
return paths;
|
|
40
|
+
})),
|
|
41
|
+
];
|
|
42
|
+
diffArgs.push("--", ".", ...excludes);
|
|
43
|
+
}
|
|
44
|
+
const files = classifyDiffFiles(parseUnifiedDiff(await git(cwd, diffArgs))).filter((file) => !isLockfilePath(file.path));
|
|
45
|
+
if (lockEntries.length === 0) {
|
|
46
|
+
return files;
|
|
47
|
+
}
|
|
48
|
+
const stats = parseNumstat(await git(cwd, [
|
|
49
|
+
"diff",
|
|
50
|
+
"--find-renames",
|
|
51
|
+
"--find-copies",
|
|
52
|
+
"--numstat",
|
|
53
|
+
"-z",
|
|
54
|
+
"--end-of-options",
|
|
55
|
+
range,
|
|
56
|
+
"--",
|
|
57
|
+
...lockEntries.map((entry) => entry.path),
|
|
58
|
+
]));
|
|
59
|
+
const stubs = lockEntries.map((entry) => lockfileDiffFile(entry, stats.get(entry.path)));
|
|
60
|
+
return mergeDiffFiles(entries, files, stubs);
|
|
61
|
+
}
|
|
62
|
+
export async function readPathDiff(cwd, baseRef, headRef, path) {
|
|
63
|
+
assertSafePath(path);
|
|
15
64
|
await resolveCommit(cwd, baseRef);
|
|
16
65
|
await resolveCommit(cwd, headRef);
|
|
17
66
|
const stdout = await git(cwd, [
|
|
@@ -23,8 +72,11 @@ export async function readDiff(cwd, baseRef, headRef) {
|
|
|
23
72
|
"--no-ext-diff",
|
|
24
73
|
"--end-of-options",
|
|
25
74
|
`${baseRef}...${headRef}`,
|
|
75
|
+
"--",
|
|
76
|
+
path,
|
|
26
77
|
]);
|
|
27
|
-
|
|
78
|
+
const files = classifyDiffFiles(parseUnifiedDiff(stdout));
|
|
79
|
+
return files.find((file) => file.path === path || file.oldPath === path);
|
|
28
80
|
}
|
|
29
81
|
export async function readHunkIndex(cwd, baseRef, headRef) {
|
|
30
82
|
const { source } = await resolveSource(cwd, baseRef, headRef);
|
|
@@ -42,6 +94,10 @@ export async function readHunkIndex(cwd, baseRef, headRef) {
|
|
|
42
94
|
skipped.push({ path: file.path, reason: "binary" });
|
|
43
95
|
continue;
|
|
44
96
|
}
|
|
97
|
+
if (isLockfilePath(file.path)) {
|
|
98
|
+
skipped.push({ path: file.path, reason: "lockfile" });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
45
101
|
for (const hunk of file.hunks) {
|
|
46
102
|
hunks.push(toHunkRef(hunk));
|
|
47
103
|
}
|
|
@@ -293,6 +349,9 @@ export function flattenHunks(files) {
|
|
|
293
349
|
if (file.binary && !file.image) {
|
|
294
350
|
continue;
|
|
295
351
|
}
|
|
352
|
+
if (isLockfilePath(file.path)) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
296
355
|
hunks.push(...file.hunks);
|
|
297
356
|
}
|
|
298
357
|
return hunks;
|
|
@@ -328,6 +387,46 @@ export function imageLiveHunk(path, oldPath) {
|
|
|
328
387
|
}
|
|
329
388
|
return hunk;
|
|
330
389
|
}
|
|
390
|
+
function lockfileDiffFile(entry, stat) {
|
|
391
|
+
const binary = stat === undefined || stat.added === null || stat.removed === null;
|
|
392
|
+
const file = {
|
|
393
|
+
path: entry.path,
|
|
394
|
+
status: entry.status,
|
|
395
|
+
binary,
|
|
396
|
+
image: false,
|
|
397
|
+
headerPatch: "",
|
|
398
|
+
patch: "",
|
|
399
|
+
hunks: [],
|
|
400
|
+
};
|
|
401
|
+
if (entry.oldPath !== undefined) {
|
|
402
|
+
file.oldPath = entry.oldPath;
|
|
403
|
+
}
|
|
404
|
+
if (!binary && stat !== undefined && stat.added !== null && stat.removed !== null) {
|
|
405
|
+
file.added = stat.added;
|
|
406
|
+
file.removed = stat.removed;
|
|
407
|
+
}
|
|
408
|
+
return file;
|
|
409
|
+
}
|
|
410
|
+
function mergeDiffFiles(entries, files, stubs) {
|
|
411
|
+
const filesByPath = new Map(files.map((file) => [file.path, file]));
|
|
412
|
+
const stubsByPath = new Map(stubs.map((file) => [file.path, file]));
|
|
413
|
+
const out = [];
|
|
414
|
+
const seen = new Set();
|
|
415
|
+
for (const entry of entries) {
|
|
416
|
+
const file = stubsByPath.get(entry.path) ?? filesByPath.get(entry.path);
|
|
417
|
+
if (file === undefined || seen.has(file.path)) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
seen.add(file.path);
|
|
421
|
+
out.push(file);
|
|
422
|
+
}
|
|
423
|
+
for (const file of files) {
|
|
424
|
+
if (!seen.has(file.path)) {
|
|
425
|
+
out.push(file);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
331
430
|
export function fileLanguage(path) {
|
|
332
431
|
if (isImagePath(path)) {
|
|
333
432
|
return "image";
|
package/dist/git/log.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { git } from "./exec.js";
|
|
2
|
+
const RECORD_SEP = "\x1e";
|
|
3
|
+
const FIELD_SEP = "\0";
|
|
2
4
|
export async function listCommits(cwd, baseRef, headRef) {
|
|
3
5
|
const stdout = await git(cwd, [
|
|
4
6
|
"log",
|
|
5
|
-
"--format=%H%x00%h%x00%s%x00%an%x00%ad",
|
|
7
|
+
"--format=%H%x00%h%x00%s%x00%an%x00%ad%x00%b%x1e",
|
|
6
8
|
"--date=short",
|
|
7
9
|
"--end-of-options",
|
|
8
10
|
`${baseRef}...${headRef}`,
|
|
@@ -11,16 +13,19 @@ export async function listCommits(cwd, baseRef, headRef) {
|
|
|
11
13
|
return [];
|
|
12
14
|
}
|
|
13
15
|
return stdout
|
|
14
|
-
.split(
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
.split(RECORD_SEP)
|
|
17
|
+
.map((record) => record.replace(/^\n/, ""))
|
|
18
|
+
.filter((record) => record.length > 0)
|
|
19
|
+
.map(parseCommitRecord);
|
|
20
|
+
}
|
|
21
|
+
function parseCommitRecord(record) {
|
|
22
|
+
const [sha, shortSha, subject, author, date, ...bodyParts] = record.split(FIELD_SEP);
|
|
23
|
+
return {
|
|
24
|
+
sha: sha ?? "",
|
|
25
|
+
shortSha: shortSha ?? "",
|
|
26
|
+
subject: subject ?? "",
|
|
27
|
+
author: author ?? "",
|
|
28
|
+
date: date ?? "",
|
|
29
|
+
body: (bodyParts.join(FIELD_SEP) ?? "").replace(/\n+$/u, ""),
|
|
30
|
+
};
|
|
26
31
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function parseNameStatus(stdout) {
|
|
2
|
+
const parts = stdout.split("\0").filter((part) => part !== "");
|
|
3
|
+
const entries = [];
|
|
4
|
+
let index = 0;
|
|
5
|
+
while (index < parts.length) {
|
|
6
|
+
const code = parts[index];
|
|
7
|
+
index += 1;
|
|
8
|
+
if (code === undefined) {
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
if (code.startsWith("R") || code.startsWith("C")) {
|
|
12
|
+
const oldPath = parts[index];
|
|
13
|
+
const path = parts[index + 1];
|
|
14
|
+
index += 2;
|
|
15
|
+
if (oldPath === undefined || path === undefined) {
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
entries.push({ status: "renamed", path, oldPath });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const path = parts[index];
|
|
22
|
+
index += 1;
|
|
23
|
+
if (path === undefined) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
entries.push({ status: statusFrom(code), path });
|
|
27
|
+
}
|
|
28
|
+
return entries;
|
|
29
|
+
}
|
|
30
|
+
/** Git `--numstat -z`: `added\\tdeleted\\tpath\\0`, or `added\\tdeleted\\t\\0old\\0new\\0` for a rename. */
|
|
31
|
+
export function parseNumstat(stdout) {
|
|
32
|
+
const parts = stdout.split("\0");
|
|
33
|
+
const out = new Map();
|
|
34
|
+
let index = 0;
|
|
35
|
+
while (index < parts.length) {
|
|
36
|
+
const field = parts[index];
|
|
37
|
+
if (field === undefined || field === "") {
|
|
38
|
+
index += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const match = /^(-|\d+)\t(-|\d+)\t(.*)$/.exec(field);
|
|
42
|
+
if (match === null || match[1] === undefined || match[2] === undefined || match[3] === undefined) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
index += 1;
|
|
46
|
+
const added = parseCount(match[1]);
|
|
47
|
+
const removed = parseCount(match[2]);
|
|
48
|
+
if (match[3] !== "") {
|
|
49
|
+
out.set(match[3], { path: match[3], added, removed });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const oldPath = parts[index];
|
|
53
|
+
const path = parts[index + 1];
|
|
54
|
+
index += 2;
|
|
55
|
+
if (oldPath === undefined || path === undefined || oldPath === "" || path === "") {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
out.set(path, { path, oldPath, added, removed });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function parseCount(value) {
|
|
63
|
+
return value === "-" ? null : Number(value);
|
|
64
|
+
}
|
|
65
|
+
function statusFrom(code) {
|
|
66
|
+
if (code.startsWith("A")) {
|
|
67
|
+
return "added";
|
|
68
|
+
}
|
|
69
|
+
if (code.startsWith("D")) {
|
|
70
|
+
return "deleted";
|
|
71
|
+
}
|
|
72
|
+
return "modified";
|
|
73
|
+
}
|
package/dist/review/coverage.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { flattenHunks, readDiff, toHunkRef } from "../git/diff.js";
|
|
2
2
|
import { hunkKey } from "../schema/identity.js";
|
|
3
|
+
import { isLockfilePath } from "../schema/lockfile.js";
|
|
3
4
|
export async function coverReview(cwd, document) {
|
|
4
5
|
const files = await readDiff(cwd, document.source.baseRef, document.source.headRef);
|
|
5
6
|
const live = flattenHunks(files);
|
|
@@ -16,6 +17,9 @@ export function joinCoverage(document, live) {
|
|
|
16
17
|
const hunks = [];
|
|
17
18
|
const stale = [];
|
|
18
19
|
for (const ref of group.hunkRefs) {
|
|
20
|
+
if (isLockfilePath(ref.path)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
19
23
|
const match = liveByKey.get(hunkKey(ref));
|
|
20
24
|
if (match === undefined) {
|
|
21
25
|
stale.push(ref);
|
|
@@ -23,7 +27,7 @@ export function joinCoverage(document, live) {
|
|
|
23
27
|
continue;
|
|
24
28
|
}
|
|
25
29
|
hunks.push(match);
|
|
26
|
-
assignedKeys.add(hunkKey(
|
|
30
|
+
assignedKeys.add(hunkKey(match));
|
|
27
31
|
}
|
|
28
32
|
return { group, hunks, stale };
|
|
29
33
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const LOCKFILE_NAMES = new Set([
|
|
2
|
+
".terraform.lock.hcl",
|
|
3
|
+
"bun.lock",
|
|
4
|
+
"bun.lockb",
|
|
5
|
+
"cabal.project.freeze",
|
|
6
|
+
"Cargo.lock",
|
|
7
|
+
"Cartfile.resolved",
|
|
8
|
+
"composer.lock",
|
|
9
|
+
"conda-lock.yml",
|
|
10
|
+
"deno.lock",
|
|
11
|
+
"flake.lock",
|
|
12
|
+
"Gemfile.lock",
|
|
13
|
+
"go.sum",
|
|
14
|
+
"go.work.sum",
|
|
15
|
+
"Gopkg.lock",
|
|
16
|
+
"gradle.lockfile",
|
|
17
|
+
"lazy-lock.json",
|
|
18
|
+
"mix.lock",
|
|
19
|
+
"npm-shrinkwrap.json",
|
|
20
|
+
"package-lock.json",
|
|
21
|
+
"Package.resolved",
|
|
22
|
+
"pdm.lock",
|
|
23
|
+
"Pipfile.lock",
|
|
24
|
+
"pixi.lock",
|
|
25
|
+
"pnpm-lock.yaml",
|
|
26
|
+
"Podfile.lock",
|
|
27
|
+
"poetry.lock",
|
|
28
|
+
"pubspec.lock",
|
|
29
|
+
"renv.lock",
|
|
30
|
+
"shrinkwrap.yaml",
|
|
31
|
+
"stack.yaml.lock",
|
|
32
|
+
"uv.lock",
|
|
33
|
+
"yarn.lock",
|
|
34
|
+
]);
|
|
35
|
+
export function isLockfilePath(path) {
|
|
36
|
+
const base = path.split("/").pop() ?? path;
|
|
37
|
+
return LOCKFILE_NAMES.has(base) || base.endsWith(".gradle.lockfile");
|
|
38
|
+
}
|
package/dist/schema/parse.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isReviewSize, REVIEW_SIZES } from "./types.js";
|
|
2
|
-
const DOCUMENT_KEYS = new Set(["version", "source", "size", "
|
|
2
|
+
const DOCUMENT_KEYS = new Set(["version", "source", "size", "summary", "why", "tickets", "groups"]);
|
|
3
3
|
const SOURCE_KEYS = new Set(["baseRef", "headRef", "range"]);
|
|
4
|
-
const TICKET_KEYS = new Set(["id", "url", "title"]);
|
|
5
|
-
const GROUP_KEYS = new Set(["id", "title", "summary", "lookFor", "dependsOn", "part", "suggestedOrder", "hunkRefs"]);
|
|
4
|
+
const TICKET_KEYS = new Set(["id", "url", "title", "part"]);
|
|
5
|
+
const GROUP_KEYS = new Set(["id", "title", "why", "summary", "lookFor", "dependsOn", "part", "suggestedOrder", "hunkRefs"]);
|
|
6
6
|
const HUNK_KEYS = new Set(["path", "oldPath", "oldStart", "oldLines", "newStart", "newLines"]);
|
|
7
7
|
export function parseReviewDocument(input) {
|
|
8
8
|
const errors = [];
|
|
@@ -15,20 +15,22 @@ export function parseReviewDocument(input) {
|
|
|
15
15
|
}
|
|
16
16
|
const source = parseSource(input.source, errors);
|
|
17
17
|
const size = parseSize(input.size, errors);
|
|
18
|
+
const summary = requiredString(input.summary, "summary", errors);
|
|
18
19
|
const tickets = parseTickets(input.tickets, errors);
|
|
19
20
|
const groups = parseGroups(input.groups, errors);
|
|
20
|
-
const
|
|
21
|
-
if (errors.length > 0 || source === undefined || size === undefined) {
|
|
21
|
+
const why = input.why === undefined ? undefined : requiredString(input.why, "why", errors);
|
|
22
|
+
if (errors.length > 0 || source === undefined || size === undefined || summary === undefined) {
|
|
22
23
|
return { ok: false, errors };
|
|
23
24
|
}
|
|
24
25
|
const document = {
|
|
25
26
|
version: 1,
|
|
26
27
|
source,
|
|
27
28
|
size,
|
|
29
|
+
summary,
|
|
28
30
|
groups,
|
|
29
31
|
};
|
|
30
|
-
if (
|
|
31
|
-
document.
|
|
32
|
+
if (why !== undefined) {
|
|
33
|
+
document.why = why;
|
|
32
34
|
}
|
|
33
35
|
if (tickets !== undefined) {
|
|
34
36
|
document.tickets = tickets;
|
|
@@ -105,6 +107,12 @@ function parseTickets(value, errors) {
|
|
|
105
107
|
ticket.title = title;
|
|
106
108
|
}
|
|
107
109
|
}
|
|
110
|
+
if (item.part !== undefined) {
|
|
111
|
+
const part = requiredString(item.part, `tickets[${i}].part`, errors);
|
|
112
|
+
if (part !== undefined) {
|
|
113
|
+
ticket.part = part;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
108
116
|
tickets.push(ticket);
|
|
109
117
|
});
|
|
110
118
|
return tickets;
|
|
@@ -124,20 +132,21 @@ function parseGroups(value, errors) {
|
|
|
124
132
|
extraKeys(item, GROUP_KEYS, `groups[${i}]`, errors);
|
|
125
133
|
const id = requiredString(item.id, `groups[${i}].id`, errors);
|
|
126
134
|
const title = requiredString(item.title, `groups[${i}].title`, errors);
|
|
135
|
+
const why = requiredString(item.why, `groups[${i}].why`, errors);
|
|
127
136
|
const summary = requiredString(item.summary, `groups[${i}].summary`, errors, { allowEmpty: true });
|
|
128
137
|
const suggestedOrder = requiredNumber(item.suggestedOrder, `groups[${i}].suggestedOrder`, errors);
|
|
129
138
|
const hunkRefs = parseHunkRefs(item.hunkRefs, `groups[${i}].hunkRefs`, errors);
|
|
130
139
|
const lookFor = parseStringList(item.lookFor, `groups[${i}].lookFor`, errors);
|
|
131
140
|
const dependsOn = parseStringList(item.dependsOn, `groups[${i}].dependsOn`, errors);
|
|
132
141
|
const part = item.part === undefined ? undefined : requiredString(item.part, `groups[${i}].part`, errors);
|
|
133
|
-
if (id === undefined || title === undefined || summary === undefined || suggestedOrder === undefined) {
|
|
142
|
+
if (id === undefined || title === undefined || why === undefined || summary === undefined || suggestedOrder === undefined) {
|
|
134
143
|
return;
|
|
135
144
|
}
|
|
136
145
|
if (ids.has(id)) {
|
|
137
146
|
errors.push(`duplicate group id "${id}"`);
|
|
138
147
|
}
|
|
139
148
|
ids.add(id);
|
|
140
|
-
const group = { id, title, summary, suggestedOrder, hunkRefs };
|
|
149
|
+
const group = { id, title, why, summary, suggestedOrder, hunkRefs };
|
|
141
150
|
if (lookFor !== undefined) {
|
|
142
151
|
group.lookFor = lookFor;
|
|
143
152
|
}
|
|
@@ -3,9 +3,9 @@ import { findPackageRoot } from "../package-root.js";
|
|
|
3
3
|
export function skillPaths(root = findPackageRoot()) {
|
|
4
4
|
return {
|
|
5
5
|
canonicalSchema: join(root, "src/schema/review.schema.json"),
|
|
6
|
+
nextSkill: join(root, "skills-next/comprehende"),
|
|
6
7
|
publishedSkill: join(root, "skills/comprehende"),
|
|
7
8
|
installedSkill: join(root, ".agents/skills/comprehende"),
|
|
8
|
-
|
|
9
|
-
installedSchema: join(root, ".agents/skills/comprehende/references/review.schema.json"),
|
|
9
|
+
nextSchema: join(root, "skills-next/comprehende/references/review.schema.json"),
|
|
10
10
|
};
|
|
11
11
|
}
|