dsh-code-review-graph 0.0.0-stage → 0.1.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.
@@ -0,0 +1,68 @@
1
+ // src/safety.ts
2
+ import { realpath, stat, lstat, readdir } from "node:fs/promises";
3
+ import { isAbsolute, relative, resolve, sep, dirname, basename, join } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
6
+ function contained(root, path) {
7
+ const part = relative(root, path);
8
+ return part === "" || !isAbsolute(part) && part !== ".." && !part.startsWith(`..${sep}`);
9
+ }
10
+ async function checkedPath(root, value, allowMissing = false) {
11
+ const candidate = resolve(root, value);
12
+ if (!contained(root, candidate)) throw new Error("Path is outside the authorized repository");
13
+ let current = candidate;
14
+ const suffix = [];
15
+ while (allowMissing && !await lstat(current).then(() => true, () => false)) {
16
+ suffix.unshift(basename(current));
17
+ current = dirname(current);
18
+ }
19
+ const canonical = join(await realpath(current), ...suffix);
20
+ if (!contained(root, canonical)) throw new Error("Symlink escapes the authorized repository");
21
+ return canonical;
22
+ }
23
+
24
+ // src/refactor.ts
25
+ async function commitPlan(ctx, exec, root, plan) {
26
+ if (!exec.agent) throw new Error("Refactor requires a session-owned agent");
27
+ const policy = ctx.sandboxPolicy.resolve({ session: exec.agent.session });
28
+ if (policy.mode === "read-only") throw new Error("Refactor is denied by read-only policy");
29
+ if (!Array.isArray(plan.files)) throw new Error("Invalid edit plan");
30
+ const prepared = [];
31
+ const seen = /* @__PURE__ */ new Set();
32
+ for (const file of plan.files) {
33
+ if (typeof file.path !== "string" || !/^[a-f0-9]{64}$/.test(file.before_sha256) || typeof file.after_content !== "string" || !Number.isInteger(file.edit_count) || file.edit_count < 0) throw new Error("Invalid edit plan file");
34
+ exec.signal.throwIfAborted();
35
+ const path = await checkedPath(root, file.path);
36
+ if (seen.has(path)) throw new Error("Duplicate edit target");
37
+ seen.add(path);
38
+ const target = await ctx.fs.resolve(path, { cwd: root, signal: exec.signal });
39
+ const before = await ctx.fs.stat(target, exec.signal);
40
+ if (!before || before.type !== "file") throw new Error("Edit target is not a regular file");
41
+ const bytes = await ctx.fs.readBytes(target, exec.signal, 32 * 1024 * 1024);
42
+ if (sha256(bytes) !== file.before_sha256) throw new Error(`Stale edit plan: ${path}`);
43
+ const after = await ctx.fs.stat(target, exec.signal);
44
+ if (after?.version !== before.version) throw new Error(`File changed during preflight: ${path}`);
45
+ const intent = await ctx.waterfall("fs/write-intent", target, exec, () => void 0);
46
+ if (intent && (intent.kind !== "replaceIfVersion" || intent.version !== before.version)) throw new Error(`File intent disagrees with edit plan: ${path}`);
47
+ prepared.push({ file, path, target, original: Buffer.from(bytes).toString("utf8"), intent: intent ?? { kind: "replaceIfVersion", version: before.version } });
48
+ }
49
+ const applied = [];
50
+ const recovery = [];
51
+ try {
52
+ for (const item of prepared) {
53
+ exec.signal.throwIfAborted();
54
+ await checkedPath(root, item.path);
55
+ const outcome = await ctx.fs.writeText(item.target, item.file.after_content, item.intent, exec.signal, policy);
56
+ ctx.emit("fs/observed", item.target, { kind: "present", version: outcome.version }, exec);
57
+ applied.push(item.path);
58
+ recovery.push({ path: item.path, before_sha256: sha256(item.file.after_content), after_content: item.original, edit_count: item.file.edit_count });
59
+ }
60
+ } catch (error) {
61
+ return { status: "partial_failure", error: String(error), files_modified: applied, recovery_patch: { files: recovery } };
62
+ }
63
+ return { status: "ok", files_modified: applied, edits_applied: plan.files.reduce((n, f) => n + f.edit_count, 0) };
64
+ }
65
+ export {
66
+ commitPlan
67
+ };
68
+ //# sourceMappingURL=refactor.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/safety.ts", "../src/refactor.ts"],
4
+ "sourcesContent": ["import { realpath, stat, lstat, readdir } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve, sep, dirname, basename, join } from 'node:path';\nimport { createHash } from 'node:crypto';\n\nexport const sha256 = (bytes: Uint8Array | string): string => createHash('sha256').update(bytes).digest('hex');\n\nexport function contained(root: string, path: string): boolean {\n const part = relative(root, path);\n return part === '' || (!isAbsolute(part) && part !== '..' && !part.startsWith(`..${sep}`));\n}\n\nexport async function checkedPath(root: string, value: string, allowMissing = false): Promise<string> {\n const candidate = resolve(root, value);\n if (!contained(root, candidate)) throw new Error('Path is outside the authorized repository');\n let current = candidate;\n const suffix: string[] = [];\n while (allowMissing && !await lstat(current).then(() => true, () => false)) {\n suffix.unshift(basename(current)); current = dirname(current);\n }\n const canonical = join(await realpath(current), ...suffix);\n if (!contained(root, canonical)) throw new Error('Symlink escapes the authorized repository');\n return canonical;\n}\n\nexport async function repository(cwd: string | undefined): Promise<string> {\n if (!cwd || !isAbsolute(cwd)) throw new Error('This session has no local absolute workspace');\n const root = await realpath(cwd);\n if (!(await stat(root)).isDirectory()) throw new Error('Workspace is not a directory');\n for (const marker of ['.git', '.svn']) {\n if (await stat(resolve(root, marker)).then(() => true, () => false)) return root;\n }\n throw new Error('Select a Git or SVN repository before using the graph');\n}\n\nexport function scrubEnv(env: NodeJS.ProcessEnv): Record<string, string> {\n return { ...Object.fromEntries(Object.entries(env).filter(([key, value]) => value !== undefined && !/KEY|PASSWORD|SECRET|TOKEN|^(DSH|CRG|OPENAI|GOOGLE|GEMINI|MINIMAX|VOYAGE|HF)_/i.test(key))), HF_HUB_OFFLINE: '1', TRANSFORMERS_OFFLINE: '1', HF_HUB_DISABLE_TELEMETRY: '1' } as Record<string, string>;\n}\n\nexport async function checkedData(root: string): Promise<void> {\n const dir = resolve(root, '.code-review-graph');\n if (!await lstat(dir).then(() => true, () => false)) return;\n await checkedPath(root, dir);\n const entries = await readdir(dir, { withFileTypes: true, recursive: true });\n for (const entry of entries) {\n if (entry.isSymbolicLink()) await checkedPath(root, join(entry.parentPath, entry.name));\n }\n}\n", "import type { Context } from '@deepseek-ai/cordis';\nimport type { ToolRunContext } from '@deepseek-ai/dsh-tools';\nimport type {} from '@deepseek-ai/dsh-fs';\nimport type {} from '@deepseek-ai/dsh-sandbox-policy';\nimport { checkedPath, sha256 } from './safety.ts';\n\nexport interface EditPlan { files: { path: string; before_sha256: string; after_content: string; edit_count: number }[] }\nexport async function commitPlan(ctx: Context, exec: ToolRunContext, root: string, plan: EditPlan) {\n if (!exec.agent) throw new Error('Refactor requires a session-owned agent');\n const policy = ctx.sandboxPolicy.resolve({ session: exec.agent.session });\n if (policy.mode === 'read-only') throw new Error('Refactor is denied by read-only policy');\n if (!Array.isArray(plan.files)) throw new Error('Invalid edit plan');\n const prepared = [];\n const seen = new Set<string>();\n for (const file of plan.files) {\n if (typeof file.path !== 'string' || !/^[a-f0-9]{64}$/.test(file.before_sha256) || typeof file.after_content !== 'string' || !Number.isInteger(file.edit_count) || file.edit_count < 0) throw new Error('Invalid edit plan file');\n exec.signal.throwIfAborted();\n const path = await checkedPath(root, file.path);\n if (seen.has(path)) throw new Error('Duplicate edit target');\n seen.add(path);\n const target = await ctx.fs.resolve(path, { cwd: root, signal: exec.signal });\n const before = await ctx.fs.stat(target, exec.signal);\n if (!before || before.type !== 'file') throw new Error('Edit target is not a regular file');\n const bytes = await ctx.fs.readBytes(target, exec.signal, 32 * 1024 * 1024);\n if (sha256(bytes) !== file.before_sha256) throw new Error(`Stale edit plan: ${path}`);\n const after = await ctx.fs.stat(target, exec.signal);\n if (after?.version !== before.version) throw new Error(`File changed during preflight: ${path}`);\n const intent = await ctx.waterfall('fs/write-intent', target, exec, () => undefined);\n if (intent && (intent.kind !== 'replaceIfVersion' || intent.version !== before.version)) throw new Error(`File intent disagrees with edit plan: ${path}`);\n prepared.push({ file, path, target, original: Buffer.from(bytes).toString('utf8'), intent: intent ?? { kind: 'replaceIfVersion' as const, version: before.version } });\n }\n const applied: string[] = [];\n const recovery: EditPlan['files'] = [];\n try {\n for (const item of prepared) {\n exec.signal.throwIfAborted();\n await checkedPath(root, item.path);\n const outcome = await ctx.fs.writeText(item.target, item.file.after_content, item.intent, exec.signal, policy);\n ctx.emit('fs/observed', item.target, { kind: 'present', version: outcome.version }, exec);\n applied.push(item.path);\n recovery.push({ path: item.path, before_sha256: sha256(item.file.after_content), after_content: item.original, edit_count: item.file.edit_count });\n }\n } catch (error) {\n // Return the complete recovery patch through the tool output; never silently roll back a later edit.\n return { status: 'partial_failure', error: String(error), files_modified: applied, recovery_patch: { files: recovery } };\n }\n return { status: 'ok', files_modified: applied, edits_applied: plan.files.reduce((n, f) => n + f.edit_count, 0) };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/safety.js ADDED
@@ -0,0 +1,52 @@
1
+ // src/safety.ts
2
+ import { realpath, stat, lstat, readdir } from "node:fs/promises";
3
+ import { isAbsolute, relative, resolve, sep, dirname, basename, join } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
6
+ function contained(root, path) {
7
+ const part = relative(root, path);
8
+ return part === "" || !isAbsolute(part) && part !== ".." && !part.startsWith(`..${sep}`);
9
+ }
10
+ async function checkedPath(root, value, allowMissing = false) {
11
+ const candidate = resolve(root, value);
12
+ if (!contained(root, candidate)) throw new Error("Path is outside the authorized repository");
13
+ let current = candidate;
14
+ const suffix = [];
15
+ while (allowMissing && !await lstat(current).then(() => true, () => false)) {
16
+ suffix.unshift(basename(current));
17
+ current = dirname(current);
18
+ }
19
+ const canonical = join(await realpath(current), ...suffix);
20
+ if (!contained(root, canonical)) throw new Error("Symlink escapes the authorized repository");
21
+ return canonical;
22
+ }
23
+ async function repository(cwd) {
24
+ if (!cwd || !isAbsolute(cwd)) throw new Error("This session has no local absolute workspace");
25
+ const root = await realpath(cwd);
26
+ if (!(await stat(root)).isDirectory()) throw new Error("Workspace is not a directory");
27
+ for (const marker of [".git", ".svn"]) {
28
+ if (await stat(resolve(root, marker)).then(() => true, () => false)) return root;
29
+ }
30
+ throw new Error("Select a Git or SVN repository before using the graph");
31
+ }
32
+ function scrubEnv(env) {
33
+ return { ...Object.fromEntries(Object.entries(env).filter(([key, value]) => value !== void 0 && !/KEY|PASSWORD|SECRET|TOKEN|^(DSH|CRG|OPENAI|GOOGLE|GEMINI|MINIMAX|VOYAGE|HF)_/i.test(key))), HF_HUB_OFFLINE: "1", TRANSFORMERS_OFFLINE: "1", HF_HUB_DISABLE_TELEMETRY: "1" };
34
+ }
35
+ async function checkedData(root) {
36
+ const dir = resolve(root, ".code-review-graph");
37
+ if (!await lstat(dir).then(() => true, () => false)) return;
38
+ await checkedPath(root, dir);
39
+ const entries = await readdir(dir, { withFileTypes: true, recursive: true });
40
+ for (const entry of entries) {
41
+ if (entry.isSymbolicLink()) await checkedPath(root, join(entry.parentPath, entry.name));
42
+ }
43
+ }
44
+ export {
45
+ checkedData,
46
+ checkedPath,
47
+ contained,
48
+ repository,
49
+ scrubEnv,
50
+ sha256
51
+ };
52
+ //# sourceMappingURL=safety.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/safety.ts"],
4
+ "sourcesContent": ["import { realpath, stat, lstat, readdir } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve, sep, dirname, basename, join } from 'node:path';\nimport { createHash } from 'node:crypto';\n\nexport const sha256 = (bytes: Uint8Array | string): string => createHash('sha256').update(bytes).digest('hex');\n\nexport function contained(root: string, path: string): boolean {\n const part = relative(root, path);\n return part === '' || (!isAbsolute(part) && part !== '..' && !part.startsWith(`..${sep}`));\n}\n\nexport async function checkedPath(root: string, value: string, allowMissing = false): Promise<string> {\n const candidate = resolve(root, value);\n if (!contained(root, candidate)) throw new Error('Path is outside the authorized repository');\n let current = candidate;\n const suffix: string[] = [];\n while (allowMissing && !await lstat(current).then(() => true, () => false)) {\n suffix.unshift(basename(current)); current = dirname(current);\n }\n const canonical = join(await realpath(current), ...suffix);\n if (!contained(root, canonical)) throw new Error('Symlink escapes the authorized repository');\n return canonical;\n}\n\nexport async function repository(cwd: string | undefined): Promise<string> {\n if (!cwd || !isAbsolute(cwd)) throw new Error('This session has no local absolute workspace');\n const root = await realpath(cwd);\n if (!(await stat(root)).isDirectory()) throw new Error('Workspace is not a directory');\n for (const marker of ['.git', '.svn']) {\n if (await stat(resolve(root, marker)).then(() => true, () => false)) return root;\n }\n throw new Error('Select a Git or SVN repository before using the graph');\n}\n\nexport function scrubEnv(env: NodeJS.ProcessEnv): Record<string, string> {\n return { ...Object.fromEntries(Object.entries(env).filter(([key, value]) => value !== undefined && !/KEY|PASSWORD|SECRET|TOKEN|^(DSH|CRG|OPENAI|GOOGLE|GEMINI|MINIMAX|VOYAGE|HF)_/i.test(key))), HF_HUB_OFFLINE: '1', TRANSFORMERS_OFFLINE: '1', HF_HUB_DISABLE_TELEMETRY: '1' } as Record<string, string>;\n}\n\nexport async function checkedData(root: string): Promise<void> {\n const dir = resolve(root, '.code-review-graph');\n if (!await lstat(dir).then(() => true, () => false)) return;\n await checkedPath(root, dir);\n const entries = await readdir(dir, { withFileTypes: true, recursive: true });\n for (const entry of entries) {\n if (entry.isSymbolicLink()) await checkedPath(root, join(entry.parentPath, entry.name));\n }\n}\n"],
5
+ "mappings": ";AAAA,SAAS,UAAU,MAAM,OAAO,eAAe;AAC/C,SAAS,YAAY,UAAU,SAAS,KAAK,SAAS,UAAU,YAAY;AAC5E,SAAS,kBAAkB;AAEpB,IAAM,SAAS,CAAC,UAAuC,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAEtG,SAAS,UAAU,MAAc,MAAuB;AAC7D,QAAM,OAAO,SAAS,MAAM,IAAI;AAChC,SAAO,SAAS,MAAO,CAAC,WAAW,IAAI,KAAK,SAAS,QAAQ,CAAC,KAAK,WAAW,KAAK,GAAG,EAAE;AAC1F;AAEA,eAAsB,YAAY,MAAc,OAAe,eAAe,OAAwB;AACpG,QAAM,YAAY,QAAQ,MAAM,KAAK;AACrC,MAAI,CAAC,UAAU,MAAM,SAAS,EAAG,OAAM,IAAI,MAAM,2CAA2C;AAC5F,MAAI,UAAU;AACd,QAAM,SAAmB,CAAC;AAC1B,SAAO,gBAAgB,CAAC,MAAM,MAAM,OAAO,EAAE,KAAK,MAAM,MAAM,MAAM,KAAK,GAAG;AAC1E,WAAO,QAAQ,SAAS,OAAO,CAAC;AAAG,cAAU,QAAQ,OAAO;AAAA,EAC9D;AACA,QAAM,YAAY,KAAK,MAAM,SAAS,OAAO,GAAG,GAAG,MAAM;AACzD,MAAI,CAAC,UAAU,MAAM,SAAS,EAAG,OAAM,IAAI,MAAM,2CAA2C;AAC5F,SAAO;AACT;AAEA,eAAsB,WAAW,KAA0C;AACzE,MAAI,CAAC,OAAO,CAAC,WAAW,GAAG,EAAG,OAAM,IAAI,MAAM,8CAA8C;AAC5F,QAAM,OAAO,MAAM,SAAS,GAAG;AAC/B,MAAI,EAAE,MAAM,KAAK,IAAI,GAAG,YAAY,EAAG,OAAM,IAAI,MAAM,8BAA8B;AACrF,aAAW,UAAU,CAAC,QAAQ,MAAM,GAAG;AACrC,QAAI,MAAM,KAAK,QAAQ,MAAM,MAAM,CAAC,EAAE,KAAK,MAAM,MAAM,MAAM,KAAK,EAAG,QAAO;AAAA,EAC9E;AACA,QAAM,IAAI,MAAM,uDAAuD;AACzE;AAEO,SAAS,SAAS,KAAgD;AACvE,SAAO,EAAE,GAAG,OAAO,YAAY,OAAO,QAAQ,GAAG,EAAE,OAAO,CAAC,CAAC,KAAK,KAAK,MAAM,UAAU,UAAa,CAAC,gFAAgF,KAAK,GAAG,CAAC,CAAC,GAAG,gBAAgB,KAAK,sBAAsB,KAAK,0BAA0B,IAAI;AACjR;AAEA,eAAsB,YAAY,MAA6B;AAC7D,QAAM,MAAM,QAAQ,MAAM,oBAAoB;AAC9C,MAAI,CAAC,MAAM,MAAM,GAAG,EAAE,KAAK,MAAM,MAAM,MAAM,KAAK,EAAG;AACrD,QAAM,YAAY,MAAM,GAAG;AAC3B,QAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,MAAM,WAAW,KAAK,CAAC;AAC3E,aAAW,SAAS,SAAS;AAC3B,QAAI,MAAM,eAAe,EAAG,OAAM,YAAY,MAAM,KAAK,MAAM,YAAY,MAAM,IAAI,CAAC;AAAA,EACxF;AACF;",
6
+ "names": []
7
+ }
package/dist/setup.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/setup.ts
4
+ import { spawn } from "node:child_process";
5
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ var environment = join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "code-review-graph", "engine");
10
+ var pythonIn = (dir) => join(dir, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
11
+ async function command(exe, args, quiet = false) {
12
+ await new Promise((resolve2, reject) => {
13
+ const child = spawn(exe, args, { stdio: quiet ? "ignore" : "inherit", windowsHide: true });
14
+ child.on("error", reject);
15
+ child.on("exit", (code) => code === 0 ? resolve2() : reject(new Error(`${exe} exited ${code}`)));
16
+ });
17
+ }
18
+ async function prepare(source, python) {
19
+ const engine = JSON.parse(await readFile(new URL("../engine.json", import.meta.url), "utf8"));
20
+ const pinned = source ?? engine.source;
21
+ const stamp = join(environment, "dsh-crg-source.json");
22
+ if (await readFile(stamp, "utf8").then((value) => JSON.parse(value).source === pinned, () => false)) {
23
+ try {
24
+ await command(pythonIn(environment), ["-c", "from code_review_graph.refactor import get_refactor_edit_plan"], true);
25
+ console.log("CRG engine is already prepared.");
26
+ return;
27
+ } catch {
28
+ }
29
+ }
30
+ await mkdir(environment, { recursive: true });
31
+ let uv = false;
32
+ try {
33
+ await command("uv", ["--version"], true);
34
+ uv = true;
35
+ } catch {
36
+ }
37
+ if (uv && !python) {
38
+ await command("uv", ["venv", "--allow-existing", "--python", ">=3.10", environment]);
39
+ await command("uv", ["pip", "install", "--python", pythonIn(environment), pinned]);
40
+ } else {
41
+ let selected;
42
+ for (const candidate of python ? [{ exe: python, args: [] }] : [{ exe: "python3", args: [] }, { exe: "python", args: [] }, { exe: "py", args: ["-3"] }]) {
43
+ try {
44
+ await command(candidate.exe, [...candidate.args, "-c", "import sys; assert sys.version_info >= (3,10)"], true);
45
+ selected = candidate;
46
+ break;
47
+ } catch {
48
+ }
49
+ }
50
+ if (!selected) throw new Error("Install uv or real Python 3.10+; Windows Store aliases are not a Python runtime.");
51
+ await command(selected.exe, [...selected.args, "-m", "venv", environment]);
52
+ await command(pythonIn(environment), ["-m", "pip", "install", pinned]);
53
+ }
54
+ await command(pythonIn(environment), ["-c", "from code_review_graph.refactor import get_refactor_edit_plan"], true);
55
+ await writeFile(stamp, JSON.stringify({ source: pinned }), "utf8");
56
+ }
57
+ if (process.argv[1] && resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1])) {
58
+ if (process.argv[2] !== "prepare") {
59
+ console.error("Usage: dsh-crg prepare [engine-source] [--python executable]");
60
+ process.exitCode = 2;
61
+ } else {
62
+ const args = process.argv.slice(3);
63
+ const flag = args.indexOf("--python");
64
+ const python = flag >= 0 ? args.splice(flag, 2)[1] : void 0;
65
+ try {
66
+ await prepare(args[0], python);
67
+ } catch (error) {
68
+ console.error(String(error));
69
+ process.exitCode = 1;
70
+ }
71
+ }
72
+ }
73
+ export {
74
+ environment,
75
+ prepare,
76
+ pythonIn
77
+ };
78
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/setup.ts"],
4
+ "sourcesContent": ["#!/usr/bin/env node\nimport { spawn } from 'node:child_process';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport const environment = join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'code-review-graph', 'engine');\nexport const pythonIn = (dir: string) => join(dir, process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python');\nasync function command(exe: string, args: string[], quiet = false): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const child = spawn(exe, args, { stdio: quiet ? 'ignore' : 'inherit', windowsHide: true });\n child.on('error', reject); child.on('exit', code => code === 0 ? resolve() : reject(new Error(`${exe} exited ${code}`)));\n });\n}\nexport async function prepare(source?: string, python?: string): Promise<void> {\n const engine = JSON.parse(await readFile(new URL('../engine.json', import.meta.url), 'utf8'));\n const pinned = source ?? engine.source;\n const stamp = join(environment, 'dsh-crg-source.json');\n if (await readFile(stamp, 'utf8').then(value => JSON.parse(value).source === pinned, () => false)) {\n try { await command(pythonIn(environment), ['-c', 'from code_review_graph.refactor import get_refactor_edit_plan'], true); console.log('CRG engine is already prepared.'); return; } catch {}\n }\n await mkdir(environment, { recursive: true });\n let uv = false;\n try { await command('uv', ['--version'], true); uv = true; } catch {}\n if (uv && !python) {\n await command('uv', ['venv', '--allow-existing', '--python', '>=3.10', environment]);\n await command('uv', ['pip', 'install', '--python', pythonIn(environment), pinned]);\n } else {\n let selected: { exe: string; args: string[] } | undefined;\n for (const candidate of python ? [{ exe: python, args: [] }] : [{ exe: 'python3', args: [] }, { exe: 'python', args: [] }, { exe: 'py', args: ['-3'] }]) {\n try { await command(candidate.exe, [...candidate.args, '-c', 'import sys; assert sys.version_info >= (3,10)'], true); selected = candidate; break; } catch {}\n }\n if (!selected) throw new Error('Install uv or real Python 3.10+; Windows Store aliases are not a Python runtime.');\n await command(selected.exe, [...selected.args, '-m', 'venv', environment]);\n await command(pythonIn(environment), ['-m', 'pip', 'install', pinned]);\n }\n await command(pythonIn(environment), ['-c', 'from code_review_graph.refactor import get_refactor_edit_plan'], true);\n await writeFile(stamp, JSON.stringify({ source: pinned }), 'utf8');\n}\nif (process.argv[1] && resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1])) {\n if (process.argv[2] !== 'prepare') { console.error('Usage: dsh-crg prepare [engine-source] [--python executable]'); process.exitCode = 2; }\n else {\n const args = process.argv.slice(3);\n const flag = args.indexOf('--python');\n const python = flag >= 0 ? args.splice(flag, 2)[1] : undefined;\n try { await prepare(args[0], python); } catch (error) { console.error(String(error)); process.exitCode = 1; }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["resolve"]
7
+ }
@@ -0,0 +1,30 @@
1
+ // src/workflows.ts
2
+ var groups = ["context", "review", "refactor", "export", "advanced", "all"];
3
+ var context = ["build_or_update_graph_tool", "get_minimal_context_tool", "query_graph_tool", "semantic_search_nodes_tool", "list_graph_stats_tool", "get_docs_section_tool"];
4
+ var members = {
5
+ context,
6
+ review: [...context, "detect_changes_tool", "get_review_context_tool", "get_impact_radius_tool", "get_affected_flows_tool", "list_flows_tool", "get_flow_tool", "list_communities_tool"],
7
+ refactor: [...context, "refactor_tool", "apply_refactor_tool", "find_large_functions_tool", "get_impact_radius_tool", "get_affected_flows_tool", "detect_changes_tool"],
8
+ export: [...context, "generate_wiki_tool", "get_wiki_page_tool"],
9
+ advanced: [...context, "embed_graph_tool", "run_postprocess_tool", "list_repos_tool", "cross_repo_search_tool", "list_communities_tool", "get_community_tool", "get_architecture_overview_tool", "get_hub_nodes_tool", "get_bridge_nodes_tool", "get_knowledge_gaps_tool", "get_surprising_connections_tool", "get_suggested_questions_tool", "traverse_graph_tool", "detect_changes_tool", "list_flows_tool", "get_flow_tool", "find_large_functions_tool"]
10
+ };
11
+ var skillWorkflow = {
12
+ "build-graph": "advanced",
13
+ "debug-issue": "review",
14
+ "explore-codebase": "advanced",
15
+ "refactor-safely": "refactor",
16
+ "review-changes": "review",
17
+ "review-delta": "review",
18
+ "review-pr": "review"
19
+ };
20
+ var publicName = (raw) => `mcp__code_review_graph__${raw}`;
21
+ function exposed(workflow, names) {
22
+ return workflow === "all" ? names : names.filter((name) => members[workflow].includes(name));
23
+ }
24
+ export {
25
+ exposed,
26
+ groups,
27
+ publicName,
28
+ skillWorkflow
29
+ };
30
+ //# sourceMappingURL=workflows.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/workflows.ts"],
4
+ "sourcesContent": ["export const groups = ['context', 'review', 'refactor', 'export', 'advanced', 'all'] as const;\nexport type Workflow = typeof groups[number];\nconst context = ['build_or_update_graph_tool', 'get_minimal_context_tool', 'query_graph_tool', 'semantic_search_nodes_tool', 'list_graph_stats_tool', 'get_docs_section_tool'];\nconst members: Record<Exclude<Workflow, 'all'>, string[]> = {\n context,\n review: [...context, 'detect_changes_tool', 'get_review_context_tool', 'get_impact_radius_tool', 'get_affected_flows_tool', 'list_flows_tool', 'get_flow_tool', 'list_communities_tool'],\n refactor: [...context, 'refactor_tool', 'apply_refactor_tool', 'find_large_functions_tool', 'get_impact_radius_tool', 'get_affected_flows_tool', 'detect_changes_tool'],\n export: [...context, 'generate_wiki_tool', 'get_wiki_page_tool'],\n advanced: [...context, 'embed_graph_tool', 'run_postprocess_tool', 'list_repos_tool', 'cross_repo_search_tool', 'list_communities_tool', 'get_community_tool', 'get_architecture_overview_tool', 'get_hub_nodes_tool', 'get_bridge_nodes_tool', 'get_knowledge_gaps_tool', 'get_surprising_connections_tool', 'get_suggested_questions_tool', 'traverse_graph_tool', 'detect_changes_tool', 'list_flows_tool', 'get_flow_tool', 'find_large_functions_tool'],\n};\nexport const skillWorkflow: Record<string, Workflow> = {\n 'build-graph': 'advanced', 'debug-issue': 'review', 'explore-codebase': 'advanced',\n 'refactor-safely': 'refactor', 'review-changes': 'review', 'review-delta': 'review', 'review-pr': 'review',\n};\nexport const publicName = (raw: string): string => `mcp__code_review_graph__${raw}`;\nexport function exposed(workflow: Workflow, names: string[]): string[] {\n return workflow === 'all' ? names : names.filter(name => members[workflow].includes(name));\n}\n"],
5
+ "mappings": ";AAAO,IAAM,SAAS,CAAC,WAAW,UAAU,YAAY,UAAU,YAAY,KAAK;AAEnF,IAAM,UAAU,CAAC,8BAA8B,4BAA4B,oBAAoB,8BAA8B,yBAAyB,uBAAuB;AAC7K,IAAM,UAAsD;AAAA,EAC1D;AAAA,EACA,QAAQ,CAAC,GAAG,SAAS,uBAAuB,2BAA2B,0BAA0B,2BAA2B,mBAAmB,iBAAiB,uBAAuB;AAAA,EACvL,UAAU,CAAC,GAAG,SAAS,iBAAiB,uBAAuB,6BAA6B,0BAA0B,2BAA2B,qBAAqB;AAAA,EACtK,QAAQ,CAAC,GAAG,SAAS,sBAAsB,oBAAoB;AAAA,EAC/D,UAAU,CAAC,GAAG,SAAS,oBAAoB,wBAAwB,mBAAmB,0BAA0B,yBAAyB,sBAAsB,kCAAkC,sBAAsB,yBAAyB,2BAA2B,mCAAmC,gCAAgC,uBAAuB,uBAAuB,mBAAmB,iBAAiB,2BAA2B;AAC7b;AACO,IAAM,gBAA0C;AAAA,EACrD,eAAe;AAAA,EAAY,eAAe;AAAA,EAAU,oBAAoB;AAAA,EACxE,mBAAmB;AAAA,EAAY,kBAAkB;AAAA,EAAU,gBAAgB;AAAA,EAAU,aAAa;AACpG;AACO,IAAM,aAAa,CAAC,QAAwB,2BAA2B,GAAG;AAC1E,SAAS,QAAQ,UAAoB,OAA2B;AACrE,SAAO,aAAa,QAAQ,QAAQ,MAAM,OAAO,UAAQ,QAAQ,QAAQ,EAAE,SAAS,IAAI,CAAC;AAC3F;",
6
+ "names": []
7
+ }
@@ -0,0 +1,103 @@
1
+ # Capability map / 能力对应表
2
+
3
+ Baseline: CRG **2.3.9**, with the contribution fork's additional read-only edit
4
+ plan API. `capabilities.json` is captured from the real engine, rather than
5
+ maintained as a second handwritten schema.
6
+
7
+ 功能基线为 CRG **2.3.9**。工具参数来自真实引擎;DSH 仅移除模型可指定的
8
+ `repo_root`,通过当前会话绑定仓库,并在执行器检查权限与路径。
9
+
10
+ All original tools are native DSH ToolDefinitions named
11
+ `mcp__code_review_graph__<original_name>`. The prefix preserves upstream workflow
12
+ references; it does not require configuring an external DSH MCP server.
13
+
14
+ `crg_workflow` offers `context`, `review`, `refactor`, `export`, `advanced`, and
15
+ `all`. The five task groups each include the six context tools. `all` exposes all
16
+ 30 tools. Restrictions are scoped to one Agent and only this plugin's tools.
17
+ `crg_workflow`, `crg_prompt`, and `crg_visualize` remain available initially.
18
+
19
+ 按需展开仅影响调用 Agent;其他 Agent 和其他插件不受影响。所有工作流均可使用
20
+ 六个上下文基础工具,`all` 展开完整的 30 个工具。
21
+
22
+ | Original tool / 原工具 | Task group / 分组 | Behavior / 功能 |
23
+ | --- | --- | --- |
24
+ | `build_or_update_graph_tool` | context | Build and incremental update / 建图与增量更新 |
25
+ | `get_minimal_context_tool` | context | Task-focused code context / 精准任务上下文 |
26
+ | `query_graph_tool` | context | Node and relationship queries / 节点与关系查询 |
27
+ | `semantic_search_nodes_tool` | context | Semantic search with offline keyword fallback / 语义搜索及离线关键词回退 |
28
+ | `list_graph_stats_tool` | context | Graph statistics / 图谱统计 |
29
+ | `get_docs_section_tool` | context | Engine documentation / 引擎文档 |
30
+ | `detect_changes_tool` | review, refactor, advanced | Git change detection / 变更检测 |
31
+ | `get_review_context_tool` | review | Review context and source snippets / 审查上下文与源码片段 |
32
+ | `get_impact_radius_tool` | review, refactor | Change impact and call sites / 影响范围与调用位置 |
33
+ | `get_affected_flows_tool` | review, refactor | Affected execution flows / 受影响执行路径 |
34
+ | `list_flows_tool` | review, advanced | Flow inventory / 执行路径列表 |
35
+ | `get_flow_tool` | review, advanced | Flow details and bounded source / 执行路径详情 |
36
+ | `refactor_tool` | refactor | Rename preview, dead code, suggestions / 重命名预览、死代码与建议 |
37
+ | `apply_refactor_tool` | refactor | Preview or DSH-controlled file writes / 预览或受控文件修改 |
38
+ | `find_large_functions_tool` | refactor, advanced | Oversized function analysis / 大函数分析 |
39
+ | `generate_wiki_tool` | export | Repository wiki / 仓库知识库生成 |
40
+ | `get_wiki_page_tool` | export | Wiki page retrieval / 知识库页面查询 |
41
+ | `run_postprocess_tool` | advanced | Community, flow and embedding postprocessing / 社区、执行路径及嵌入后处理 |
42
+ | `embed_graph_tool` | advanced | Explicit authorized embedding / 主动授权的向量生成 |
43
+ | `list_repos_tool` | advanced | Only current and explicitly authorized roots / 当前及明确授权仓库 |
44
+ | `cross_repo_search_tool` | advanced | Search authorized repositories / 授权范围内跨仓库搜索 |
45
+ | `list_communities_tool` | review, advanced | Community inventory / 社区列表 |
46
+ | `get_community_tool` | advanced | Community details / 社区详情 |
47
+ | `get_architecture_overview_tool` | advanced | Architecture and coupling / 架构与耦合分析 |
48
+ | `get_hub_nodes_tool` | advanced | Central entities / 核心节点 |
49
+ | `get_bridge_nodes_tool` | advanced | Cross-community bridges / 跨社区桥接节点 |
50
+ | `get_knowledge_gaps_tool` | advanced | Coverage gaps / 知识缺口 |
51
+ | `get_surprising_connections_tool` | advanced | Unexpected relationships / 潜在关联 |
52
+ | `get_suggested_questions_tool` | advanced | Exploration questions / 探索问题建议 |
53
+ | `traverse_graph_tool` | advanced | Relationship traversal / 关系遍历 |
54
+
55
+ The internal `get_refactor_edit_plan_tool` is **not model-visible**. The Host
56
+ requests its full `files[{path,before_sha256,after_content,edit_count}]`, then
57
+ preflights every file and writes through native DSH file intents, policy and
58
+ `ctx.fs`. Presentation limits on rename previews never truncate the edit plan.
59
+ Failed writes return committed paths and a complete hash-guarded recovery patch.
60
+
61
+ 内部编辑计划接口不向模型展开;提交前校验全部文件的原始字节哈希。每个文件原子
62
+ 写入,保留 UTF-8、BOM 与换行。发生失败后停止后续写入,返回已提交文件和完整恢复补丁。
63
+
64
+ ## Five prompts / 五个工作流提示
65
+
66
+ Call `crg_prompt` with `name` and optional `base`/`description`. Text comes from
67
+ the engine, with original tool names rewritten to their native DSH names.
68
+
69
+ | Prompt | Selected group / 自动展开 |
70
+ | --- | --- |
71
+ | `review_changes` | review |
72
+ | `architecture_map` | advanced |
73
+ | `debug_issue` | review |
74
+ | `onboard_developer` | advanced |
75
+ | `pre_merge_check` | all |
76
+
77
+ ## Seven skills / 七个技能
78
+
79
+ Bundled upstream content is registered through DSH's native skill service. Each
80
+ skill begins by selecting the relevant workflow; runtime tool references are
81
+ rewritten consistently.
82
+
83
+ | Upstream skill | Native skill | Selected group |
84
+ | --- | --- | --- |
85
+ | build-graph | `crg-build-graph` | advanced |
86
+ | debug-issue | `crg-debug-issue` | review |
87
+ | explore-codebase | `crg-explore-codebase` | advanced |
88
+ | refactor-safely | `crg-refactor-safely` | refactor |
89
+ | review-changes | `crg-review-changes` | review |
90
+ | review-delta | `crg-review-delta` | review |
91
+ | review-pr | `crg-review-pr` | review |
92
+
93
+ ## Graph and export / 图谱与导出
94
+
95
+ `crg_visualize` reuses the upstream renderer and CLI: full, file and community
96
+ views; file/symbol/changed-code/flow seeds; shortest paths; depth and size limits;
97
+ HTML, JSON, GraphML, Cypher, Obsidian, and optional SVG exports. SVG requires the
98
+ engine's optional visualization dependencies. The Web/Desktop client uses the
99
+ two released workspace readers and displays an offline sandboxed iframe.
100
+ Headless tools return the generated local path.
101
+
102
+ 图谱侧栏保留搜索、过滤、缩放、拖动、聚合、路径视图和 HTML 下载。全部资源通过
103
+ 工作区文件通道读取并内联;iframe 禁止同源访问和联网,不启动固定端口服务。
@@ -0,0 +1,113 @@
1
+ # Validation / 验证记录
2
+
3
+ Local checkpoint: **2026-10-08**, Windows, Node **24.14.1**, Python **3.11.16**.
4
+ CRG base **2.3.9**; exact contribution engine commit is in `engine.json`.
5
+ DSH packages are the real published releases **0.1.5-rc.2** and **0.2.0-rc.2**.
6
+
7
+ Windows 本地验证及 Windows/Linux/macOS 的远程 CI 均已通过。桌面版已检查
8
+ 原生插件 ownership 规则及共享 Client 图谱组件,**尚未启动 Electron 应用进行整机实测**。
9
+ 这些范围不同,不能把组件测试描述为桌面整机验收。未运行真实模型推理。
10
+
11
+ ## Results
12
+
13
+ | Check | Local result | Scope |
14
+ | --- | --- | --- |
15
+ | TypeScript and Host/Client build | Passed | Prebuilt ESM Host, CLI, browser Client |
16
+ | Plugin safety contracts | 4 passed | Root/linked-directory fences, all 30 tools, both readers, offline iframe, full refactor recovery |
17
+ | Real engine worker lifecycle | 3 passed | Shared root worker, last owner cleanup, resumed owner, cancellation, timeout, missing engine |
18
+ | DSH 0.1.5-rc.2 session | Passed | 52 recorded deterministic tool calls, including 9 expected refusals |
19
+ | DSH 0.2.0-rc.2 session | Passed | Same native session and real engine checks |
20
+ | Browser graph on both readers | Passed | Full/file/community views, search results, filters, zoom, drag, community drill/back, HTML download; zero external requests and page errors |
21
+ | Native CLI lifecycle on both releases | Passed | Isolated web/headless profiles, add/readd/remove, bundle patch composition, retained user data, desktop profile ownership rejection |
22
+ | Explicit dependency preparation | Passed | Isolated real engine, read-only plan API probe, repeat preparation without package installation |
23
+ | Core Python regression set | 805 passed, 4 skipped | All changed platform/refactor/renderer/source-fence areas and their related regressions |
24
+ | Ruff for changed Python sources/tests | Passed | All modified Python files |
25
+ | Windows/Linux/macOS × both DSH releases | 6 CI jobs passed | Real pinned engine, isolated native installation, session calls, browser graph and explicit setup |
26
+ | Core Windows/Linux/macOS regression matrix | 3 CI jobs passed | 808 passed, 1 platform-specific skip on each OS |
27
+ | Electron application launch | Not run | Native shared Client contribution and ownership are covered separately |
28
+ | Real model inference | Not run | Deterministic automation only, no paid inference jobs |
29
+
30
+ The four local core skips comprise three existing symlink checks requiring
31
+ Windows privileges (one uninstall case and two watcher cases) and a POSIX
32
+ executable-bit check. The plugin's directory-junction escape checks **did run
33
+ and pass** without those privileges. CI runners have symlink privileges: their
34
+ one skip is the POSIX executable-bit check on Windows, or the native Windows
35
+ path-semantics check on Linux/macOS.
36
+ SVG export passed with the explicitly prepared `[eval]` dependency set. Actual
37
+ embedding model inference is not included; optional dependencies/models must be
38
+ prepared explicitly.
39
+
40
+ ## Session evidence
41
+
42
+ - [DSH 0.1.5-rc.2 transcript](https://github.com/sumingwang233/dsh-code-review-graph/blob/main/docs/evidence/session-0.1.5-rc.2-win32.json)
43
+ - [DSH 0.2.0-rc.2 transcript](https://github.com/sumingwang233/dsh-code-review-graph/blob/main/docs/evidence/session-0.2.0-rc.2-win32.json)
44
+
45
+ The test uses actual released Cordis, Session, AgentRegistry, ToolRuntime,
46
+ filesystem and session-policy packages; its Agents are created deterministically
47
+ without a model loop. Tools invoke the real Python CRG backend. It checks:
48
+
49
+ - Agent-local tool selection and independent repositories; first graph build;
50
+ real watcher indexing after source changes; surviving owners after one exits.
51
+ - All five original prompts; context, review, impact, affected flows,
52
+ architecture, cross-repository search and postprocessing results.
53
+ - Unauthorized repository/provider/root/path inputs and linked documentation;
54
+ stale rename plans;
55
+ native session read-only policy rejection; exact real BOM/CRLF preservation.
56
+ - HTML and JSON/GraphML/Cypher/Obsidian/SVG generation and a shortest-path view;
57
+ all seven native skills are registered and their tool references are adapted.
58
+
59
+ Only test-directory paths are replaced by `<TEST_TMP>` for public evidence.
60
+ Where ToolRuntime returns both a structured value and identical rendered text,
61
+ the transcript retains the structured value and omits duplicate rendering;
62
+ error content is kept. Upstream reviewers decide whether deterministic evidence
63
+ satisfies their contribution requirement.
64
+
65
+ The Playwright harness imports the **actual native client contribution** and
66
+ mounts its graph in Chromium, using a fixture for each release's workspace asset
67
+ reader. It checks native sidebar identity/slot registration. This verifies the
68
+ shared Web/Desktop component, not an entire DSH Web server or Electron process.
69
+
70
+ ## Reproduction
71
+
72
+ ```sh
73
+ npm ci
74
+ npm run typecheck
75
+ npm run build
76
+ npm test
77
+ npm run prepare:tests
78
+ npm run test:backend
79
+ npm run test:integration
80
+ npx playwright install chromium
81
+ npm run test:browser
82
+ npm pack
83
+ npm run test:install
84
+ npm run test:setup
85
+ ```
86
+
87
+ Provide `DSH_VERSION` and `CRG_COMMAND` in the **test child process environment**.
88
+ `CRG_COMMAND` points to a prepared real engine. Setup tests and normal
89
+ installation use the publicly available exact source in `engine.json`.
90
+ Test profiles use temporary
91
+ `DSH_HOME` values and never modify the user's DSH profiles or credentials.
92
+
93
+ Core regression command:
94
+
95
+ ```sh
96
+ python -X utf8 -m pytest tests/test_cli_install.py tests/test_dsh.py tests/test_uninstall.py tests/test_refactor_edit_plan.py tests/test_refactor.py tests/test_skills.py tests/test_main.py tests/test_prompts.py tests/test_dsh_renderer.py tests/test_visualization.py tests/test_dsh_source_fence.py tests/test_incremental.py tests/test_flows.py tests/test_tools.py tests/test_review_utf8_content.py -q
97
+ ```
98
+
99
+ The adapter's `.github/workflows/ci.yml` runs both exact releases on Windows,
100
+ Linux and macOS using a real pinned engine, isolated profile lifecycle, browser
101
+ checks and dependency preparation. The core's `dsh-platform.yml` runs the Python
102
+ contract/regression set on the same three OS families.
103
+
104
+ - [Adapter CI: all six combinations passed](https://github.com/sumingwang233/dsh-code-review-graph/actions/runs/37716247823), source `9c478191b743ecc443cc0eede5d67927a367d7ed`.
105
+ - [Core CI: all three OS jobs passed](https://github.com/sumingwang233/code-review-graph/actions/runs/37717599685), source `8ac789138463cd3b8282688bb94d682e90ca1df3`.
106
+
107
+ Each adapter CI artifact contains its native session transcript, browser report,
108
+ profile lifecycle report and tested package. Windows records 52 calls per
109
+ release; Linux/macOS record 53. Each transcript contains nine expected refusals.
110
+ Browser reports have zero external requests and page errors. The pinned engine
111
+ commit `9eba19ffaa60fda02a459a844067e5c190c9013d` has identical Python production
112
+ code and dependency metadata to the tested core head; later commits only adjust
113
+ CI and resolve the existing Bash regression test's executable path on Windows.
package/engine.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "baseline": "2.3.9",
3
+ "repository": "https://github.com/sumingwang233/code-review-graph",
4
+ "commit": "9eba19ffaa60fda02a459a844067e5c190c9013d",
5
+ "source": "code-review-graph @ https://github.com/sumingwang233/code-review-graph/archive/9eba19ffaa60fda02a459a844067e5c190c9013d.zip",
6
+ "upstreamBase": "6b12d11625cbec3b6773e076cb3d136464fa90e5",
7
+ "reason": "Read-only get_refactor_edit_plan_tool is not yet available in an official CRG release."
8
+ }
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "dsh-code-review-graph",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Local code intelligence, review workflows and interactive graphs for DeepSeek Harness",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": "github:sumingwang233/dsh-code-review-graph",
8
+ "engines": { "node": ">=22.19" },
9
+ "exports": { ".": "./dist/index.js", "./client": "./dist/client.js", "./package.json": "./package.json" },
10
+ "bin": { "dsh-crg": "./dist/setup.js" },
11
+ "files": ["dist", "cordis.patch.yml", "engine.json", "capabilities.json", "skills", "LICENSE", "NOTICE", "README.md", "README.en.md", "docs/capabilities.md", "docs/validation.md"],
12
+ "dsh": { "bundle": { "patch": "./cordis.patch.yml" }, "client": { "platform": "web", "inject": ["@deepseek-ai/dsh-api-workspace-files", "@deepseek-ai/dsh-client-ui-sidebar-right", "@deepseek-ai/dsh-api-remotes"] } },
13
+ "scripts": {
14
+ "build": "node scripts/build.mjs",
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "node --test tests/*.test.mjs",
17
+ "test:integration": "node --test tests/integration.mjs",
18
+ "test:browser": "node --test tests/browser.mjs",
19
+ "test:install": "node --test tests/install.mjs",
20
+ "test:setup": "node --test tests/setup.mjs",
21
+ "test:backend": "node --test tests/backend.mjs",
22
+ "prepare:tests": "node scripts/prepare-test-runtime.mjs",
23
+ "prepack": "npm run typecheck && npm run build"
24
+ },
25
+ "dependencies": { "@modelcontextprotocol/sdk": "1.32.1", "ajv": "8.20.0" },
26
+ "peerDependencies": { "@deepseek-ai/cordis": "^4.0.0", "@deepseek-ai/schemastery": "^3.18.0", "react": "^18.2.0" },
27
+ "devDependencies": {
28
+ "@deepseek-ai/cordis": "4.0.4", "@deepseek-ai/schemastery": "3.18.4",
29
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2", "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
30
+ "@deepseek-ai/dsh-session": "0.2.0-rc.2", "@deepseek-ai/dsh-fs": "0.2.0-rc.2",
31
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2", "@deepseek-ai/dsh-skill": "0.2.0-rc.2",
32
+ "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
33
+ "@deepseek-ai/dsh-client-ui-sidebar-right": "0.2.0-rc.2", "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2", "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
34
+ "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2", "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2",
35
+ "@deepseek-ai/dsh-api-workspace-files": "0.2.0-rc.2",
36
+ "@types/node": "^24.0.0", "@types/react": "^18.3.0", "react": "18.3.1", "react-dom": "18.3.1",
37
+ "typescript": "^5.9.0", "esbuild": "^0.25.0", "@playwright/test": "^1.55.0"
38
+ }
39
+ }
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: build-graph
3
+ description: Build or update the code review knowledge graph. Run this first to initialize, or let hooks keep it updated automatically.
4
+ argument-hint: "[full]"
5
+ ---
6
+
7
+ # Build Graph
8
+
9
+ Build or incrementally update the knowledge graph for this repository.
10
+
11
+ ## Steps
12
+
13
+ 1. Call `list_graph_stats_tool`. If `last_updated` is null, the graph has never been built.
14
+ 2. For a first build, or when the argument is `full`, call `build_or_update_graph_tool(full_rebuild=True)`. Otherwise call `build_or_update_graph_tool()` for an incremental update.
15
+ 3. Report the response: `status` (`ok`, `partial` or `error`) and `summary`.
16
+
17
+ ## When to Use
18
+
19
+ - First set-up of a repository, or after a branch switch or large refactor.
20
+ - When the graph looks stale. Hooks installed by `code-review-graph install` run an update after each edit and before each commit, so manual builds are rarely needed.
21
+
22
+ ## Notes
23
+
24
+ - The database is `.code-review-graph/graph.db` in the repository root.
25
+ - Binary files, dependency and build directories, and patterns in `.code-review-graphignore` are skipped.
26
+ - For the list of supported languages call `get_docs_section_tool(section_name="languages")`.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: debug-issue
3
+ description: Systematically debug issues using graph-powered code navigation
4
+ ---
5
+
6
+ ## Debug Issue
7
+
8
+ Trace a bug through the knowledge graph before reading source.
9
+
10
+ ### Steps
11
+
12
+ 1. Call `semantic_search_nodes_tool` to find code related to the issue.
13
+ 2. Call `query_graph_tool` with `callers_of` and `callees_of` to trace the call chain in both directions.
14
+ 3. Call `get_flow_tool` for the execution path that reaches the suspect code. Its entry point is where the bug is triggered.
15
+ 4. Call `detect_changes_tool` to check whether a recent change caused the issue.
16
+ 5. Call `get_impact_radius_tool` on the suspect files to see what a fix would affect.
17
+
18
+ ## Token Efficiency Rules
19
+ - Call `get_minimal_context_tool(task="<your task>")` before any other graph tool.
20
+ - Pass `detail_level="minimal"` wherever a tool accepts it. Use "standard" only when minimal is not enough.
21
+ - Prefer a targeted `query_graph_tool` call over a broad listing call.
22
+ - Budget: about five tool calls and 800 tokens of graph output per task.
23
+ - Read the implementation and its tests before changing code. The graph narrows scope; it does not replace the source.