comprehende 0.1.0 → 0.3.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.
Files changed (73) hide show
  1. package/README.md +20 -49
  2. package/dist/api/error.js +8 -0
  3. package/dist/api/live.js +249 -0
  4. package/dist/api/paths.js +96 -0
  5. package/dist/api/snapshot.js +45 -0
  6. package/dist/api/types.js +1 -0
  7. package/dist/cli/args.js +7 -1
  8. package/dist/cli/commands.js +6 -0
  9. package/dist/cli/main.js +22 -10
  10. package/dist/git/blob.js +21 -0
  11. package/dist/git/diff.js +44 -2
  12. package/dist/git/exec.js +29 -3
  13. package/dist/git/lfs.js +39 -0
  14. package/dist/schema/cli-pin.js +18 -0
  15. package/dist/schema/image-diff.js +51 -0
  16. package/dist/schema/image.js +35 -0
  17. package/dist/schema/parse.js +5 -1
  18. package/dist/schema/skill-sync.js +90 -0
  19. package/dist/server/http.js +83 -197
  20. package/dist/ui/assets/index-BxIxxAYx.css +1 -0
  21. package/dist/ui/assets/index-D4bY-beP.js +1598 -0
  22. package/dist/ui/index.html +16 -2
  23. package/package.json +15 -11
  24. package/skills/comprehende/SKILL.md +44 -29
  25. package/skills/comprehende/references/example.md +21 -0
  26. package/skills/comprehende/references/review.schema.json +6 -1
  27. package/dist/cli/args.d.ts +0 -20
  28. package/dist/cli/args.js.map +0 -1
  29. package/dist/cli/commands.d.ts +0 -12
  30. package/dist/cli/commands.js.map +0 -1
  31. package/dist/cli/main.d.ts +0 -2
  32. package/dist/cli/main.js.map +0 -1
  33. package/dist/git/blame.d.ts +0 -8
  34. package/dist/git/blame.js.map +0 -1
  35. package/dist/git/diff.d.ts +0 -13
  36. package/dist/git/diff.js.map +0 -1
  37. package/dist/git/exec.d.ts +0 -10
  38. package/dist/git/exec.js.map +0 -1
  39. package/dist/git/log.d.ts +0 -8
  40. package/dist/git/log.js.map +0 -1
  41. package/dist/git/repo.d.ts +0 -7
  42. package/dist/git/repo.js.map +0 -1
  43. package/dist/git/show.d.ts +0 -2
  44. package/dist/git/show.js.map +0 -1
  45. package/dist/package-root.d.ts +0 -2
  46. package/dist/package-root.js.map +0 -1
  47. package/dist/review/coverage.d.ts +0 -20
  48. package/dist/review/coverage.js.map +0 -1
  49. package/dist/review/effort.d.ts +0 -1
  50. package/dist/review/effort.js +0 -17
  51. package/dist/review/effort.js.map +0 -1
  52. package/dist/review/generate.d.ts +0 -20
  53. package/dist/review/generate.js +0 -290
  54. package/dist/review/generate.js.map +0 -1
  55. package/dist/schema/blame-runs.d.ts +0 -14
  56. package/dist/schema/blame-runs.js.map +0 -1
  57. package/dist/schema/hunk-meta.d.ts +0 -26
  58. package/dist/schema/hunk-meta.js.map +0 -1
  59. package/dist/schema/hunk-patch.d.ts +0 -2
  60. package/dist/schema/hunk-patch.js +0 -49
  61. package/dist/schema/hunk-patch.js.map +0 -1
  62. package/dist/schema/identity.d.ts +0 -3
  63. package/dist/schema/identity.js.map +0 -1
  64. package/dist/schema/parse.d.ts +0 -13
  65. package/dist/schema/parse.js.map +0 -1
  66. package/dist/schema/skill-paths.d.ts +0 -7
  67. package/dist/schema/skill-paths.js.map +0 -1
  68. package/dist/schema/types.d.ts +0 -73
  69. package/dist/schema/types.js.map +0 -1
  70. package/dist/server/http.d.ts +0 -12
  71. package/dist/server/http.js.map +0 -1
  72. package/dist/ui/assets/index-BCZwzr3E.js +0 -1585
  73. package/dist/ui/assets/index-DsGDgUsX.css +0 -1
package/dist/git/diff.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isImagePath, isLfsPointerText } from "../schema/image.js";
1
2
  import { git } from "./exec.js";
2
3
  import { rangeLabel, resolveCommit } from "./repo.js";
3
4
  const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
@@ -23,7 +24,7 @@ export async function readDiff(cwd, baseRef, headRef) {
23
24
  "--end-of-options",
24
25
  `${baseRef}...${headRef}`,
25
26
  ]);
26
- return parseUnifiedDiff(stdout);
27
+ return classifyDiffFiles(parseUnifiedDiff(stdout));
27
28
  }
28
29
  export async function readHunkIndex(cwd, baseRef, headRef) {
29
30
  const { source } = await resolveSource(cwd, baseRef, headRef);
@@ -31,6 +32,12 @@ export async function readHunkIndex(cwd, baseRef, headRef) {
31
32
  const hunks = [];
32
33
  const skipped = [];
33
34
  for (const file of files) {
35
+ if (file.image) {
36
+ for (const hunk of file.hunks) {
37
+ hunks.push(toHunkRef(hunk));
38
+ }
39
+ continue;
40
+ }
34
41
  if (file.binary) {
35
42
  skipped.push({ path: file.path, reason: "binary" });
36
43
  continue;
@@ -196,6 +203,7 @@ class FileBuilder {
196
203
  path,
197
204
  status,
198
205
  binary: this.binary,
206
+ image: false,
199
207
  headerPatch: this.headerPatch,
200
208
  patch: this.patch,
201
209
  hunks,
@@ -282,14 +290,48 @@ function stripDiffPath(raw) {
282
290
  export function flattenHunks(files) {
283
291
  const hunks = [];
284
292
  for (const file of files) {
285
- if (file.binary) {
293
+ if (file.binary && !file.image) {
286
294
  continue;
287
295
  }
288
296
  hunks.push(...file.hunks);
289
297
  }
290
298
  return hunks;
291
299
  }
300
+ export function classifyDiffFiles(files) {
301
+ return files.map(classifyDiffFile);
302
+ }
303
+ function classifyDiffFile(file) {
304
+ if (!isImagePath(file.path) && (file.oldPath === undefined || !isImagePath(file.oldPath))) {
305
+ return file;
306
+ }
307
+ if (file.binary || isLfsPointerText(file.patch)) {
308
+ return asImageFile(file);
309
+ }
310
+ return file;
311
+ }
312
+ function asImageFile(file) {
313
+ return { ...file, image: true, hunks: [imageLiveHunk(file.path, file.oldPath)] };
314
+ }
315
+ export function imageLiveHunk(path, oldPath) {
316
+ const hunk = {
317
+ path,
318
+ oldStart: 0,
319
+ oldLines: 0,
320
+ newStart: 0,
321
+ newLines: 0,
322
+ header: "image",
323
+ lines: [],
324
+ patch: "",
325
+ };
326
+ if (oldPath !== undefined) {
327
+ hunk.oldPath = oldPath;
328
+ }
329
+ return hunk;
330
+ }
292
331
  export function fileLanguage(path) {
332
+ if (isImagePath(path)) {
333
+ return "image";
334
+ }
293
335
  const ext = path.split(".").pop()?.toLowerCase();
294
336
  switch (ext) {
295
337
  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 new GitError(`git ${args.join(" ")} failed: ${failure.stderr.trim() || failure.message}`, args, failure.stderr, failure.code);
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: typeof record.stdout === "string" ? record.stdout : "",
48
- stderr: typeof record.stderr === "string" ? record.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
+ }
@@ -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
+ }
@@ -0,0 +1,18 @@
1
+ export function cliPin(version) {
2
+ return `npx comprehende@${version}`;
3
+ }
4
+ const PIN = /npx comprehende@([^\s`]+)/g;
5
+ export function listedCliPins(markdown) {
6
+ return [...markdown.matchAll(PIN)].map((match) => match[1] ?? "");
7
+ }
8
+ export function applyCliPin(markdown, version) {
9
+ return markdown.replace(PIN, cliPin(version));
10
+ }
11
+ export function cliPinErrors(markdown, version) {
12
+ const expected = cliPin(version);
13
+ const pins = listedCliPins(markdown);
14
+ if (pins.length === 0) {
15
+ return [`SKILL.md must pin ${expected}`];
16
+ }
17
+ return [...new Set(pins.filter((pin) => pin !== version))].map((pin) => `SKILL.md pins npx comprehende@${pin}, package.json version is ${version}`);
18
+ }
@@ -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
+ }
@@ -2,7 +2,7 @@ import { isReviewSize, REVIEW_SIZES } from "./types.js";
2
2
  const DOCUMENT_KEYS = new Set(["version", "source", "size", "walkthrough", "tickets", "groups"]);
3
3
  const SOURCE_KEYS = new Set(["baseRef", "headRef", "range"]);
4
4
  const TICKET_KEYS = new Set(["id", "url", "title"]);
5
- const GROUP_KEYS = new Set(["id", "title", "summary", "lookFor", "dependsOn", "suggestedOrder", "hunkRefs"]);
5
+ const GROUP_KEYS = new Set(["id", "title", "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 = [];
@@ -129,6 +129,7 @@ function parseGroups(value, errors) {
129
129
  const hunkRefs = parseHunkRefs(item.hunkRefs, `groups[${i}].hunkRefs`, errors);
130
130
  const lookFor = parseStringList(item.lookFor, `groups[${i}].lookFor`, errors);
131
131
  const dependsOn = parseStringList(item.dependsOn, `groups[${i}].dependsOn`, errors);
132
+ const part = item.part === undefined ? undefined : requiredString(item.part, `groups[${i}].part`, errors);
132
133
  if (id === undefined || title === undefined || summary === undefined || suggestedOrder === undefined) {
133
134
  return;
134
135
  }
@@ -143,6 +144,9 @@ function parseGroups(value, errors) {
143
144
  if (dependsOn !== undefined) {
144
145
  group.dependsOn = dependsOn;
145
146
  }
147
+ if (part !== undefined) {
148
+ group.part = part;
149
+ }
146
150
  groups.push(group);
147
151
  });
148
152
  const known = new Set(groups.map((group) => group.id));
@@ -0,0 +1,90 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import { join, relative } from "node:path";
4
+ import { cliPinErrors } from "./cli-pin.js";
5
+ import { skillPaths } from "./skill-paths.js";
6
+ import { findPackageRoot, readPackageVersion } from "../package-root.js";
7
+ export function skillSyncErrors(input) {
8
+ const errors = [];
9
+ if (input.publishedSchema !== input.canonicalSchema) {
10
+ errors.push("skills/comprehende/references/review.schema.json drifted. Run: pnpm sync:skill");
11
+ }
12
+ if (input.installedSchema !== input.canonicalSchema) {
13
+ errors.push(".agents/skills/comprehende/references/review.schema.json drifted. Run: pnpm sync:skill");
14
+ }
15
+ errors.push(...cliPinErrors(input.publishedSkillMd, input.version));
16
+ const publishedKeys = [...input.publishedFiles.keys()].sort();
17
+ const installedKeys = [...input.installedFiles.keys()].sort();
18
+ if (publishedKeys.join("\n") !== installedKeys.join("\n")) {
19
+ errors.push(".agents/skills/comprehende is not a copy of skills/comprehende. Run: pnpm sync:skill");
20
+ return errors;
21
+ }
22
+ for (const rel of publishedKeys) {
23
+ if (input.publishedFiles.get(rel) !== input.installedFiles.get(rel)) {
24
+ errors.push(`${rel} differs between skills/ and .agents/. Run: pnpm sync:skill`);
25
+ }
26
+ }
27
+ return errors;
28
+ }
29
+ export async function loadWorkingTreeSkillSync(root = findPackageRoot()) {
30
+ const paths = skillPaths(root);
31
+ const publishedFiles = await readTree(paths.publishedSkill);
32
+ const installedFiles = await readTree(paths.installedSkill);
33
+ return {
34
+ version: readPackageVersion(),
35
+ canonicalSchema: await readFile(paths.canonicalSchema, "utf8"),
36
+ publishedSchema: await readFile(paths.publishedSchema, "utf8"),
37
+ installedSchema: await readFile(paths.installedSchema, "utf8"),
38
+ publishedSkillMd: publishedFiles.get("SKILL.md") ?? "",
39
+ publishedFiles,
40
+ installedFiles,
41
+ };
42
+ }
43
+ export function loadStagedSkillSync(root = findPackageRoot()) {
44
+ const publishedFiles = gitStagedTree(root, "skills/comprehende");
45
+ const installedFiles = gitStagedTree(root, ".agents/skills/comprehende");
46
+ const pkg = JSON.parse(gitShowStaged(root, "package.json"));
47
+ return {
48
+ version: pkg.version,
49
+ canonicalSchema: gitShowStaged(root, "src/schema/review.schema.json"),
50
+ publishedSchema: gitShowStaged(root, "skills/comprehende/references/review.schema.json"),
51
+ installedSchema: gitShowStaged(root, ".agents/skills/comprehende/references/review.schema.json"),
52
+ publishedSkillMd: publishedFiles.get("SKILL.md") ?? "",
53
+ publishedFiles,
54
+ installedFiles,
55
+ };
56
+ }
57
+ async function readTree(dir) {
58
+ const out = new Map();
59
+ const walk = async (current) => {
60
+ for (const entry of await readdir(current, { withFileTypes: true })) {
61
+ const full = join(current, entry.name);
62
+ if (entry.isDirectory()) {
63
+ await walk(full);
64
+ continue;
65
+ }
66
+ out.set(relative(dir, full), await readFile(full, "utf8"));
67
+ }
68
+ };
69
+ await walk(dir);
70
+ return out;
71
+ }
72
+ function gitStagedTree(root, prefix) {
73
+ const listing = execFileSync("git", ["ls-files", "-z", "--", prefix], {
74
+ cwd: root,
75
+ encoding: "utf8",
76
+ });
77
+ const out = new Map();
78
+ for (const full of listing.split("\0").filter(Boolean)) {
79
+ out.set(relative(prefix, full), gitShowStaged(root, full));
80
+ }
81
+ return out;
82
+ }
83
+ function gitShowStaged(root, path) {
84
+ try {
85
+ return execFileSync("git", ["show", `:${path}`], { cwd: root, encoding: "utf8" });
86
+ }
87
+ catch {
88
+ throw new Error(`staged file missing: ${path}`);
89
+ }
90
+ }