comprehende 0.5.4 → 0.6.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 +1 -1
- package/dist/api/agent-md.js +10 -9
- package/dist/api/live.js +16 -2
- package/dist/cli/args.js +1 -1
- package/dist/cli/commands.js +5 -2
- package/dist/cli/main.js +5 -6
- package/dist/git/exec.js +22 -0
- package/dist/git/lfs.js +2 -6
- package/dist/git/repo.js +9 -3
- package/dist/review/pins.js +30 -0
- package/dist/schema/parse.js +206 -20
- package/dist/schema/skill-sync.js +3 -1
- package/dist/schema/source.js +89 -0
- package/dist/schema/types.js +8 -0
- package/dist/ui/assets/bricolage-grotesque-latin-ext-opsz-normal-IcJDqblK.woff2 +0 -0
- package/dist/ui/assets/bricolage-grotesque-latin-opsz-normal-Cre6nC2_.woff2 +0 -0
- package/dist/ui/assets/bricolage-grotesque-vietnamese-opsz-normal-D9N6E8K1.woff2 +0 -0
- package/dist/ui/assets/index-C1EoRByu.css +1 -0
- package/dist/ui/assets/index-su33x8uU.js +1713 -0
- package/dist/ui/index.html +2 -2
- package/package.json +3 -3
- package/skills/comprehende/SKILL.md +33 -15
- package/skills/comprehende/references/example.md +10 -5
- package/skills/comprehende/references/review.schema.json +95 -27
- package/dist/ui/assets/index-BQTydqt9.css +0 -1
- package/dist/ui/assets/index-COQ0_ea8.js +0 -1713
- package/dist/ui/assets/source-serif-4-cyrillic-400-normal-B8Z6Jvzv.woff +0 -0
- package/dist/ui/assets/source-serif-4-cyrillic-400-normal-C44pakCn.woff2 +0 -0
- package/dist/ui/assets/source-serif-4-cyrillic-600-normal-BDP5YDHD.woff2 +0 -0
- package/dist/ui/assets/source-serif-4-cyrillic-600-normal-DFnC3vAY.woff +0 -0
- package/dist/ui/assets/source-serif-4-cyrillic-ext-400-normal-CWV284fw.woff2 +0 -0
- package/dist/ui/assets/source-serif-4-cyrillic-ext-400-normal-DLkyKLvH.woff +0 -0
- package/dist/ui/assets/source-serif-4-cyrillic-ext-600-normal-D_E8FDgP.woff2 +0 -0
- package/dist/ui/assets/source-serif-4-cyrillic-ext-600-normal-DiT2IGQm.woff +0 -0
- package/dist/ui/assets/source-serif-4-greek-400-normal-BccYJ-vZ.woff +0 -0
- package/dist/ui/assets/source-serif-4-greek-400-normal-Cw5-AHNN.woff2 +0 -0
- package/dist/ui/assets/source-serif-4-greek-600-normal-DdRkKr-1.woff2 +0 -0
- package/dist/ui/assets/source-serif-4-greek-600-normal-DhXQw1Kl.woff +0 -0
- package/dist/ui/assets/source-serif-4-latin-400-normal-DJ5YJwmz.woff2 +0 -0
- package/dist/ui/assets/source-serif-4-latin-400-normal-Dn3IlU-Z.woff +0 -0
- package/dist/ui/assets/source-serif-4-latin-600-normal-DMD1h6_f.woff +0 -0
- package/dist/ui/assets/source-serif-4-latin-600-normal-DouSKlru.woff2 +0 -0
- package/dist/ui/assets/source-serif-4-latin-ext-400-normal-CEpydyUl.woff +0 -0
- package/dist/ui/assets/source-serif-4-latin-ext-400-normal-Cp7z-ARB.woff2 +0 -0
- package/dist/ui/assets/source-serif-4-latin-ext-600-normal-0sIUyHMO.woff2 +0 -0
- package/dist/ui/assets/source-serif-4-latin-ext-600-normal-C6NNqpJh.woff +0 -0
- package/dist/ui/assets/source-serif-4-vietnamese-400-normal-BbG3LG1A.woff2 +0 -0
- package/dist/ui/assets/source-serif-4-vietnamese-400-normal-DR1UuFon.woff +0 -0
- package/dist/ui/assets/source-serif-4-vietnamese-600-normal-B0WJbuqT.woff +0 -0
- package/dist/ui/assets/source-serif-4-vietnamese-600-normal-CznyfUKR.woff2 +0 -0
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ This tool is for preventing this cognitive surrender while trying to maintain mo
|
|
|
42
42
|
|
|
43
43
|
`comprehende serve` and `comprehende export` share one UI and one git payload. Serve resolves refs to commit SHAs when it starts, then computes those payloads from the objects on each request. Export writes the same JSON (and image bytes) next to the UI so any static file server can host the review.
|
|
44
44
|
|
|
45
|
-
`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.
|
|
45
|
+
`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.6.0` (or `node /path/to/comprehende/dist/cli/main.js` after `pnpm build`).
|
|
46
46
|
|
|
47
47
|
## Release
|
|
48
48
|
|
package/dist/api/agent-md.js
CHANGED
|
@@ -23,8 +23,9 @@ function overviewAgentMd(review) {
|
|
|
23
23
|
"Answer questions about this git change.",
|
|
24
24
|
overviewSteps(review),
|
|
25
25
|
pinBlock(review, { commits: true }),
|
|
26
|
-
|
|
26
|
+
sourcesBlock(review),
|
|
27
27
|
coverageBlock(review),
|
|
28
|
+
joinBlocks(["The title:", review.document.title]),
|
|
28
29
|
review.document.why !== undefined ? joinBlocks(["The why:", review.document.why]) : null,
|
|
29
30
|
joinBlocks([`The what (${sizeLabel(review.document.size)}):`, review.document.summary]),
|
|
30
31
|
reviewConcernsBlock(review),
|
|
@@ -78,17 +79,17 @@ function pinBlock(review, options) {
|
|
|
78
79
|
commits,
|
|
79
80
|
]);
|
|
80
81
|
}
|
|
81
|
-
function
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
82
|
+
function sourcesBlock(review) {
|
|
83
|
+
const sources = review.document.sources ?? [];
|
|
84
|
+
if (sources.length === 0) {
|
|
84
85
|
return null;
|
|
85
86
|
}
|
|
86
|
-
const lines =
|
|
87
|
-
const
|
|
88
|
-
const url =
|
|
89
|
-
return `- ${
|
|
87
|
+
const lines = sources.map((source) => {
|
|
88
|
+
const gist = source.gist !== undefined ? ` ${source.gist}` : "";
|
|
89
|
+
const url = source.url !== undefined ? `\n ${source.url}` : "";
|
|
90
|
+
return `- ${source.kind} ${source.label}${gist}${url}`;
|
|
90
91
|
});
|
|
91
|
-
return ["
|
|
92
|
+
return ["Sources:", ...lines].join("\n");
|
|
92
93
|
}
|
|
93
94
|
function coverageBlock(review) {
|
|
94
95
|
const lines = [];
|
package/dist/api/live.js
CHANGED
|
@@ -6,8 +6,10 @@ import { GitError } from "../git/exec.js";
|
|
|
6
6
|
import { listCommits } from "../git/log.js";
|
|
7
7
|
import { pinRange, readRepoIdentity } from "../git/repo.js";
|
|
8
8
|
import { showFile } from "../git/show.js";
|
|
9
|
-
import { coverReview } from "../review/coverage.js";
|
|
9
|
+
import { coverReview, coverageErrors } from "../review/coverage.js";
|
|
10
|
+
import { commentPinErrors, staleCommentPins } from "../review/pins.js";
|
|
10
11
|
import { isLockfilePath } from "../schema/lockfile.js";
|
|
12
|
+
import { sourceCitationErrors } from "../schema/source.js";
|
|
11
13
|
import { AGENT_MD_MEDIA_TYPE, agentMd } from "./agent-md.js";
|
|
12
14
|
import { ApiError } from "./error.js";
|
|
13
15
|
export async function pinReviewSource(cwd, dataPath) {
|
|
@@ -18,6 +20,7 @@ export async function openReview(cwd, dataPath, pin) {
|
|
|
18
20
|
const document = await loadDocument(dataPath);
|
|
19
21
|
const range = pin ?? (await pinRange(cwd, document.source.baseRef, document.source.headRef));
|
|
20
22
|
const { files, coverage } = await coverReview(cwd, document, range);
|
|
23
|
+
const pins = await staleCommentPins(cwd, document, range);
|
|
21
24
|
const commits = await listCommits(cwd, range.baseSha, range.headSha);
|
|
22
25
|
const repo = await readRepoIdentity(cwd);
|
|
23
26
|
return {
|
|
@@ -33,12 +36,13 @@ export async function openReview(cwd, dataPath, pin) {
|
|
|
33
36
|
},
|
|
34
37
|
files,
|
|
35
38
|
coverage,
|
|
39
|
+
staleCommentPins: pins,
|
|
36
40
|
mergeBaseSha: range.mergeBaseSha,
|
|
37
41
|
commits,
|
|
38
42
|
};
|
|
39
43
|
}
|
|
40
44
|
export function reviewPayload(ctx) {
|
|
41
|
-
const { document, repo, resolved, files, coverage, commits } = ctx;
|
|
45
|
+
const { document, repo, resolved, files, coverage, staleCommentPins, commits } = ctx;
|
|
42
46
|
const lockfiles = lockfileFiles(files);
|
|
43
47
|
return {
|
|
44
48
|
document,
|
|
@@ -49,6 +53,7 @@ export function reviewPayload(ctx) {
|
|
|
49
53
|
assignedHunks: coverage.assignedHunks,
|
|
50
54
|
unassignedCount: coverage.unassigned.length,
|
|
51
55
|
staleCount: coverage.stale.length,
|
|
56
|
+
staleSourceCount: staleCommentPins.length,
|
|
52
57
|
},
|
|
53
58
|
groups: coverage.groups
|
|
54
59
|
.slice()
|
|
@@ -61,6 +66,7 @@ export function reviewPayload(ctx) {
|
|
|
61
66
|
lookFor: group.group.lookFor ?? [],
|
|
62
67
|
dependsOn: group.group.dependsOn ?? [],
|
|
63
68
|
part: group.group.part,
|
|
69
|
+
sources: group.group.sources ?? [],
|
|
64
70
|
suggestedOrder: group.group.suggestedOrder,
|
|
65
71
|
hunkCount: group.hunks.length,
|
|
66
72
|
staleCount: group.stale.length,
|
|
@@ -75,6 +81,7 @@ export function reviewPayload(ctx) {
|
|
|
75
81
|
files: lockfiles.map((file) => file.path),
|
|
76
82
|
},
|
|
77
83
|
stale: coverage.stale,
|
|
84
|
+
staleSources: staleCommentPins,
|
|
78
85
|
files: files.map((file) => {
|
|
79
86
|
const entry = {
|
|
80
87
|
path: file.path,
|
|
@@ -92,6 +99,13 @@ export function reviewPayload(ctx) {
|
|
|
92
99
|
commits,
|
|
93
100
|
};
|
|
94
101
|
}
|
|
102
|
+
export function reviewProblems(ctx) {
|
|
103
|
+
return [
|
|
104
|
+
...coverageErrors(ctx.coverage),
|
|
105
|
+
...sourceCitationErrors(ctx.document),
|
|
106
|
+
...commentPinErrors(ctx.staleCommentPins),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
95
109
|
export function hunksPayload(ctx, groupId) {
|
|
96
110
|
if (groupId === "") {
|
|
97
111
|
throw new ApiError(400, "missing group");
|
package/dist/cli/args.js
CHANGED
|
@@ -8,7 +8,7 @@ Commands:
|
|
|
8
8
|
List hunk refs from live git (no patch text)
|
|
9
9
|
|
|
10
10
|
validate --data <review.json>
|
|
11
|
-
Check schema, ref resolution, and
|
|
11
|
+
Check schema, ref resolution, hunk coverage, and source citations
|
|
12
12
|
|
|
13
13
|
serve --data <review.json> [--port <n>] [--open]
|
|
14
14
|
Serve the local UI on 127.0.0.1 (pins commit SHAs at start)
|
package/dist/cli/commands.js
CHANGED
|
@@ -3,7 +3,9 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
3
3
|
import { readHunkIndex, resolveSource } from "../git/diff.js";
|
|
4
4
|
import { defaultBaseRef } from "../git/repo.js";
|
|
5
5
|
import { coverReview, coverageErrors } from "../review/coverage.js";
|
|
6
|
+
import { commentPinErrors, staleCommentPins } from "../review/pins.js";
|
|
6
7
|
import { parseReviewJson } from "../schema/parse.js";
|
|
8
|
+
import { sourceCitationErrors } from "../schema/source.js";
|
|
7
9
|
export async function resolveRange(cwd, base, head) {
|
|
8
10
|
const baseRef = base ?? (await defaultBaseRef(cwd));
|
|
9
11
|
const headRef = head ?? "HEAD";
|
|
@@ -36,9 +38,10 @@ export function resolveOutPath(out, cwd) {
|
|
|
36
38
|
}
|
|
37
39
|
export async function cmdValidate(cwd, dataPath) {
|
|
38
40
|
const document = await loadDocument(dataPath);
|
|
39
|
-
await resolveSource(cwd, document.source.baseRef, document.source.headRef);
|
|
41
|
+
const resolved = await resolveSource(cwd, document.source.baseRef, document.source.headRef);
|
|
40
42
|
const { coverage } = await coverReview(cwd, document);
|
|
41
|
-
const
|
|
43
|
+
const pins = await staleCommentPins(cwd, document, resolved);
|
|
44
|
+
const errors = [...coverageErrors(coverage), ...sourceCitationErrors(document), ...commentPinErrors(pins)];
|
|
42
45
|
if (errors.length > 0) {
|
|
43
46
|
throw new Error(errors.join("\n\n"));
|
|
44
47
|
}
|
package/dist/cli/main.js
CHANGED
|
@@ -6,8 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { parseArgv, USAGE } from "./args.js";
|
|
7
7
|
import { cmdIndex, cmdValidate, resolveDataPath, resolveOutPath } from "./commands.js";
|
|
8
8
|
import { exportStaticSite } from "../api/snapshot.js";
|
|
9
|
-
import { openReview, pinReviewSource } from "../api/live.js";
|
|
10
|
-
import { coverageErrors } from "../review/coverage.js";
|
|
9
|
+
import { openReview, pinReviewSource, reviewProblems } from "../api/live.js";
|
|
11
10
|
import { assertWorkTree } from "../git/repo.js";
|
|
12
11
|
import { readPackageVersion } from "../package-root.js";
|
|
13
12
|
import { startServer } from "../server/http.js";
|
|
@@ -44,7 +43,7 @@ export async function run(argv) {
|
|
|
44
43
|
const dataPath = resolveDataPath(request.data, request.cwd);
|
|
45
44
|
const pin = await pinReviewSource(request.cwd, dataPath);
|
|
46
45
|
const ctx = await openReview(request.cwd, dataPath, pin);
|
|
47
|
-
|
|
46
|
+
warnReview(ctx, "serve continues; git wins, unassigned/stale are visible");
|
|
48
47
|
const running = await startServer({ cwd: request.cwd, dataPath, port: request.port, pin });
|
|
49
48
|
console.log(running.url);
|
|
50
49
|
console.error(`serving ${dataPath} cwd=${request.cwd} localhost only`);
|
|
@@ -58,7 +57,7 @@ export async function run(argv) {
|
|
|
58
57
|
const dataPath = resolveDataPath(request.data, request.cwd);
|
|
59
58
|
const outDir = resolveOutPath(request.out, request.cwd);
|
|
60
59
|
const ctx = await openReview(request.cwd, dataPath);
|
|
61
|
-
|
|
60
|
+
warnReview(ctx, "export continues; git wins, unassigned/stale are visible");
|
|
62
61
|
const result = await exportStaticSite({ cwd: request.cwd, dataPath, outDir, ctx });
|
|
63
62
|
console.log(result.outDir);
|
|
64
63
|
console.error(`exported ${dataPath} ${result.apiFiles.length} api files no git in the folder`);
|
|
@@ -71,8 +70,8 @@ export async function run(argv) {
|
|
|
71
70
|
return 1;
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
|
-
function
|
|
75
|
-
const problems =
|
|
73
|
+
function warnReview(ctx, note) {
|
|
74
|
+
const problems = reviewProblems(ctx);
|
|
76
75
|
if (problems.length > 0) {
|
|
77
76
|
console.error(`coverage issues (${note}):\n${problems.join("\n\n")}`);
|
|
78
77
|
}
|
package/dist/git/exec.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
const execFileAsync = promisify(execFile);
|
|
4
|
+
const GIT_DIR_VARS = new Set([
|
|
5
|
+
"GIT_DIR",
|
|
6
|
+
"GIT_WORK_TREE",
|
|
7
|
+
"GIT_INDEX_FILE",
|
|
8
|
+
"GIT_OBJECT_DIRECTORY",
|
|
9
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
10
|
+
"GIT_COMMON_DIR",
|
|
11
|
+
"GIT_PREFIX",
|
|
12
|
+
]);
|
|
13
|
+
/** Drop inherited git dir vars so `cwd` is the repository, including linked worktrees. */
|
|
14
|
+
export function gitEnv(base = process.env) {
|
|
15
|
+
const env = {};
|
|
16
|
+
for (const [key, value] of Object.entries(base)) {
|
|
17
|
+
if (value === undefined || GIT_DIR_VARS.has(key.toUpperCase())) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
env[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return env;
|
|
23
|
+
}
|
|
4
24
|
export class GitError extends Error {
|
|
5
25
|
args;
|
|
6
26
|
stderr;
|
|
@@ -17,6 +37,7 @@ export async function git(cwd, args, opts) {
|
|
|
17
37
|
try {
|
|
18
38
|
const { stdout } = await execFileAsync("git", ["-c", "core.quotepath=false", ...args], {
|
|
19
39
|
cwd,
|
|
40
|
+
env: gitEnv(),
|
|
20
41
|
encoding: "utf8",
|
|
21
42
|
maxBuffer: 64 * 1024 * 1024,
|
|
22
43
|
});
|
|
@@ -34,6 +55,7 @@ export async function gitBuffer(cwd, args, opts) {
|
|
|
34
55
|
try {
|
|
35
56
|
const { stdout } = await execFileAsync("git", ["-c", "core.quotepath=false", ...args], {
|
|
36
57
|
cwd,
|
|
58
|
+
env: gitEnv(),
|
|
37
59
|
encoding: "buffer",
|
|
38
60
|
maxBuffer: 64 * 1024 * 1024,
|
|
39
61
|
...(opts?.input !== undefined ? { input: Buffer.from(opts.input) } : {}),
|
package/dist/git/lfs.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { join } from "node:path";
|
|
4
4
|
import { LFS_POINTER_VERSION } from "../schema/image.js";
|
|
5
|
-
import {
|
|
5
|
+
import { gitCommonDir } from "./repo.js";
|
|
6
6
|
const POINTER_MAX = 1024;
|
|
7
7
|
export function parseLfsPointer(bytes) {
|
|
8
8
|
if (bytes.length === 0 || bytes.length > POINTER_MAX) {
|
|
@@ -25,10 +25,6 @@ export function parseLfsPointer(bytes) {
|
|
|
25
25
|
export function lfsObjectPath(gitCommonDir, oid) {
|
|
26
26
|
return join(gitCommonDir, "lfs", "objects", oid.slice(0, 2), oid.slice(2, 4), oid);
|
|
27
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
28
|
export async function readLfsObject(cwd, pointer) {
|
|
33
29
|
const common = await gitCommonDir(cwd);
|
|
34
30
|
const stored = lfsObjectPath(common, pointer.oid);
|
package/dist/git/repo.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
1
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
2
2
|
import { git, gitOk } from "./exec.js";
|
|
3
3
|
export async function assertWorkTree(cwd) {
|
|
4
4
|
const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"], { allowFail: true });
|
|
@@ -30,12 +30,18 @@ export function nameFromRemoteUrl(url) {
|
|
|
30
30
|
const last = pathPart.split("/").filter(Boolean).at(-1);
|
|
31
31
|
return last === undefined || last === "" ? null : last;
|
|
32
32
|
}
|
|
33
|
+
export async function gitCommonDir(cwd) {
|
|
34
|
+
const raw = (await git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"])).trim();
|
|
35
|
+
return isAbsolute(raw) ? raw : resolve(cwd, raw);
|
|
36
|
+
}
|
|
33
37
|
export async function readRepoIdentity(cwd) {
|
|
34
38
|
const originText = (await git(cwd, ["config", "--get", "remote.origin.url"], { allowFail: true })).trim();
|
|
35
39
|
const origin = originText === "" ? null : stripRemoteCredentials(originText);
|
|
36
|
-
const top = (await git(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
37
40
|
const fromOrigin = origin !== null ? nameFromRemoteUrl(origin) : null;
|
|
38
|
-
|
|
41
|
+
if (fromOrigin !== null) {
|
|
42
|
+
return { name: fromOrigin, origin };
|
|
43
|
+
}
|
|
44
|
+
return { name: basename(dirname(await gitCommonDir(cwd))), origin };
|
|
39
45
|
}
|
|
40
46
|
/** Drop userinfo from an http(s) remote so a copied prompt never carries a token. */
|
|
41
47
|
export function stripRemoteCredentials(url) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { fileExistsAt, showFile } from "../git/show.js";
|
|
2
|
+
import { isLinePinned, textLineCount } from "../schema/source.js";
|
|
3
|
+
export async function staleCommentPins(cwd, document, range) {
|
|
4
|
+
const stale = [];
|
|
5
|
+
for (const source of document.sources ?? []) {
|
|
6
|
+
if (!isLinePinned(source)) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
if (!(await commentPinMatches(cwd, source, range))) {
|
|
10
|
+
stale.push({ id: source.id, path: source.path, side: source.side, line: source.line });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return stale;
|
|
14
|
+
}
|
|
15
|
+
export function commentPinErrors(pins) {
|
|
16
|
+
if (pins.length === 0) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const lines = pins.map((pin) => ` ${pin.id} ${pin.path} ${pin.side}:${pin.line}`);
|
|
20
|
+
return [`stale: ${pins.length} comment pin(s) do not match live git:\n${lines.join("\n")}`];
|
|
21
|
+
}
|
|
22
|
+
async function commentPinMatches(cwd, source, range) {
|
|
23
|
+
const sha = source.side === "old" ? range.baseSha : range.headSha;
|
|
24
|
+
if (!(await fileExistsAt(cwd, sha, source.path))) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const content = await showFile(cwd, sha, source.path);
|
|
28
|
+
const count = textLineCount(content);
|
|
29
|
+
return source.line >= 1 && source.line <= count;
|
|
30
|
+
}
|
package/dist/schema/parse.js
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
import { isReviewSize, REVIEW_SIZES } from "./types.js";
|
|
2
|
-
const DOCUMENT_KEYS = new Set(["version", "source", "size", "summary", "why", "tickets", "groups"]);
|
|
1
|
+
import { isReviewSize, isSourceKind, isSourceSide, REVIEW_SIZES, SOURCE_KINDS, } from "./types.js";
|
|
2
|
+
const DOCUMENT_KEYS = new Set(["version", "source", "size", "title", "summary", "why", "tickets", "sources", "groups"]);
|
|
3
3
|
const SOURCE_KEYS = new Set(["baseRef", "headRef", "range"]);
|
|
4
4
|
const TICKET_KEYS = new Set(["id", "url", "title", "part"]);
|
|
5
|
-
const
|
|
5
|
+
const REVIEW_SOURCE_KEYS = new Set([
|
|
6
|
+
"id",
|
|
7
|
+
"kind",
|
|
8
|
+
"label",
|
|
9
|
+
"url",
|
|
10
|
+
"title",
|
|
11
|
+
"gist",
|
|
12
|
+
"part",
|
|
13
|
+
"author",
|
|
14
|
+
"body",
|
|
15
|
+
"path",
|
|
16
|
+
"side",
|
|
17
|
+
"line",
|
|
18
|
+
]);
|
|
19
|
+
const COMMENT_ONLY_KEYS = ["author", "body", "path", "side", "line"];
|
|
20
|
+
const GROUP_KEYS = new Set([
|
|
21
|
+
"id",
|
|
22
|
+
"title",
|
|
23
|
+
"why",
|
|
24
|
+
"summary",
|
|
25
|
+
"lookFor",
|
|
26
|
+
"dependsOn",
|
|
27
|
+
"part",
|
|
28
|
+
"sources",
|
|
29
|
+
"suggestedOrder",
|
|
30
|
+
"hunkRefs",
|
|
31
|
+
]);
|
|
6
32
|
const HUNK_KEYS = new Set(["path", "oldPath", "oldStart", "oldLines", "newStart", "newLines"]);
|
|
7
33
|
export function parseReviewDocument(input) {
|
|
8
34
|
const errors = [];
|
|
@@ -15,25 +41,27 @@ export function parseReviewDocument(input) {
|
|
|
15
41
|
}
|
|
16
42
|
const source = parseSource(input.source, errors);
|
|
17
43
|
const size = parseSize(input.size, errors);
|
|
44
|
+
const title = requiredString(input.title, "title", errors);
|
|
18
45
|
const summary = requiredString(input.summary, "summary", errors);
|
|
19
|
-
const
|
|
20
|
-
const groups = parseGroups(input.groups, errors);
|
|
46
|
+
const sources = parseDocumentSources(input.sources, input.tickets, errors);
|
|
47
|
+
const groups = parseGroups(input.groups, sources, errors);
|
|
21
48
|
const why = input.why === undefined ? undefined : requiredString(input.why, "why", errors);
|
|
22
|
-
if (errors.length > 0 || source === undefined || size === undefined || summary === undefined) {
|
|
49
|
+
if (errors.length > 0 || source === undefined || size === undefined || title === undefined || summary === undefined) {
|
|
23
50
|
return { ok: false, errors };
|
|
24
51
|
}
|
|
25
52
|
const document = {
|
|
26
53
|
version: 1,
|
|
27
54
|
source,
|
|
28
55
|
size,
|
|
56
|
+
title,
|
|
29
57
|
summary,
|
|
30
58
|
groups,
|
|
31
59
|
};
|
|
32
60
|
if (why !== undefined) {
|
|
33
61
|
document.why = why;
|
|
34
62
|
}
|
|
35
|
-
if (
|
|
36
|
-
document.
|
|
63
|
+
if (sources !== undefined) {
|
|
64
|
+
document.sources = sources;
|
|
37
65
|
}
|
|
38
66
|
return { ok: true, document };
|
|
39
67
|
}
|
|
@@ -75,15 +103,154 @@ function parseSource(value, errors) {
|
|
|
75
103
|
}
|
|
76
104
|
return source;
|
|
77
105
|
}
|
|
78
|
-
function
|
|
79
|
-
if (
|
|
106
|
+
function parseDocumentSources(sourcesValue, ticketsValue, errors) {
|
|
107
|
+
if (sourcesValue !== undefined && ticketsValue !== undefined) {
|
|
108
|
+
errors.push("document has both sources and tickets; use sources");
|
|
109
|
+
}
|
|
110
|
+
if (sourcesValue !== undefined) {
|
|
111
|
+
return parseSources(sourcesValue, errors);
|
|
112
|
+
}
|
|
113
|
+
if (ticketsValue !== undefined) {
|
|
114
|
+
return parseLegacyTickets(ticketsValue, errors);
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
function parseSources(value, errors) {
|
|
119
|
+
if (!Array.isArray(value)) {
|
|
120
|
+
errors.push("sources must be an array");
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const sources = [];
|
|
124
|
+
const ids = new Set();
|
|
125
|
+
value.forEach((item, i) => {
|
|
126
|
+
const source = parseSourceItem(item, `sources[${i}]`, errors);
|
|
127
|
+
if (source === undefined) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (ids.has(source.id)) {
|
|
131
|
+
errors.push(`duplicate source id "${source.id}"`);
|
|
132
|
+
}
|
|
133
|
+
ids.add(source.id);
|
|
134
|
+
sources.push(source);
|
|
135
|
+
});
|
|
136
|
+
return sources;
|
|
137
|
+
}
|
|
138
|
+
function parseSourceItem(value, label, errors) {
|
|
139
|
+
if (!isRecord(value)) {
|
|
140
|
+
errors.push(`${label} must be an object`);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
extraKeys(value, REVIEW_SOURCE_KEYS, label, errors);
|
|
144
|
+
const id = requiredString(value.id, `${label}.id`, errors);
|
|
145
|
+
const kind = parseKind(value.kind, `${label}.kind`, errors);
|
|
146
|
+
const sourceLabel = requiredString(value.label, `${label}.label`, errors);
|
|
147
|
+
if (id === undefined || kind === undefined || sourceLabel === undefined) {
|
|
80
148
|
return undefined;
|
|
81
149
|
}
|
|
150
|
+
const source = { id, kind, label: sourceLabel };
|
|
151
|
+
if (value.url !== undefined) {
|
|
152
|
+
const url = requiredString(value.url, `${label}.url`, errors);
|
|
153
|
+
if (url !== undefined) {
|
|
154
|
+
source.url = url;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (value.title !== undefined) {
|
|
158
|
+
const title = requiredString(value.title, `${label}.title`, errors);
|
|
159
|
+
if (title !== undefined) {
|
|
160
|
+
source.title = title;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (value.gist !== undefined) {
|
|
164
|
+
const gist = requiredString(value.gist, `${label}.gist`, errors);
|
|
165
|
+
if (gist !== undefined) {
|
|
166
|
+
source.gist = gist;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (value.part !== undefined) {
|
|
170
|
+
const part = requiredString(value.part, `${label}.part`, errors);
|
|
171
|
+
if (part !== undefined) {
|
|
172
|
+
source.part = part;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (kind === "transcript" && source.url !== undefined) {
|
|
176
|
+
errors.push(`${label}.url must be omitted for transcripts`);
|
|
177
|
+
}
|
|
178
|
+
assignCommentFields(value, source, kind, label, errors);
|
|
179
|
+
return source;
|
|
180
|
+
}
|
|
181
|
+
function assignCommentFields(value, source, kind, label, errors) {
|
|
182
|
+
const present = COMMENT_ONLY_KEYS.filter((key) => value[key] !== undefined);
|
|
183
|
+
if (kind !== "pr-comment") {
|
|
184
|
+
for (const key of present) {
|
|
185
|
+
errors.push(`${label}.${key} is only valid on pr-comment sources`);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (value.author !== undefined) {
|
|
190
|
+
const author = requiredString(value.author, `${label}.author`, errors);
|
|
191
|
+
if (author !== undefined) {
|
|
192
|
+
source.author = author;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (value.body !== undefined) {
|
|
196
|
+
const body = requiredString(value.body, `${label}.body`, errors, { allowEmpty: true });
|
|
197
|
+
if (body !== undefined) {
|
|
198
|
+
source.body = body;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (source.author === undefined) {
|
|
202
|
+
errors.push(`${label}.author is required on pr-comment sources`);
|
|
203
|
+
}
|
|
204
|
+
if (source.body === undefined) {
|
|
205
|
+
errors.push(`${label}.body is required on pr-comment sources`);
|
|
206
|
+
}
|
|
207
|
+
const pinCount = [value.path, value.side, value.line].filter((item) => item !== undefined).length;
|
|
208
|
+
if (pinCount === 0) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (pinCount !== 3) {
|
|
212
|
+
errors.push(`${label} line pin needs path, side, and line together`);
|
|
213
|
+
}
|
|
214
|
+
if (value.path !== undefined) {
|
|
215
|
+
const path = requiredString(value.path, `${label}.path`, errors);
|
|
216
|
+
if (path !== undefined) {
|
|
217
|
+
source.path = path;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (value.side !== undefined) {
|
|
221
|
+
if (!isSourceSide(value.side)) {
|
|
222
|
+
errors.push(`${label}.side must be old or new`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
source.side = value.side;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (value.line !== undefined) {
|
|
229
|
+
const line = requiredInt(value.line, `${label}.line`, errors);
|
|
230
|
+
if (line !== undefined) {
|
|
231
|
+
if (line < 1) {
|
|
232
|
+
errors.push(`${label}.line must be a positive integer`);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
source.line = line;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function parseKind(value, label, errors) {
|
|
241
|
+
if (isSourceKind(value)) {
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
errors.push(`${label} must be one of ${SOURCE_KINDS.join(", ")}`);
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
function parseLegacyTickets(value, errors) {
|
|
82
248
|
if (!Array.isArray(value)) {
|
|
83
249
|
errors.push("tickets must be an array");
|
|
84
250
|
return undefined;
|
|
85
251
|
}
|
|
86
|
-
const
|
|
252
|
+
const sources = [];
|
|
253
|
+
const ids = new Set();
|
|
87
254
|
value.forEach((item, i) => {
|
|
88
255
|
if (!isRecord(item)) {
|
|
89
256
|
errors.push(`tickets[${i}] must be an object`);
|
|
@@ -94,30 +261,34 @@ function parseTickets(value, errors) {
|
|
|
94
261
|
if (id === undefined) {
|
|
95
262
|
return;
|
|
96
263
|
}
|
|
97
|
-
|
|
264
|
+
if (ids.has(id)) {
|
|
265
|
+
errors.push(`duplicate source id "${id}"`);
|
|
266
|
+
}
|
|
267
|
+
ids.add(id);
|
|
268
|
+
const source = { id, kind: "ticket", label: id };
|
|
98
269
|
if (item.url !== undefined) {
|
|
99
270
|
const url = requiredString(item.url, `tickets[${i}].url`, errors);
|
|
100
271
|
if (url !== undefined) {
|
|
101
|
-
|
|
272
|
+
source.url = url;
|
|
102
273
|
}
|
|
103
274
|
}
|
|
104
275
|
if (item.title !== undefined) {
|
|
105
276
|
const title = requiredString(item.title, `tickets[${i}].title`, errors);
|
|
106
277
|
if (title !== undefined) {
|
|
107
|
-
|
|
278
|
+
source.title = title;
|
|
108
279
|
}
|
|
109
280
|
}
|
|
110
281
|
if (item.part !== undefined) {
|
|
111
282
|
const part = requiredString(item.part, `tickets[${i}].part`, errors);
|
|
112
283
|
if (part !== undefined) {
|
|
113
|
-
|
|
284
|
+
source.part = part;
|
|
114
285
|
}
|
|
115
286
|
}
|
|
116
|
-
|
|
287
|
+
sources.push(source);
|
|
117
288
|
});
|
|
118
|
-
return
|
|
289
|
+
return sources;
|
|
119
290
|
}
|
|
120
|
-
function parseGroups(value, errors) {
|
|
291
|
+
function parseGroups(value, sources, errors) {
|
|
121
292
|
if (!Array.isArray(value)) {
|
|
122
293
|
errors.push("groups must be an array");
|
|
123
294
|
return [];
|
|
@@ -138,6 +309,7 @@ function parseGroups(value, errors) {
|
|
|
138
309
|
const hunkRefs = parseHunkRefs(item.hunkRefs, `groups[${i}].hunkRefs`, errors);
|
|
139
310
|
const lookFor = parseStringList(item.lookFor, `groups[${i}].lookFor`, errors);
|
|
140
311
|
const dependsOn = parseStringList(item.dependsOn, `groups[${i}].dependsOn`, errors);
|
|
312
|
+
const groupSources = parseStringList(item.sources, `groups[${i}].sources`, errors);
|
|
141
313
|
const part = item.part === undefined ? undefined : requiredString(item.part, `groups[${i}].part`, errors);
|
|
142
314
|
if (id === undefined || title === undefined || why === undefined || summary === undefined || suggestedOrder === undefined) {
|
|
143
315
|
return;
|
|
@@ -156,18 +328,32 @@ function parseGroups(value, errors) {
|
|
|
156
328
|
if (part !== undefined) {
|
|
157
329
|
group.part = part;
|
|
158
330
|
}
|
|
331
|
+
if (groupSources !== undefined) {
|
|
332
|
+
group.sources = groupSources;
|
|
333
|
+
}
|
|
159
334
|
groups.push(group);
|
|
160
335
|
});
|
|
161
|
-
const
|
|
336
|
+
const knownGroups = new Set(groups.map((group) => group.id));
|
|
337
|
+
const knownSources = new Set((sources ?? []).map((source) => source.id));
|
|
162
338
|
for (const group of groups) {
|
|
163
339
|
for (const dep of group.dependsOn ?? []) {
|
|
164
340
|
if (dep === group.id) {
|
|
165
341
|
errors.push(`groups id "${group.id}" depends on itself`);
|
|
166
342
|
}
|
|
167
|
-
else if (!
|
|
343
|
+
else if (!knownGroups.has(dep)) {
|
|
168
344
|
errors.push(`groups id "${group.id}" dependsOn unknown group "${dep}"`);
|
|
169
345
|
}
|
|
170
346
|
}
|
|
347
|
+
const seen = new Set();
|
|
348
|
+
for (const sourceId of group.sources ?? []) {
|
|
349
|
+
if (seen.has(sourceId)) {
|
|
350
|
+
errors.push(`groups id "${group.id}" lists source "${sourceId}" twice`);
|
|
351
|
+
}
|
|
352
|
+
seen.add(sourceId);
|
|
353
|
+
if (!knownSources.has(sourceId)) {
|
|
354
|
+
errors.push(`groups id "${group.id}" sources unknown id "${sourceId}"`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
171
357
|
}
|
|
172
358
|
return groups;
|
|
173
359
|
}
|
|
@@ -4,6 +4,7 @@ import { dirname, join, relative } from "node:path";
|
|
|
4
4
|
import { applyCliPin, cliPinErrors, listedCliPins } from "./cli-pin.js";
|
|
5
5
|
import { skillPaths } from "./skill-paths.js";
|
|
6
6
|
import { findPackageRoot } from "../package-root.js";
|
|
7
|
+
import { gitEnv } from "../git/exec.js";
|
|
7
8
|
export function skillSyncErrors(input) {
|
|
8
9
|
const errors = [];
|
|
9
10
|
if (input.nextSchema !== input.canonicalSchema) {
|
|
@@ -110,6 +111,7 @@ function gitStagedTree(root, prefix) {
|
|
|
110
111
|
const listing = execFileSync("git", ["ls-files", "-z", "--", prefix], {
|
|
111
112
|
cwd: root,
|
|
112
113
|
encoding: "utf8",
|
|
114
|
+
env: gitEnv(),
|
|
113
115
|
});
|
|
114
116
|
const out = new Map();
|
|
115
117
|
for (const full of listing.split("\0").filter(Boolean)) {
|
|
@@ -119,7 +121,7 @@ function gitStagedTree(root, prefix) {
|
|
|
119
121
|
}
|
|
120
122
|
function gitShowStaged(root, path) {
|
|
121
123
|
try {
|
|
122
|
-
return execFileSync("git", ["show", `:${path}`], { cwd: root, encoding: "utf8" });
|
|
124
|
+
return execFileSync("git", ["show", `:${path}`], { cwd: root, encoding: "utf8", env: gitEnv() });
|
|
123
125
|
}
|
|
124
126
|
catch {
|
|
125
127
|
throw new Error(`staged file missing: ${path}`);
|