comprehende 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/dist/api/error.js +8 -0
- package/dist/api/live.js +295 -0
- package/dist/api/paths.js +114 -0
- package/dist/api/snapshot.js +45 -0
- package/dist/api/types.js +1 -0
- package/dist/cli/args.js +7 -1
- package/dist/cli/commands.js +6 -0
- package/dist/cli/main.js +22 -10
- package/dist/git/blob.js +21 -0
- package/dist/git/diff.js +144 -3
- package/dist/git/exec.js +29 -3
- package/dist/git/lfs.js +39 -0
- 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/image-diff.js +51 -0
- package/dist/schema/image.js +35 -0
- 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/server/http.js +83 -198
- package/dist/ui/assets/index-jNrhIkrB.css +1 -0
- package/dist/ui/assets/index-wHrmWxbz.js +1598 -0
- package/dist/ui/index.html +3 -2
- package/package.json +2 -1
- package/skills/comprehende/SKILL.md +56 -27
- package/skills/comprehende/references/example.md +13 -3
- package/skills/comprehende/references/review.schema.json +20 -5
- package/dist/ui/assets/index-BSOLeQ7C.css +0 -1
- package/dist/ui/assets/index-Bgy9jIuw.js +0 -1598
package/dist/git/diff.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { isImagePath, isLfsPointerText } from "../schema/image.js";
|
|
2
|
+
import { isLockfilePath } from "../schema/lockfile.js";
|
|
1
3
|
import { git } from "./exec.js";
|
|
2
|
-
import {
|
|
4
|
+
import { parseNameStatus, parseNumstat } from "./name-status.js";
|
|
5
|
+
import { assertSafePath, rangeLabel, resolveCommit } from "./repo.js";
|
|
3
6
|
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
4
7
|
export async function resolveSource(cwd, baseRef, headRef) {
|
|
5
8
|
const baseSha = await resolveCommit(cwd, baseRef);
|
|
@@ -11,6 +14,53 @@ export async function resolveSource(cwd, baseRef, headRef) {
|
|
|
11
14
|
};
|
|
12
15
|
}
|
|
13
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);
|
|
14
64
|
await resolveCommit(cwd, baseRef);
|
|
15
65
|
await resolveCommit(cwd, headRef);
|
|
16
66
|
const stdout = await git(cwd, [
|
|
@@ -22,8 +72,11 @@ export async function readDiff(cwd, baseRef, headRef) {
|
|
|
22
72
|
"--no-ext-diff",
|
|
23
73
|
"--end-of-options",
|
|
24
74
|
`${baseRef}...${headRef}`,
|
|
75
|
+
"--",
|
|
76
|
+
path,
|
|
25
77
|
]);
|
|
26
|
-
|
|
78
|
+
const files = classifyDiffFiles(parseUnifiedDiff(stdout));
|
|
79
|
+
return files.find((file) => file.path === path || file.oldPath === path);
|
|
27
80
|
}
|
|
28
81
|
export async function readHunkIndex(cwd, baseRef, headRef) {
|
|
29
82
|
const { source } = await resolveSource(cwd, baseRef, headRef);
|
|
@@ -31,10 +84,20 @@ export async function readHunkIndex(cwd, baseRef, headRef) {
|
|
|
31
84
|
const hunks = [];
|
|
32
85
|
const skipped = [];
|
|
33
86
|
for (const file of files) {
|
|
87
|
+
if (file.image) {
|
|
88
|
+
for (const hunk of file.hunks) {
|
|
89
|
+
hunks.push(toHunkRef(hunk));
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
34
93
|
if (file.binary) {
|
|
35
94
|
skipped.push({ path: file.path, reason: "binary" });
|
|
36
95
|
continue;
|
|
37
96
|
}
|
|
97
|
+
if (isLockfilePath(file.path)) {
|
|
98
|
+
skipped.push({ path: file.path, reason: "lockfile" });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
38
101
|
for (const hunk of file.hunks) {
|
|
39
102
|
hunks.push(toHunkRef(hunk));
|
|
40
103
|
}
|
|
@@ -196,6 +259,7 @@ class FileBuilder {
|
|
|
196
259
|
path,
|
|
197
260
|
status,
|
|
198
261
|
binary: this.binary,
|
|
262
|
+
image: false,
|
|
199
263
|
headerPatch: this.headerPatch,
|
|
200
264
|
patch: this.patch,
|
|
201
265
|
hunks,
|
|
@@ -282,14 +346,91 @@ function stripDiffPath(raw) {
|
|
|
282
346
|
export function flattenHunks(files) {
|
|
283
347
|
const hunks = [];
|
|
284
348
|
for (const file of files) {
|
|
285
|
-
if (file.binary) {
|
|
349
|
+
if (file.binary && !file.image) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (isLockfilePath(file.path)) {
|
|
286
353
|
continue;
|
|
287
354
|
}
|
|
288
355
|
hunks.push(...file.hunks);
|
|
289
356
|
}
|
|
290
357
|
return hunks;
|
|
291
358
|
}
|
|
359
|
+
export function classifyDiffFiles(files) {
|
|
360
|
+
return files.map(classifyDiffFile);
|
|
361
|
+
}
|
|
362
|
+
function classifyDiffFile(file) {
|
|
363
|
+
if (!isImagePath(file.path) && (file.oldPath === undefined || !isImagePath(file.oldPath))) {
|
|
364
|
+
return file;
|
|
365
|
+
}
|
|
366
|
+
if (file.binary || isLfsPointerText(file.patch)) {
|
|
367
|
+
return asImageFile(file);
|
|
368
|
+
}
|
|
369
|
+
return file;
|
|
370
|
+
}
|
|
371
|
+
function asImageFile(file) {
|
|
372
|
+
return { ...file, image: true, hunks: [imageLiveHunk(file.path, file.oldPath)] };
|
|
373
|
+
}
|
|
374
|
+
export function imageLiveHunk(path, oldPath) {
|
|
375
|
+
const hunk = {
|
|
376
|
+
path,
|
|
377
|
+
oldStart: 0,
|
|
378
|
+
oldLines: 0,
|
|
379
|
+
newStart: 0,
|
|
380
|
+
newLines: 0,
|
|
381
|
+
header: "image",
|
|
382
|
+
lines: [],
|
|
383
|
+
patch: "",
|
|
384
|
+
};
|
|
385
|
+
if (oldPath !== undefined) {
|
|
386
|
+
hunk.oldPath = oldPath;
|
|
387
|
+
}
|
|
388
|
+
return hunk;
|
|
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
|
+
}
|
|
292
430
|
export function fileLanguage(path) {
|
|
431
|
+
if (isImagePath(path)) {
|
|
432
|
+
return "image";
|
|
433
|
+
}
|
|
293
434
|
const ext = path.split(".").pop()?.toLowerCase();
|
|
294
435
|
switch (ext) {
|
|
295
436
|
case "ts":
|
package/dist/git/exec.js
CHANGED
|
@@ -27,7 +27,21 @@ export async function git(cwd, args, opts) {
|
|
|
27
27
|
if (opts?.allowFail) {
|
|
28
28
|
return failure.stdout;
|
|
29
29
|
}
|
|
30
|
-
throw
|
|
30
|
+
throw gitFailure(args, failure);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function gitBuffer(cwd, args, opts) {
|
|
34
|
+
try {
|
|
35
|
+
const { stdout } = await execFileAsync("git", ["-c", "core.quotepath=false", ...args], {
|
|
36
|
+
cwd,
|
|
37
|
+
encoding: "buffer",
|
|
38
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
39
|
+
...(opts?.input !== undefined ? { input: Buffer.from(opts.input) } : {}),
|
|
40
|
+
});
|
|
41
|
+
return stdout;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw gitFailure(args, asExecFailure(error));
|
|
31
45
|
}
|
|
32
46
|
}
|
|
33
47
|
export async function gitOk(cwd, args) {
|
|
@@ -44,10 +58,22 @@ function asExecFailure(error) {
|
|
|
44
58
|
const record = error;
|
|
45
59
|
return {
|
|
46
60
|
message: typeof record.message === "string" ? record.message : "git failed",
|
|
47
|
-
stdout:
|
|
48
|
-
stderr:
|
|
61
|
+
stdout: bufferText(record.stdout),
|
|
62
|
+
stderr: bufferText(record.stderr),
|
|
49
63
|
code: typeof record.code === "number" ? record.code : null,
|
|
50
64
|
};
|
|
51
65
|
}
|
|
52
66
|
return { message: "git failed", stdout: "", stderr: "", code: null };
|
|
53
67
|
}
|
|
68
|
+
function bufferText(value) {
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
if (value instanceof Buffer) {
|
|
73
|
+
return value.toString("utf8");
|
|
74
|
+
}
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
function gitFailure(args, failure) {
|
|
78
|
+
return new GitError(`git ${args.join(" ")} failed: ${failure.stderr.trim() || failure.message}`, args, failure.stderr, failure.code);
|
|
79
|
+
}
|
package/dist/git/lfs.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { LFS_POINTER_VERSION } from "../schema/image.js";
|
|
5
|
+
import { git } from "./exec.js";
|
|
6
|
+
const POINTER_MAX = 1024;
|
|
7
|
+
export function parseLfsPointer(bytes) {
|
|
8
|
+
if (bytes.length === 0 || bytes.length > POINTER_MAX) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
if (bytes.includes(0)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const text = new TextDecoder("utf8").decode(bytes);
|
|
15
|
+
if (!text.startsWith(`${LFS_POINTER_VERSION}\n`)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const oid = /^oid sha256:([a-f0-9]{64})$/m.exec(text)?.[1];
|
|
19
|
+
const sizeRaw = /^size (\d+)$/m.exec(text)?.[1];
|
|
20
|
+
if (oid === undefined || sizeRaw === undefined) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return { oid, size: Number(sizeRaw) };
|
|
24
|
+
}
|
|
25
|
+
export function lfsObjectPath(gitCommonDir, oid) {
|
|
26
|
+
return join(gitCommonDir, "lfs", "objects", oid.slice(0, 2), oid.slice(2, 4), oid);
|
|
27
|
+
}
|
|
28
|
+
export async function gitCommonDir(cwd) {
|
|
29
|
+
const raw = (await git(cwd, ["rev-parse", "--git-common-dir"])).trim();
|
|
30
|
+
return isAbsolute(raw) ? raw : resolve(cwd, raw);
|
|
31
|
+
}
|
|
32
|
+
export async function readLfsObject(cwd, pointer) {
|
|
33
|
+
const common = await gitCommonDir(cwd);
|
|
34
|
+
const stored = lfsObjectPath(common, pointer.oid);
|
|
35
|
+
if (!existsSync(stored)) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return readFile(stored);
|
|
39
|
+
}
|
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,51 @@
|
|
|
1
|
+
const UNCHANGED_KEEP = 0.35;
|
|
2
|
+
const THRESHOLD = 8;
|
|
3
|
+
/** Paint a same-size RGBA pair. Unchanged pixels go gray; changed pixels go magenta. */
|
|
4
|
+
export function diffRgba(oldPixels, newPixels, width, height, threshold = THRESHOLD) {
|
|
5
|
+
const total = width * height;
|
|
6
|
+
const pixels = new Uint8ClampedArray(total * 4);
|
|
7
|
+
let changed = 0;
|
|
8
|
+
for (let i = 0; i < total; i += 1) {
|
|
9
|
+
const o = i * 4;
|
|
10
|
+
const oldA = oldPixels[o + 3] ?? 0;
|
|
11
|
+
const newA = newPixels[o + 3] ?? 0;
|
|
12
|
+
const empty = oldA === 0 && newA === 0;
|
|
13
|
+
const dr = Math.abs((oldPixels[o] ?? 0) - (newPixels[o] ?? 0));
|
|
14
|
+
const dg = Math.abs((oldPixels[o + 1] ?? 0) - (newPixels[o + 1] ?? 0));
|
|
15
|
+
const db = Math.abs((oldPixels[o + 2] ?? 0) - (newPixels[o + 2] ?? 0));
|
|
16
|
+
const da = Math.abs(oldA - newA);
|
|
17
|
+
const delta = Math.max(dr, dg, db, da);
|
|
18
|
+
if (empty || delta <= threshold) {
|
|
19
|
+
const gray = grayOf(newPixels[o] ?? 0, newPixels[o + 1] ?? 0, newPixels[o + 2] ?? 0);
|
|
20
|
+
const faded = Math.round(gray * UNCHANGED_KEEP);
|
|
21
|
+
pixels[o] = faded;
|
|
22
|
+
pixels[o + 1] = faded;
|
|
23
|
+
pixels[o + 2] = faded;
|
|
24
|
+
pixels[o + 3] = 255;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
changed += 1;
|
|
28
|
+
if (oldA === 0 && newA > 0) {
|
|
29
|
+
pixels[o] = 26;
|
|
30
|
+
pixels[o + 1] = 127;
|
|
31
|
+
pixels[o + 2] = 55;
|
|
32
|
+
pixels[o + 3] = 255;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (newA === 0 && oldA > 0) {
|
|
36
|
+
pixels[o] = 207;
|
|
37
|
+
pixels[o + 1] = 34;
|
|
38
|
+
pixels[o + 2] = 46;
|
|
39
|
+
pixels[o + 3] = 255;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
pixels[o] = 200;
|
|
43
|
+
pixels[o + 1] = 40;
|
|
44
|
+
pixels[o + 2] = 160;
|
|
45
|
+
pixels[o + 3] = 255;
|
|
46
|
+
}
|
|
47
|
+
return { pixels, changed, total };
|
|
48
|
+
}
|
|
49
|
+
function grayOf(r, g, b) {
|
|
50
|
+
return Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b);
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "bmp", "avif", "ico"];
|
|
2
|
+
const IMAGE_EXT = new Set(IMAGE_EXTENSIONS);
|
|
3
|
+
export const LFS_POINTER_VERSION = "version https://git-lfs.github.com/spec/v1";
|
|
4
|
+
const MEDIA = {
|
|
5
|
+
png: "image/png",
|
|
6
|
+
jpg: "image/jpeg",
|
|
7
|
+
jpeg: "image/jpeg",
|
|
8
|
+
gif: "image/gif",
|
|
9
|
+
webp: "image/webp",
|
|
10
|
+
bmp: "image/bmp",
|
|
11
|
+
avif: "image/avif",
|
|
12
|
+
ico: "image/x-icon",
|
|
13
|
+
};
|
|
14
|
+
export function imageExtension(path) {
|
|
15
|
+
const base = path.split("/").pop() ?? path;
|
|
16
|
+
const dot = base.lastIndexOf(".");
|
|
17
|
+
if (dot <= 0) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const ext = base.slice(dot + 1).toLowerCase();
|
|
21
|
+
return IMAGE_EXT.has(ext) ? ext : undefined;
|
|
22
|
+
}
|
|
23
|
+
export function isImagePath(path) {
|
|
24
|
+
return imageExtension(path) !== undefined;
|
|
25
|
+
}
|
|
26
|
+
export function imageMediaType(path) {
|
|
27
|
+
const ext = imageExtension(path);
|
|
28
|
+
return ext === undefined ? "application/octet-stream" : MEDIA[ext];
|
|
29
|
+
}
|
|
30
|
+
export function isLfsPointerText(text) {
|
|
31
|
+
return text.includes(LFS_POINTER_VERSION) && /oid sha256:[a-f0-9]{64}/.test(text);
|
|
32
|
+
}
|
|
33
|
+
export function isImageHunkRef(ref) {
|
|
34
|
+
return ref.oldStart === 0 && ref.oldLines === 0 && ref.newStart === 0 && ref.newLines === 0;
|
|
35
|
+
}
|
|
@@ -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
|
}
|