dsh-code-review-graph 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,30 @@
1
+ # 0.1.1
2
+
3
+ 修复 DSH Web/桌面客户端的原生加载和图谱侧栏显示:
4
+
5
+ - Client 发行文件改用 DSH 官方 closure-factory 格式,解决 `Cannot use import statement outside a module`。
6
+ - 侧栏正文按插件 provider ID 注册,解决打开图谱后显示“这类内容还没有可用的查看方式”。
7
+ - 浏览器回归直接执行发行文件;补充真实 Windows Desktop 0.2.0-rc.2 的隔离安装、启用、图谱读取、搜索、HTML 导出和卸载/重装验证。
8
+
9
+ 桌面安装 `dsh-code-review-graph@0.1.1`,完成后点击“立即启用”。已有旧版先通过
10
+ 原生插件管理器卸载;安装状态尚未确认时先核对状态。
11
+
12
+ 用户报告的 pnpm 十分钟无输出超时未能复现。相同旧包及原有依赖组合均可正常安装;
13
+ 本次修复不代表该安装器超时已解决。其他桌面平台/版本未整机实测,详细范围见
14
+ [验证记录](validation.md)。没有运行模型推理,也未修改用户的 DSH 配置。
15
+
16
+ ## English
17
+
18
+ - Emit the Client in DSH's native closure-factory format, fixing `Cannot use import statement outside a module` in the actual loader.
19
+ - Register the sidebar body under the plugin provider ID, fixing the unavailable-view fallback when opening a graph.
20
+ - Execute the published Client in browser regressions; add isolated installation, enablement, native graph reading/search/HTML export, removal and reinstallation checks using the actual Windows Desktop 0.2.0-rc.2 application.
21
+
22
+ Install `dsh-code-review-graph@0.1.1` in the native Desktop plugin manager, then
23
+ choose **Enable now**. Remove an existing old version through that manager first;
24
+ reconcile an unconfirmed installation before proceeding.
25
+
26
+ The reported ten-minute pnpm no-output timeout did not reproduce with the same
27
+ old package and dependency combination. This release does not claim to resolve
28
+ that installer timeout. Other Desktop platforms/releases have not been tested as
29
+ complete applications. See [validation](validation.md) for scope. No model
30
+ inference or changes to the user's DSH configuration were involved.
@@ -0,0 +1,161 @@
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
+ 0.1.1 修复了真实原生加载器才会暴露的两个问题:Client 发行文件必须使用
8
+ `window.__ModuleLoader__.load({ id, factory })`,侧栏正文必须按 provider ID 注册。
9
+ 0.1.0 的浏览器测试重新打包了源文件,并错误地接受了按 kind 注册正文,未发现这两个问题。
10
+ 回归测试现在直接执行发行 Client 文件,并检查 provider ID。
11
+
12
+ Windows 本地增加实际安装的 DSH Desktop **0.2.0-rc.2** 验证;三平台、两版 DSH
13
+ 的 CI 覆盖原生运行时、CLI 和浏览器组件。Desktop 0.1.5-rc.2、Linux/macOS
14
+ 桌面应用尚未整机实测。未运行真实模型推理。
15
+
16
+ ## Results
17
+
18
+ | Check | Local result | Scope |
19
+ | --- | --- | --- |
20
+ | TypeScript and Host/Client build | Passed | ESM Host/CLI; native closure-factory browser Client |
21
+ | Plugin safety and native Client contracts | 5 passed | Root/linked-directory fences, all 30 tools, both readers, offline iframe, full refactor recovery; actual published Client executes as a classic script |
22
+ | Real engine worker lifecycle | 3 passed | Shared root worker, last owner cleanup, resumed owner, cancellation, timeout, missing engine |
23
+ | DSH 0.1.5-rc.2 session | Passed | 52 recorded deterministic tool calls, including 9 expected refusals |
24
+ | DSH 0.2.0-rc.2 session | Passed | Same native session and real engine checks |
25
+ | 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 |
26
+ | Native CLI lifecycle on both releases | Passed | Isolated web/headless profiles, add/readd/remove, bundle patch composition, retained user data, desktop profile ownership rejection |
27
+ | Explicit dependency preparation | Passed | Isolated real engine, read-only plan API probe, repeat preparation without package installation |
28
+ | Core Python regression set | 805 passed, 4 skipped | All changed platform/refactor/renderer/source-fence areas and their related regressions |
29
+ | Ruff for changed Python sources/tests | Passed | All modified Python files |
30
+ | Windows/Linux/macOS × both DSH releases | 6 CI jobs passed | Real pinned engine, isolated native installation, session calls, browser graph and explicit setup |
31
+ | Core Windows/Linux/macOS regression matrix | 3 CI jobs passed | 808 passed, 1 platform-specific skip on each OS |
32
+ | Windows Desktop 0.2.0-rc.2 | Passed | Actual packaged Electron application, native install/enable/remove/reinstall, graph reading/search/HTML download, unrelated configuration retained; no model |
33
+ | Other Desktop releases/platforms | Not run | Browser/CLI checks do not establish complete Electron coverage |
34
+ | Real model inference | Not run | Deterministic automation only, no paid inference jobs |
35
+
36
+ The four local core skips comprise three existing symlink checks requiring
37
+ Windows privileges (one uninstall case and two watcher cases) and a POSIX
38
+ executable-bit check. The plugin's directory-junction escape checks **did run
39
+ and pass** without those privileges. CI runners have symlink privileges: their
40
+ one skip is the POSIX executable-bit check on Windows, or the native Windows
41
+ path-semantics check on Linux/macOS.
42
+ SVG export passed with the explicitly prepared `[eval]` dependency set. Actual
43
+ embedding model inference is not included; optional dependencies/models must be
44
+ prepared explicitly.
45
+
46
+ ## Session evidence
47
+
48
+ - [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)
49
+ - [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)
50
+
51
+ The test uses actual released Cordis, Session, AgentRegistry, ToolRuntime,
52
+ filesystem and session-policy packages; its Agents are created deterministically
53
+ without a model loop. Tools invoke the real Python CRG backend. It checks:
54
+
55
+ - Agent-local tool selection and independent repositories; first graph build;
56
+ real watcher indexing after source changes; surviving owners after one exits.
57
+ - All five original prompts; context, review, impact, affected flows,
58
+ architecture, cross-repository search and postprocessing results.
59
+ - Unauthorized repository/provider/root/path inputs and linked documentation;
60
+ stale rename plans;
61
+ native session read-only policy rejection; exact real BOM/CRLF preservation.
62
+ - HTML and JSON/GraphML/Cypher/Obsidian/SVG generation and a shortest-path view;
63
+ all seven native skills are registered and their tool references are adapted.
64
+
65
+ Only test-directory paths are replaced by `<TEST_TMP>` for public evidence.
66
+ Where ToolRuntime returns both a structured value and identical rendered text,
67
+ the transcript retains the structured value and omits duplicate rendering;
68
+ error content is kept. Upstream reviewers decide whether deterministic evidence
69
+ satisfies their contribution requirement.
70
+
71
+ The Playwright browser harness executes **`dist/client.js` as published**, through
72
+ the native classic-script factory contract, and mounts its graph in Chromium.
73
+ Only the host services and each release's workspace reader are fixtures. It
74
+ checks provider ID/slot registration. This remains a component test, separate
75
+ from the real Desktop test.
76
+
77
+ ## Desktop regression and reported timeout
78
+
79
+ - [Clean Desktop profile evidence](evidence/desktop-0.2.0-rc.2-win32-clean.json)
80
+ - [Existing dependency combination evidence](evidence/desktop-0.2.0-rc.2-win32-dependency-combination.json)
81
+
82
+ `tests/desktop.mjs` launches the actual installed Electron executable with a
83
+ temporary DSH_HOME, userData, HOME/USERPROFILE and APPDATA. It uses the native
84
+ plugin manager to install the packed tarball, enable it, uninstall it, reinstall
85
+ it and uninstall it again. It checks that existing dependencies, bundles and an
86
+ unrelated user setting survive. The real engine CLI builds a small Python graph;
87
+ the Desktop sidebar reads it through the actual workspace-file service, searches
88
+ it and exports the HTML through Electron's download pipeline. Session creation
89
+ is deterministic, without model inference. This complements the real DSH tool
90
+ session transcripts; it does not claim that the model drove the Desktop UI.
91
+
92
+ The official Desktop Host hardcodes port 19387. A test-only preload changes that
93
+ test process's listener to an OS-assigned port so the user's running Desktop is
94
+ untouched. Test windows stay hidden; the native welcome flow uses “Set up later”
95
+ without signing in, copying credentials or configuring a model key. No test
96
+ settings are written to the user's profile.
97
+
98
+ The reported pnpm log said `Done in 4.1s`, followed by the Host's 600000 ms
99
+ no-output timeout. The same 0.1.0 package completed successfully with bundled
100
+ Electron/pnpm, a clean native Desktop profile, and a native Desktop profile
101
+ containing the three existing plugins and package-manager policy files. The
102
+ latter reproduced the same `+98 -27` dependency change. The original hang has
103
+ **not been reproduced**. 0.1.1 fixes the two independently confirmed Client
104
+ defects and the missing regression coverage; it does not claim to fix that
105
+ unresolved package-manager timeout.
106
+
107
+ ## Reproduction
108
+
109
+ ```sh
110
+ npm ci
111
+ npm run typecheck
112
+ npm run build
113
+ npm test
114
+ npm run prepare:tests
115
+ npm run test:backend
116
+ npm run test:integration
117
+ npx playwright install chromium
118
+ npm run test:browser
119
+ npm pack
120
+ npm run test:install
121
+ npm run test:setup
122
+ ```
123
+
124
+ Provide `DSH_VERSION` and `CRG_COMMAND` in the **test child process environment**.
125
+ `CRG_COMMAND` points to a prepared real engine. Setup tests and normal
126
+ installation use the publicly available exact source in `engine.json`.
127
+ Test profiles use temporary
128
+ `DSH_HOME` values and never modify the user's DSH profiles or credentials.
129
+
130
+ For the separate real Desktop check, first run `npm pack`, set
131
+ `DSH_DESKTOP_EXECUTABLE` to a supported installed Desktop application and
132
+ `CRG_COMMAND` to a prepared real engine, then run `npm run test:desktop`.
133
+ `DSH_VERSION` must match the actual application version (default 0.2.0-rc.2).
134
+ Missing application/engine inputs fail explicitly. To reproduce a dependency
135
+ combination, additionally set `DSH_DESKTOP_PROFILE_FIXTURE` to a profile directory
136
+ and `DSH_DESKTOP_PNPM` to the application's bundled pnpm entry. Only
137
+ `package.json`, `pnpm-lock.yaml` and `pnpm-workspace.yaml` are copied to the
138
+ temporary profile. Evidence is written to `test-results/desktop-*.json` with the
139
+ tested package SHA-256 and native operation results.
140
+
141
+ Core regression command:
142
+
143
+ ```sh
144
+ 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
145
+ ```
146
+
147
+ The adapter's `.github/workflows/ci.yml` runs both exact releases on Windows,
148
+ Linux and macOS using a real pinned engine, isolated profile lifecycle, browser
149
+ checks and dependency preparation. The core's `dsh-platform.yml` runs the Python
150
+ contract/regression set on the same three OS families.
151
+
152
+ - [Historical 0.1.0 adapter CI: six combinations passed](https://github.com/sumingwang233/dsh-code-review-graph/actions/runs/37716247823), source `9c478191b743ecc443cc0eede5d67927a367d7ed`; the old browser harness missed the Client packaging/slot defects described above.
153
+ - [Core CI: all three OS jobs passed](https://github.com/sumingwang233/code-review-graph/actions/runs/37717599685), source `8ac789138463cd3b8282688bb94d682e90ca1df3`.
154
+
155
+ Each adapter CI artifact contains its native session transcript, browser report,
156
+ profile lifecycle report and tested package. Windows records 52 calls per
157
+ release; Linux/macOS record 53. Each transcript contains nine expected refusals.
158
+ Browser reports have zero external requests and page errors. The pinned engine
159
+ commit `9eba19ffaa60fda02a459a844067e5c190c9013d` has identical Python production
160
+ code and dependency metadata to the tested core head; later commits only adjust
161
+ 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
+ }