@deeplake/hivemind 0.7.75 → 0.7.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/bundle/cli.js +1077 -230
- package/codex/bundle/graph-on-stop.js +3148 -0
- package/codex/bundle/graph-pull-worker.js +40 -1
- package/codex/bundle/pre-tool-use.js +1237 -21
- package/codex/bundle/session-start.js +49 -0
- package/codex/bundle/shell/deeplake-shell.js +725 -20
- package/codex/skills/hivemind-graph/SKILL.md +94 -0
- package/cursor/bundle/graph-on-stop.js +3148 -0
- package/cursor/bundle/graph-pull-worker.js +40 -1
- package/cursor/bundle/pre-tool-use.js +1232 -8
- package/cursor/bundle/session-start.js +263 -7
- package/cursor/bundle/shell/deeplake-shell.js +725 -20
- package/hermes/bundle/graph-on-stop.js +3148 -0
- package/hermes/bundle/graph-pull-worker.js +40 -1
- package/hermes/bundle/pre-tool-use.js +1225 -8
- package/hermes/bundle/session-start.js +262 -8
- package/hermes/bundle/shell/deeplake-shell.js +725 -20
- package/openclaw/dist/index.js +28 -1
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/package.json +1 -1
- package/package.json +2 -1
- package/scripts/ensure-tree-sitter.mjs +6 -1
|
@@ -54,13 +54,13 @@ var init_index_marker_store = __esm({
|
|
|
54
54
|
|
|
55
55
|
// dist/src/utils/stdin.js
|
|
56
56
|
function readStdin() {
|
|
57
|
-
return new Promise((
|
|
57
|
+
return new Promise((resolve4, reject) => {
|
|
58
58
|
let data = "";
|
|
59
59
|
process.stdin.setEncoding("utf-8");
|
|
60
60
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
61
61
|
process.stdin.on("end", () => {
|
|
62
62
|
try {
|
|
63
|
-
|
|
63
|
+
resolve4(JSON.parse(data));
|
|
64
64
|
} catch (err) {
|
|
65
65
|
reject(new Error(`Failed to parse hook input: ${err}`));
|
|
66
66
|
}
|
|
@@ -522,7 +522,7 @@ function getQueryTimeoutMs() {
|
|
|
522
522
|
return Number(process.env.HIVEMIND_QUERY_TIMEOUT_MS ?? 1e4);
|
|
523
523
|
}
|
|
524
524
|
function sleep2(ms) {
|
|
525
|
-
return new Promise((
|
|
525
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
526
526
|
}
|
|
527
527
|
function isTimeoutError(error) {
|
|
528
528
|
const name = error instanceof Error ? error.name.toLowerCase() : "";
|
|
@@ -552,7 +552,7 @@ var Semaphore = class {
|
|
|
552
552
|
this.active++;
|
|
553
553
|
return;
|
|
554
554
|
}
|
|
555
|
-
await new Promise((
|
|
555
|
+
await new Promise((resolve4) => this.waiting.push(resolve4));
|
|
556
556
|
}
|
|
557
557
|
release() {
|
|
558
558
|
this.active--;
|
|
@@ -1710,7 +1710,7 @@ var EmbedClient = class {
|
|
|
1710
1710
|
}
|
|
1711
1711
|
}
|
|
1712
1712
|
connectOnce() {
|
|
1713
|
-
return new Promise((
|
|
1713
|
+
return new Promise((resolve4, reject) => {
|
|
1714
1714
|
const sock = connect(this.socketPath);
|
|
1715
1715
|
const to = setTimeout(() => {
|
|
1716
1716
|
sock.destroy();
|
|
@@ -1718,7 +1718,7 @@ var EmbedClient = class {
|
|
|
1718
1718
|
}, this.timeoutMs);
|
|
1719
1719
|
sock.once("connect", () => {
|
|
1720
1720
|
clearTimeout(to);
|
|
1721
|
-
|
|
1721
|
+
resolve4(sock);
|
|
1722
1722
|
});
|
|
1723
1723
|
sock.once("error", (e) => {
|
|
1724
1724
|
clearTimeout(to);
|
|
@@ -1800,7 +1800,7 @@ var EmbedClient = class {
|
|
|
1800
1800
|
throw new Error("daemon did not become ready within spawnWaitMs");
|
|
1801
1801
|
}
|
|
1802
1802
|
sendAndWait(sock, req) {
|
|
1803
|
-
return new Promise((
|
|
1803
|
+
return new Promise((resolve4, reject) => {
|
|
1804
1804
|
let buf = "";
|
|
1805
1805
|
const to = setTimeout(() => {
|
|
1806
1806
|
sock.destroy();
|
|
@@ -1815,7 +1815,7 @@ var EmbedClient = class {
|
|
|
1815
1815
|
const line = buf.slice(0, nl);
|
|
1816
1816
|
clearTimeout(to);
|
|
1817
1817
|
try {
|
|
1818
|
-
|
|
1818
|
+
resolve4(JSON.parse(line));
|
|
1819
1819
|
} catch (e) {
|
|
1820
1820
|
reject(e);
|
|
1821
1821
|
}
|
|
@@ -2317,6 +2317,1217 @@ async function handleGrepDirect(api, table, sessionsTable, params) {
|
|
|
2317
2317
|
return capOutputForClaude(joined, { kind: "grep" });
|
|
2318
2318
|
}
|
|
2319
2319
|
|
|
2320
|
+
// dist/src/graph/vfs-handler.js
|
|
2321
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync9, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2322
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2323
|
+
import { join as join13, dirname as dirname6 } from "node:path";
|
|
2324
|
+
|
|
2325
|
+
// dist/src/graph/last-build.js
|
|
2326
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync7, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2327
|
+
import { dirname as dirname3, join as join10 } from "node:path";
|
|
2328
|
+
function lastBuildPath(baseDir, worktreeId) {
|
|
2329
|
+
if (worktreeId !== void 0) {
|
|
2330
|
+
return join10(baseDir, "worktrees", worktreeId, ".last-build.json");
|
|
2331
|
+
}
|
|
2332
|
+
return join10(baseDir, ".last-build.json");
|
|
2333
|
+
}
|
|
2334
|
+
function readLastBuild(baseDir, worktreeId) {
|
|
2335
|
+
let path = lastBuildPath(baseDir, worktreeId);
|
|
2336
|
+
if (!existsSync5(path)) {
|
|
2337
|
+
if (worktreeId === void 0)
|
|
2338
|
+
return null;
|
|
2339
|
+
const legacy = lastBuildPath(baseDir, void 0);
|
|
2340
|
+
if (!existsSync5(legacy))
|
|
2341
|
+
return null;
|
|
2342
|
+
path = legacy;
|
|
2343
|
+
}
|
|
2344
|
+
let raw;
|
|
2345
|
+
try {
|
|
2346
|
+
raw = readFileSync7(path, "utf8");
|
|
2347
|
+
} catch {
|
|
2348
|
+
return null;
|
|
2349
|
+
}
|
|
2350
|
+
let parsed;
|
|
2351
|
+
try {
|
|
2352
|
+
parsed = JSON.parse(raw);
|
|
2353
|
+
} catch {
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
if (parsed === null || typeof parsed !== "object")
|
|
2357
|
+
return null;
|
|
2358
|
+
const o = parsed;
|
|
2359
|
+
if (typeof o.ts !== "number" || !Number.isFinite(o.ts))
|
|
2360
|
+
return null;
|
|
2361
|
+
if (o.commit_sha !== null && typeof o.commit_sha !== "string")
|
|
2362
|
+
return null;
|
|
2363
|
+
if (typeof o.snapshot_sha256 !== "string")
|
|
2364
|
+
return null;
|
|
2365
|
+
const out = { ts: o.ts, commit_sha: o.commit_sha, snapshot_sha256: o.snapshot_sha256 };
|
|
2366
|
+
if (typeof o.node_count === "number" && Number.isFinite(o.node_count) && o.node_count >= 0) {
|
|
2367
|
+
out.node_count = o.node_count;
|
|
2368
|
+
}
|
|
2369
|
+
if (typeof o.edge_count === "number" && Number.isFinite(o.edge_count) && o.edge_count >= 0) {
|
|
2370
|
+
out.edge_count = o.edge_count;
|
|
2371
|
+
}
|
|
2372
|
+
return out;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// dist/src/graph/snapshot.js
|
|
2376
|
+
import { createHash } from "node:crypto";
|
|
2377
|
+
import { mkdirSync as mkdirSync7, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2378
|
+
import { homedir as homedir8 } from "node:os";
|
|
2379
|
+
import { dirname as dirname5, join as join12 } from "node:path";
|
|
2380
|
+
|
|
2381
|
+
// dist/src/graph/history.js
|
|
2382
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "node:fs";
|
|
2383
|
+
import { dirname as dirname4, join as join11 } from "node:path";
|
|
2384
|
+
|
|
2385
|
+
// dist/src/graph/resolve/cross-file.js
|
|
2386
|
+
import { posix } from "node:path";
|
|
2387
|
+
|
|
2388
|
+
// dist/src/graph/snapshot.js
|
|
2389
|
+
function graphsRoot() {
|
|
2390
|
+
return process.env.HIVEMIND_GRAPHS_HOME ?? join12(homedir8(), ".hivemind", "graphs");
|
|
2391
|
+
}
|
|
2392
|
+
function repoDir(repoKey) {
|
|
2393
|
+
return join12(graphsRoot(), repoKey);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// dist/src/utils/repo-identity.js
|
|
2397
|
+
import { execSync } from "node:child_process";
|
|
2398
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2399
|
+
import { basename, resolve as resolve2 } from "node:path";
|
|
2400
|
+
var DEFAULT_PORTS = {
|
|
2401
|
+
http: "80",
|
|
2402
|
+
https: "443",
|
|
2403
|
+
ssh: "22",
|
|
2404
|
+
git: "9418"
|
|
2405
|
+
};
|
|
2406
|
+
function normalizeGitRemoteUrl(url) {
|
|
2407
|
+
let s = url.trim();
|
|
2408
|
+
const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i);
|
|
2409
|
+
const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
|
|
2410
|
+
if (schemeMatch)
|
|
2411
|
+
s = s.slice(schemeMatch[0].length);
|
|
2412
|
+
if (!scheme) {
|
|
2413
|
+
const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
|
|
2414
|
+
if (scp)
|
|
2415
|
+
s = `${scp[1]}/${scp[2]}`;
|
|
2416
|
+
}
|
|
2417
|
+
s = s.replace(/^[^@/]+@/, "");
|
|
2418
|
+
if (scheme && DEFAULT_PORTS[scheme]) {
|
|
2419
|
+
s = s.replace(new RegExp(`^([^/]+):${DEFAULT_PORTS[scheme]}(/|$)`), "$1$2");
|
|
2420
|
+
}
|
|
2421
|
+
s = s.replace(/\.git\/?$/i, "");
|
|
2422
|
+
s = s.replace(/\/+$/, "");
|
|
2423
|
+
return s.toLowerCase();
|
|
2424
|
+
}
|
|
2425
|
+
function deriveProjectKey(cwd) {
|
|
2426
|
+
const absCwd = resolve2(cwd);
|
|
2427
|
+
const project = basename(absCwd) || "unknown";
|
|
2428
|
+
let signature = null;
|
|
2429
|
+
try {
|
|
2430
|
+
const raw = execSync("git config --get remote.origin.url", {
|
|
2431
|
+
cwd: absCwd,
|
|
2432
|
+
encoding: "utf-8",
|
|
2433
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2434
|
+
}).trim();
|
|
2435
|
+
signature = raw ? normalizeGitRemoteUrl(raw) : null;
|
|
2436
|
+
} catch {
|
|
2437
|
+
}
|
|
2438
|
+
const input = signature ?? absCwd;
|
|
2439
|
+
const key = createHash2("sha1").update(input).digest("hex").slice(0, 16);
|
|
2440
|
+
return { key, project };
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// dist/src/graph/render/neighborhood.js
|
|
2444
|
+
var CAP = 25;
|
|
2445
|
+
function renderNeighborhood(snap, file) {
|
|
2446
|
+
const allFiles = [...new Set(snap.nodes.map((n) => n.source_file))];
|
|
2447
|
+
let resolved = null;
|
|
2448
|
+
if (allFiles.includes(file)) {
|
|
2449
|
+
resolved = file;
|
|
2450
|
+
} else {
|
|
2451
|
+
const matches = allFiles.filter((f) => f.endsWith(file) || f.includes(file));
|
|
2452
|
+
if (matches.length === 1) {
|
|
2453
|
+
resolved = matches[0];
|
|
2454
|
+
} else if (matches.length > 1) {
|
|
2455
|
+
const lines2 = [];
|
|
2456
|
+
lines2.push(`"${file}" matches multiple files \u2014 which did you mean?`);
|
|
2457
|
+
lines2.push("");
|
|
2458
|
+
for (const m of matches.slice(0, 10))
|
|
2459
|
+
lines2.push(` ${m}`);
|
|
2460
|
+
if (matches.length > 10)
|
|
2461
|
+
lines2.push(` ... and ${matches.length - 10} more`);
|
|
2462
|
+
return lines2.join("\n");
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
if (resolved === null) {
|
|
2466
|
+
const lines2 = [];
|
|
2467
|
+
lines2.push(`No nodes for "${file}".`);
|
|
2468
|
+
const parts = file.split("/").filter((p) => p.length > 2);
|
|
2469
|
+
const close = allFiles.filter((f) => parts.some((p) => f.includes(p))).slice(0, 3);
|
|
2470
|
+
if (close.length > 0) {
|
|
2471
|
+
lines2.push("Did you mean:");
|
|
2472
|
+
for (const c of close)
|
|
2473
|
+
lines2.push(` ${c}`);
|
|
2474
|
+
}
|
|
2475
|
+
return lines2.join("\n");
|
|
2476
|
+
}
|
|
2477
|
+
const fileNodes = snap.nodes.filter((n) => n.source_file === resolved);
|
|
2478
|
+
const fileNodeIds = new Set(fileNodes.map((n) => n.id));
|
|
2479
|
+
const fileOf = /* @__PURE__ */ new Map();
|
|
2480
|
+
for (const n of snap.nodes)
|
|
2481
|
+
fileOf.set(n.id, n.source_file);
|
|
2482
|
+
const sorted = [...fileNodes].sort((a, b) => {
|
|
2483
|
+
const la = parseLocation(a.source_location);
|
|
2484
|
+
const lb = parseLocation(b.source_location);
|
|
2485
|
+
if (la !== lb)
|
|
2486
|
+
return la - lb;
|
|
2487
|
+
return a.label.localeCompare(b.label);
|
|
2488
|
+
});
|
|
2489
|
+
const lines = [];
|
|
2490
|
+
lines.push(`## Symbols in ${resolved}`);
|
|
2491
|
+
lines.push("");
|
|
2492
|
+
if (sorted.length === 0) {
|
|
2493
|
+
lines.push(" (no symbols)");
|
|
2494
|
+
} else {
|
|
2495
|
+
for (const n of sorted) {
|
|
2496
|
+
const exp = n.exported ? "exported" : "internal";
|
|
2497
|
+
lines.push(` ${n.label.padEnd(32)} ${n.kind.padEnd(12)} ${exp.padEnd(10)} ${n.source_location}`);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
lines.push("");
|
|
2501
|
+
lines.push("## Cross-file neighbors");
|
|
2502
|
+
lines.push("");
|
|
2503
|
+
lines.push("Note: 'calls' edges are intra-file only in the current extractor \u2014 cross-file");
|
|
2504
|
+
lines.push("neighbors here are driven mainly by 'imports' edges.");
|
|
2505
|
+
lines.push("");
|
|
2506
|
+
const outgoing = [];
|
|
2507
|
+
const incoming = [];
|
|
2508
|
+
for (const e of snap.links) {
|
|
2509
|
+
const srcIn = fileNodeIds.has(e.source);
|
|
2510
|
+
const tgtIn = fileNodeIds.has(e.target);
|
|
2511
|
+
if (srcIn === tgtIn)
|
|
2512
|
+
continue;
|
|
2513
|
+
if (srcIn) {
|
|
2514
|
+
const tgtFile = fileOf.get(e.target);
|
|
2515
|
+
if (tgtFile !== void 0 && tgtFile !== resolved)
|
|
2516
|
+
outgoing.push(e);
|
|
2517
|
+
} else {
|
|
2518
|
+
const srcFile = fileOf.get(e.source);
|
|
2519
|
+
if (srcFile !== void 0 && srcFile !== resolved)
|
|
2520
|
+
incoming.push(e);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
renderDirectionGroup(lines, outgoing, "Outgoing", "source");
|
|
2524
|
+
renderDirectionGroup(lines, incoming, "Incoming", "target");
|
|
2525
|
+
return lines.join("\n");
|
|
2526
|
+
}
|
|
2527
|
+
function renderDirectionGroup(lines, edges, label, selfField) {
|
|
2528
|
+
const otherField = selfField === "source" ? "target" : "source";
|
|
2529
|
+
const byRelation = /* @__PURE__ */ new Map();
|
|
2530
|
+
for (const e of edges) {
|
|
2531
|
+
const otherId = e[otherField];
|
|
2532
|
+
const rel = e.relation;
|
|
2533
|
+
let nodeMap = byRelation.get(rel);
|
|
2534
|
+
if (!nodeMap) {
|
|
2535
|
+
nodeMap = /* @__PURE__ */ new Map();
|
|
2536
|
+
byRelation.set(rel, nodeMap);
|
|
2537
|
+
}
|
|
2538
|
+
nodeMap.set(otherId, (nodeMap.get(otherId) ?? 0) + 1);
|
|
2539
|
+
}
|
|
2540
|
+
if (byRelation.size === 0) {
|
|
2541
|
+
lines.push(`${label}: (none)`);
|
|
2542
|
+
lines.push("");
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
lines.push(`${label}:`);
|
|
2546
|
+
let totalShown = 0;
|
|
2547
|
+
const sortedRels = [...byRelation.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
2548
|
+
for (const [rel, nodeMap] of sortedRels) {
|
|
2549
|
+
const entries = [...nodeMap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
2550
|
+
lines.push(` ${rel} (${entries.length}):`);
|
|
2551
|
+
let shownInRel = 0;
|
|
2552
|
+
for (const [otherId, cnt] of entries) {
|
|
2553
|
+
if (totalShown >= CAP)
|
|
2554
|
+
break;
|
|
2555
|
+
const suffix = cnt > 1 ? ` \xD7${cnt}` : "";
|
|
2556
|
+
lines.push(` ${otherId}${suffix}`);
|
|
2557
|
+
shownInRel++;
|
|
2558
|
+
totalShown++;
|
|
2559
|
+
}
|
|
2560
|
+
const remaining = entries.length - shownInRel;
|
|
2561
|
+
if (remaining > 0)
|
|
2562
|
+
lines.push(` ... and ${remaining} more`);
|
|
2563
|
+
}
|
|
2564
|
+
if (totalShown >= CAP) {
|
|
2565
|
+
const total = [...byRelation.values()].reduce((s, m) => s + m.size, 0);
|
|
2566
|
+
if (total > CAP)
|
|
2567
|
+
lines.push(` ... and ${total - CAP} more`);
|
|
2568
|
+
}
|
|
2569
|
+
lines.push("");
|
|
2570
|
+
}
|
|
2571
|
+
function parseLocation(loc) {
|
|
2572
|
+
const m = loc.match(/^L(\d+)/);
|
|
2573
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// dist/src/graph/render/layers.js
|
|
2577
|
+
var LAYER_RULES = [
|
|
2578
|
+
{ layer: "Tests", test: (p) => p.includes("/tests/") || p.includes(".test.") || p.includes("/__tests__/") },
|
|
2579
|
+
{ layer: "Hooks", test: (p) => p.includes("/hooks/") },
|
|
2580
|
+
{ layer: "CLI", test: (p) => p.includes("/cli/") || p.includes("/commands/") },
|
|
2581
|
+
{ layer: "Graph", test: (p) => p.includes("/graph/") },
|
|
2582
|
+
{ layer: "Shell/VFS", test: (p) => p.includes("/shell/") },
|
|
2583
|
+
{ layer: "Embeddings", test: (p) => p.includes("/embeddings/") },
|
|
2584
|
+
{ layer: "Skillify", test: (p) => p.includes("/skillify/") },
|
|
2585
|
+
{ layer: "Config", test: (p) => /(?:^|\/)config\.[^/]+$/.test(p) || /\.config\.[^/]+$/.test(p) },
|
|
2586
|
+
{ layer: "Utils", test: (p) => p.includes("/utils/") }
|
|
2587
|
+
];
|
|
2588
|
+
function layerOf(sourceFile) {
|
|
2589
|
+
const p = sourceFile.startsWith("/") ? sourceFile : `/${sourceFile}`;
|
|
2590
|
+
for (const rule of LAYER_RULES) {
|
|
2591
|
+
if (rule.test(p))
|
|
2592
|
+
return rule.layer;
|
|
2593
|
+
}
|
|
2594
|
+
return "Core";
|
|
2595
|
+
}
|
|
2596
|
+
function renderLayers(snap) {
|
|
2597
|
+
try {
|
|
2598
|
+
const layerNodes = /* @__PURE__ */ new Map();
|
|
2599
|
+
const layerFiles = /* @__PURE__ */ new Map();
|
|
2600
|
+
for (const node of snap.nodes) {
|
|
2601
|
+
const layer = layerOf(node.source_file);
|
|
2602
|
+
layerNodes.set(layer, (layerNodes.get(layer) ?? 0) + 1);
|
|
2603
|
+
let fileMap = layerFiles.get(layer);
|
|
2604
|
+
if (!fileMap) {
|
|
2605
|
+
fileMap = /* @__PURE__ */ new Map();
|
|
2606
|
+
layerFiles.set(layer, fileMap);
|
|
2607
|
+
}
|
|
2608
|
+
fileMap.set(node.source_file, (fileMap.get(node.source_file) ?? 0) + 1);
|
|
2609
|
+
}
|
|
2610
|
+
if (layerNodes.size === 0) {
|
|
2611
|
+
return "No nodes in snapshot \u2014 nothing to layer.";
|
|
2612
|
+
}
|
|
2613
|
+
const sorted = [...layerNodes.entries()].sort(([, a], [, b]) => b - a);
|
|
2614
|
+
const lines = [];
|
|
2615
|
+
lines.push("## Architectural Layers");
|
|
2616
|
+
lines.push("");
|
|
2617
|
+
for (const [layer, count] of sorted) {
|
|
2618
|
+
lines.push(`${layer.padEnd(14)} ${String(count).padStart(4)} node${count === 1 ? "" : "s"}`);
|
|
2619
|
+
const fileMap = layerFiles.get(layer);
|
|
2620
|
+
const topFiles = [...fileMap.entries()].sort(([, a], [, b]) => b - a).slice(0, 5);
|
|
2621
|
+
for (const [file, n] of topFiles) {
|
|
2622
|
+
lines.push(` ${String(n).padStart(3)} ${file}`);
|
|
2623
|
+
}
|
|
2624
|
+
if (fileMap.size > 5) {
|
|
2625
|
+
lines.push(` ... and ${fileMap.size - 5} more file${fileMap.size - 5 === 1 ? "" : "s"}`);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
lines.push("");
|
|
2629
|
+
lines.push(`Total: ${snap.nodes.length} node${snap.nodes.length === 1 ? "" : "s"} across ${sorted.length} layer${sorted.length === 1 ? "" : "s"}`);
|
|
2630
|
+
return lines.join("\n");
|
|
2631
|
+
} catch {
|
|
2632
|
+
return "Failed to render layer view.";
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
// dist/src/graph/render/tour.js
|
|
2637
|
+
var LINE_CAP = 60;
|
|
2638
|
+
function renderTour(snap) {
|
|
2639
|
+
if (snap.nodes.length === 0) {
|
|
2640
|
+
return "Graph is empty \u2014 no nodes to tour.";
|
|
2641
|
+
}
|
|
2642
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
2643
|
+
for (const n of snap.nodes)
|
|
2644
|
+
nodeMap.set(n.id, n);
|
|
2645
|
+
const inDegOrig = /* @__PURE__ */ new Map();
|
|
2646
|
+
for (const n of snap.nodes)
|
|
2647
|
+
inDegOrig.set(n.id, 0);
|
|
2648
|
+
for (const e of snap.links) {
|
|
2649
|
+
if (nodeMap.has(e.source) && nodeMap.has(e.target)) {
|
|
2650
|
+
inDegOrig.set(e.target, (inDegOrig.get(e.target) ?? 0) + 1);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
const entryPoints = snap.nodes.filter((n) => n.exported && inDegOrig.get(n.id) === 0).sort((a, b) => a.id.localeCompare(b.id));
|
|
2654
|
+
const entrySet = new Set(entryPoints.map((n) => n.id));
|
|
2655
|
+
const revAdj = /* @__PURE__ */ new Map();
|
|
2656
|
+
const inDegRev = /* @__PURE__ */ new Map();
|
|
2657
|
+
for (const n of snap.nodes) {
|
|
2658
|
+
revAdj.set(n.id, []);
|
|
2659
|
+
inDegRev.set(n.id, 0);
|
|
2660
|
+
}
|
|
2661
|
+
for (const e of snap.links) {
|
|
2662
|
+
if (!nodeMap.has(e.source) || !nodeMap.has(e.target))
|
|
2663
|
+
continue;
|
|
2664
|
+
revAdj.get(e.target).push(e.source);
|
|
2665
|
+
inDegRev.set(e.source, (inDegRev.get(e.source) ?? 0) + 1);
|
|
2666
|
+
}
|
|
2667
|
+
const queue = [];
|
|
2668
|
+
for (const n of snap.nodes) {
|
|
2669
|
+
if (inDegRev.get(n.id) === 0)
|
|
2670
|
+
queue.push(n.id);
|
|
2671
|
+
}
|
|
2672
|
+
queue.sort();
|
|
2673
|
+
const topoOrder = [];
|
|
2674
|
+
while (queue.length > 0) {
|
|
2675
|
+
const id = queue.shift();
|
|
2676
|
+
topoOrder.push(id);
|
|
2677
|
+
const newReady = [];
|
|
2678
|
+
for (const dep of revAdj.get(id) ?? []) {
|
|
2679
|
+
const d = (inDegRev.get(dep) ?? 0) - 1;
|
|
2680
|
+
inDegRev.set(dep, d);
|
|
2681
|
+
if (d === 0)
|
|
2682
|
+
newReady.push(dep);
|
|
2683
|
+
}
|
|
2684
|
+
if (newReady.length > 0) {
|
|
2685
|
+
for (const x of newReady)
|
|
2686
|
+
queue.push(x);
|
|
2687
|
+
queue.sort();
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
const topoSet = new Set(topoOrder);
|
|
2691
|
+
const cyclic = snap.nodes.filter((n) => !topoSet.has(n.id)).sort((a, b) => a.id.localeCompare(b.id));
|
|
2692
|
+
const walkthrough = topoOrder.filter((id) => !entrySet.has(id));
|
|
2693
|
+
const totalNodes = snap.nodes.length;
|
|
2694
|
+
const lines = [];
|
|
2695
|
+
lines.push(`# Code Graph Tour \u2014 ${totalNodes} node${totalNodes !== 1 ? "s" : ""}`);
|
|
2696
|
+
lines.push("");
|
|
2697
|
+
lines.push(`## Entry points (${entryPoints.length})`);
|
|
2698
|
+
if (entryPoints.length === 0) {
|
|
2699
|
+
lines.push(" (none \u2014 all exported nodes have at least one incoming edge)");
|
|
2700
|
+
} else {
|
|
2701
|
+
lines.push(" Exported symbols with no incoming edges \u2014 likely top-level public API.");
|
|
2702
|
+
lines.push("");
|
|
2703
|
+
for (let i = 0; i < entryPoints.length; i++) {
|
|
2704
|
+
if (lines.length >= LINE_CAP) {
|
|
2705
|
+
lines.push(` ... and ${entryPoints.length - i} more`);
|
|
2706
|
+
break;
|
|
2707
|
+
}
|
|
2708
|
+
lines.push(` ${i + 1}. ${entryPoints[i].id} [${entryPoints[i].kind}]`);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
lines.push("");
|
|
2712
|
+
lines.push(`## Walkthrough \u2014 dependency order (${walkthrough.length})`);
|
|
2713
|
+
if (walkthrough.length === 0) {
|
|
2714
|
+
lines.push(" (all non-entry nodes are cyclic)");
|
|
2715
|
+
} else {
|
|
2716
|
+
lines.push(" Dependencies before dependents (bottom-up).");
|
|
2717
|
+
lines.push("");
|
|
2718
|
+
for (let i = 0; i < walkthrough.length; i++) {
|
|
2719
|
+
if (lines.length >= LINE_CAP) {
|
|
2720
|
+
lines.push(` ... and ${walkthrough.length - i} more`);
|
|
2721
|
+
break;
|
|
2722
|
+
}
|
|
2723
|
+
const n = nodeMap.get(walkthrough[i]);
|
|
2724
|
+
lines.push(` ${i + 1}. ${n.id} [${n.kind}]`);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
lines.push("");
|
|
2728
|
+
if (cyclic.length > 0) {
|
|
2729
|
+
lines.push(`## Cyclic / remaining (${cyclic.length})`);
|
|
2730
|
+
lines.push(" These nodes form cycles and were not reached by topological sort.");
|
|
2731
|
+
lines.push("");
|
|
2732
|
+
for (let i = 0; i < cyclic.length; i++) {
|
|
2733
|
+
if (lines.length >= LINE_CAP) {
|
|
2734
|
+
lines.push(` ... and ${cyclic.length - i} more`);
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2737
|
+
lines.push(` ${i + 1}. ${cyclic[i].id} [${cyclic[i].kind}]`);
|
|
2738
|
+
}
|
|
2739
|
+
lines.push("");
|
|
2740
|
+
}
|
|
2741
|
+
lines.push(`Total: ${entryPoints.length} entry + ${walkthrough.length} walkthrough` + (cyclic.length > 0 ? ` + ${cyclic.length} cyclic` : "") + ` = ${totalNodes} nodes`);
|
|
2742
|
+
return lines.join("\n");
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// dist/src/graph/render/path.js
|
|
2746
|
+
function resolvePattern(snap, pattern) {
|
|
2747
|
+
const needle = pattern.toLowerCase();
|
|
2748
|
+
return snap.nodes.filter((n) => n.id.toLowerCase().includes(needle) || n.label.toLowerCase().includes(needle)).map((n) => n.id).sort();
|
|
2749
|
+
}
|
|
2750
|
+
function buildAdjacency(snap, undirected) {
|
|
2751
|
+
const adj = /* @__PURE__ */ new Map();
|
|
2752
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
2753
|
+
for (const n of snap.nodes) {
|
|
2754
|
+
adj.set(n.id, []);
|
|
2755
|
+
nodeIds.add(n.id);
|
|
2756
|
+
}
|
|
2757
|
+
for (const edge of snap.links) {
|
|
2758
|
+
if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target))
|
|
2759
|
+
continue;
|
|
2760
|
+
adj.get(edge.source).push({ neighborId: edge.target, edge, reversed: false });
|
|
2761
|
+
if (undirected) {
|
|
2762
|
+
adj.get(edge.target).push({ neighborId: edge.source, edge, reversed: true });
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
for (const neighbors of adj.values()) {
|
|
2766
|
+
neighbors.sort((a, b) => a.neighborId.localeCompare(b.neighborId) || a.edge.relation.localeCompare(b.edge.relation) || (a.reversed === b.reversed ? 0 : a.reversed ? 1 : -1));
|
|
2767
|
+
}
|
|
2768
|
+
return adj;
|
|
2769
|
+
}
|
|
2770
|
+
function bfs(adj, fromId, toId) {
|
|
2771
|
+
if (fromId === toId)
|
|
2772
|
+
return [];
|
|
2773
|
+
const parent = /* @__PURE__ */ new Map();
|
|
2774
|
+
const visited = /* @__PURE__ */ new Set([fromId]);
|
|
2775
|
+
const queue = [fromId];
|
|
2776
|
+
while (queue.length > 0) {
|
|
2777
|
+
const current = queue.shift();
|
|
2778
|
+
for (const { neighborId, edge, reversed } of adj.get(current) ?? []) {
|
|
2779
|
+
if (visited.has(neighborId))
|
|
2780
|
+
continue;
|
|
2781
|
+
visited.add(neighborId);
|
|
2782
|
+
parent.set(neighborId, { parentId: current, hop: { edge, reversed } });
|
|
2783
|
+
if (neighborId === toId) {
|
|
2784
|
+
const hops = [];
|
|
2785
|
+
let cur = toId;
|
|
2786
|
+
while (cur !== fromId) {
|
|
2787
|
+
const p = parent.get(cur);
|
|
2788
|
+
hops.unshift(p.hop);
|
|
2789
|
+
cur = p.parentId;
|
|
2790
|
+
}
|
|
2791
|
+
return hops;
|
|
2792
|
+
}
|
|
2793
|
+
queue.push(neighborId);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return null;
|
|
2797
|
+
}
|
|
2798
|
+
function renderHops(fromId, hops, undirected) {
|
|
2799
|
+
const lines = [];
|
|
2800
|
+
lines.push(`${undirected ? "Undirected path" : "Directed path"} (${hops.length} hop${hops.length === 1 ? "" : "s"}):`);
|
|
2801
|
+
lines.push("");
|
|
2802
|
+
lines.push(` ${fromId}`);
|
|
2803
|
+
for (const { edge, reversed } of hops) {
|
|
2804
|
+
if (reversed) {
|
|
2805
|
+
lines.push(` <--${edge.relation}-- ${edge.source} [real edge: ${edge.source} \u2192 ${edge.target}]`);
|
|
2806
|
+
} else {
|
|
2807
|
+
lines.push(` --${edge.relation}--> ${edge.target}`);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
if (undirected) {
|
|
2811
|
+
lines.push("");
|
|
2812
|
+
lines.push("Note: no directed path exists. Arrows with <-- are traversed against their declared direction.");
|
|
2813
|
+
}
|
|
2814
|
+
return lines.join("\n");
|
|
2815
|
+
}
|
|
2816
|
+
function candidateList(pattern, ids) {
|
|
2817
|
+
const lines = [`"${pattern}" matches ${ids.length} nodes \u2014 be more specific:`];
|
|
2818
|
+
lines.push("");
|
|
2819
|
+
const shown = ids.slice(0, 20);
|
|
2820
|
+
for (let i = 0; i < shown.length; i++)
|
|
2821
|
+
lines.push(` [${i + 1}] ${shown[i]}`);
|
|
2822
|
+
if (ids.length > 20)
|
|
2823
|
+
lines.push(` ... and ${ids.length - 20} more`);
|
|
2824
|
+
return lines.join("\n");
|
|
2825
|
+
}
|
|
2826
|
+
function renderPath(snap, fromPattern, toPattern) {
|
|
2827
|
+
const fromIds = resolvePattern(snap, fromPattern);
|
|
2828
|
+
const toIds = resolvePattern(snap, toPattern);
|
|
2829
|
+
if (fromIds.length === 0) {
|
|
2830
|
+
return `No node matches "${fromPattern}". Try cat memory/graph/find/<pattern> to explore.`;
|
|
2831
|
+
}
|
|
2832
|
+
if (toIds.length === 0) {
|
|
2833
|
+
return `No node matches "${toPattern}". Try cat memory/graph/find/<pattern> to explore.`;
|
|
2834
|
+
}
|
|
2835
|
+
if (fromIds.length > 1)
|
|
2836
|
+
return candidateList(fromPattern, fromIds);
|
|
2837
|
+
if (toIds.length > 1)
|
|
2838
|
+
return candidateList(toPattern, toIds);
|
|
2839
|
+
const fromId = fromIds[0];
|
|
2840
|
+
const toId = toIds[0];
|
|
2841
|
+
if (fromId === toId) {
|
|
2842
|
+
return `"${fromId}" is the same node on both ends \u2014 path length 0.`;
|
|
2843
|
+
}
|
|
2844
|
+
const dirPath = bfs(buildAdjacency(snap, false), fromId, toId);
|
|
2845
|
+
if (dirPath !== null)
|
|
2846
|
+
return renderHops(fromId, dirPath, false);
|
|
2847
|
+
const undirPath = bfs(buildAdjacency(snap, true), fromId, toId);
|
|
2848
|
+
if (undirPath !== null)
|
|
2849
|
+
return renderHops(fromId, undirPath, true);
|
|
2850
|
+
const fromNode = snap.nodes.find((n) => n.id === fromId);
|
|
2851
|
+
const toNode = snap.nodes.find((n) => n.id === toId);
|
|
2852
|
+
const sameFile = fromNode && toNode && fromNode.source_file === toNode.source_file;
|
|
2853
|
+
const context = sameFile ? `Both are in ${fromNode.source_file} \u2014 same file but no connecting edges.` : `Sources: ${fromNode?.source_file ?? "?"} vs ${toNode?.source_file ?? "?"} \u2014 they appear disconnected.`;
|
|
2854
|
+
return [`No path found between:`, ` from: ${fromId}`, ` to: ${toId}`, ``, context].join("\n");
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
// dist/src/graph/render/impact.js
|
|
2858
|
+
var IMPACT_CAP = 80;
|
|
2859
|
+
var MAX_DEPTH = 25;
|
|
2860
|
+
function renderImpact(snap, pattern) {
|
|
2861
|
+
const needle = pattern.toLowerCase();
|
|
2862
|
+
const matches = snap.nodes.filter((n) => n.id.toLowerCase().includes(needle));
|
|
2863
|
+
if (matches.length === 0) {
|
|
2864
|
+
return `No node matches "${pattern}". Try cat memory/graph/find/${pattern} to explore.`;
|
|
2865
|
+
}
|
|
2866
|
+
if (matches.length > 1) {
|
|
2867
|
+
const lines2 = [`"${pattern}" matches ${matches.length} nodes \u2014 be more specific:`, ""];
|
|
2868
|
+
for (const m of matches.slice(0, 20))
|
|
2869
|
+
lines2.push(` ${m.id}`);
|
|
2870
|
+
if (matches.length > 20)
|
|
2871
|
+
lines2.push(` ... and ${matches.length - 20} more`);
|
|
2872
|
+
return lines2.join("\n");
|
|
2873
|
+
}
|
|
2874
|
+
const target = matches[0];
|
|
2875
|
+
const nodeIds = new Set(snap.nodes.map((n) => n.id));
|
|
2876
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
2877
|
+
for (const e of snap.links) {
|
|
2878
|
+
if (!nodeIds.has(e.source))
|
|
2879
|
+
continue;
|
|
2880
|
+
const list = incoming.get(e.target);
|
|
2881
|
+
if (list)
|
|
2882
|
+
list.push(e);
|
|
2883
|
+
else
|
|
2884
|
+
incoming.set(e.target, [e]);
|
|
2885
|
+
}
|
|
2886
|
+
const depthOf = /* @__PURE__ */ new Map();
|
|
2887
|
+
const viaOf = /* @__PURE__ */ new Map();
|
|
2888
|
+
depthOf.set(target.id, 0);
|
|
2889
|
+
let frontier = [target.id];
|
|
2890
|
+
let depth = 0;
|
|
2891
|
+
while (frontier.length > 0 && depth < MAX_DEPTH) {
|
|
2892
|
+
depth++;
|
|
2893
|
+
const next = [];
|
|
2894
|
+
for (const id of frontier) {
|
|
2895
|
+
const edges = (incoming.get(id) ?? []).slice().sort((a, b) => a.source.localeCompare(b.source) || a.relation.localeCompare(b.relation));
|
|
2896
|
+
for (const e of edges) {
|
|
2897
|
+
if (depthOf.has(e.source))
|
|
2898
|
+
continue;
|
|
2899
|
+
depthOf.set(e.source, depth);
|
|
2900
|
+
viaOf.set(e.source, { rel: e.relation, from: id });
|
|
2901
|
+
next.push(e.source);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
next.sort();
|
|
2905
|
+
frontier = next;
|
|
2906
|
+
}
|
|
2907
|
+
const dependents = [...depthOf.entries()].filter(([id]) => id !== target.id);
|
|
2908
|
+
const total = dependents.length;
|
|
2909
|
+
const lines = [];
|
|
2910
|
+
lines.push(`Impact of ${target.id}`);
|
|
2911
|
+
if (target.signature)
|
|
2912
|
+
lines.push(` ${target.signature}`);
|
|
2913
|
+
lines.push("");
|
|
2914
|
+
if (total === 0) {
|
|
2915
|
+
lines.push("No resolved dependents \u2014 nothing in the graph reaches this symbol.");
|
|
2916
|
+
lines.push("(Cross-file resolution is partial; this is a lower bound, not proof it's unused.)");
|
|
2917
|
+
return lines.join("\n");
|
|
2918
|
+
}
|
|
2919
|
+
lines.push(`${total} dependent${total === 1 ? "" : "s"} (transitive), by depth:`);
|
|
2920
|
+
lines.push("");
|
|
2921
|
+
const byDepth = /* @__PURE__ */ new Map();
|
|
2922
|
+
for (const [id, d] of dependents) {
|
|
2923
|
+
const list = byDepth.get(d) ?? [];
|
|
2924
|
+
list.push(id);
|
|
2925
|
+
byDepth.set(d, list);
|
|
2926
|
+
}
|
|
2927
|
+
let shown = 0;
|
|
2928
|
+
for (const d of [...byDepth.keys()].sort((a, b) => a - b)) {
|
|
2929
|
+
const ids = byDepth.get(d).sort();
|
|
2930
|
+
lines.push(` depth ${d} (${ids.length}):`);
|
|
2931
|
+
for (const id of ids) {
|
|
2932
|
+
if (shown >= IMPACT_CAP)
|
|
2933
|
+
break;
|
|
2934
|
+
const via = viaOf.get(id);
|
|
2935
|
+
const tag = via ? ` [${via.rel} \u2192 ${via.from}]` : "";
|
|
2936
|
+
lines.push(` ${id}${tag}`);
|
|
2937
|
+
shown++;
|
|
2938
|
+
}
|
|
2939
|
+
if (shown >= IMPACT_CAP)
|
|
2940
|
+
break;
|
|
2941
|
+
}
|
|
2942
|
+
if (total > shown)
|
|
2943
|
+
lines.push(` ... and ${total - shown} more`);
|
|
2944
|
+
lines.push("");
|
|
2945
|
+
lines.push("Note: only RESOLVED edges are traversed (cross-file resolution is partial),");
|
|
2946
|
+
lines.push("so this is a lower bound on impact, not a completeness guarantee.");
|
|
2947
|
+
return lines.join("\n");
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
// dist/src/graph/vfs-handler.js
|
|
2951
|
+
function workTreeIdFor(cwd) {
|
|
2952
|
+
return createHash3("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
2953
|
+
}
|
|
2954
|
+
function handleGraphVfs(subpath, cwd) {
|
|
2955
|
+
const path = subpath.replace(/^\/+/, "");
|
|
2956
|
+
if (path === "" || path === "/") {
|
|
2957
|
+
return { kind: "ok", body: dirListing() };
|
|
2958
|
+
}
|
|
2959
|
+
if (path === "index.md" || path === "index") {
|
|
2960
|
+
return loadSnapshotOrError(cwd, (snap, baseDir) => ({
|
|
2961
|
+
kind: "ok",
|
|
2962
|
+
body: renderIndex(snap, baseDir, cwd)
|
|
2963
|
+
}));
|
|
2964
|
+
}
|
|
2965
|
+
if (path.startsWith("find/")) {
|
|
2966
|
+
const pattern = path.slice("find/".length);
|
|
2967
|
+
if (pattern === "") {
|
|
2968
|
+
return { kind: "not-found", message: "find/ requires a pattern: cat memory/graph/find/<keyword>" };
|
|
2969
|
+
}
|
|
2970
|
+
return loadSnapshotOrError(cwd, (snap, baseDir) => ({
|
|
2971
|
+
kind: "ok",
|
|
2972
|
+
body: renderFind(snap, pattern, baseDir, workTreeIdFor(cwd))
|
|
2973
|
+
}));
|
|
2974
|
+
}
|
|
2975
|
+
if (path.startsWith("show/")) {
|
|
2976
|
+
const key = path.slice("show/".length);
|
|
2977
|
+
if (key === "") {
|
|
2978
|
+
return { kind: "not-found", message: "show/ requires a handle or pattern" };
|
|
2979
|
+
}
|
|
2980
|
+
return loadSnapshotOrError(cwd, (snap, baseDir) => ({
|
|
2981
|
+
kind: "ok",
|
|
2982
|
+
body: renderShow(snap, key, baseDir, workTreeIdFor(cwd))
|
|
2983
|
+
}));
|
|
2984
|
+
}
|
|
2985
|
+
if (path.startsWith("query/")) {
|
|
2986
|
+
const pattern = path.slice("query/".length);
|
|
2987
|
+
if (pattern === "") {
|
|
2988
|
+
return { kind: "not-found", message: "query/ requires a pattern: cat memory/graph/query/<keyword>" };
|
|
2989
|
+
}
|
|
2990
|
+
return loadSnapshotOrError(cwd, (snap, baseDir) => ({
|
|
2991
|
+
kind: "ok",
|
|
2992
|
+
body: renderQuery(snap, pattern, baseDir, workTreeIdFor(cwd))
|
|
2993
|
+
}));
|
|
2994
|
+
}
|
|
2995
|
+
if (path.startsWith("impact/")) {
|
|
2996
|
+
const pattern = path.slice("impact/".length);
|
|
2997
|
+
if (pattern === "") {
|
|
2998
|
+
return { kind: "not-found", message: "impact/ requires a pattern: cat memory/graph/impact/<symbol>" };
|
|
2999
|
+
}
|
|
3000
|
+
return loadSnapshotOrError(cwd, (snap) => ({ kind: "ok", body: renderImpact(snap, pattern) }));
|
|
3001
|
+
}
|
|
3002
|
+
if (path.startsWith("neighborhood/")) {
|
|
3003
|
+
const file = path.slice("neighborhood/".length);
|
|
3004
|
+
if (file === "") {
|
|
3005
|
+
return { kind: "not-found", message: "neighborhood/ requires a file path: cat memory/graph/neighborhood/<file>" };
|
|
3006
|
+
}
|
|
3007
|
+
return loadSnapshotOrError(cwd, (snap) => ({ kind: "ok", body: renderNeighborhood(snap, file) }));
|
|
3008
|
+
}
|
|
3009
|
+
if (path === "layers" || path === "layers/" || path === "layers/index.md") {
|
|
3010
|
+
return loadSnapshotOrError(cwd, (snap) => ({ kind: "ok", body: renderLayers(snap) }));
|
|
3011
|
+
}
|
|
3012
|
+
if (path === "tour" || path === "tour/" || path === "tour/index.md") {
|
|
3013
|
+
return loadSnapshotOrError(cwd, (snap) => ({ kind: "ok", body: renderTour(snap) }));
|
|
3014
|
+
}
|
|
3015
|
+
if (path.startsWith("path/")) {
|
|
3016
|
+
const rest = path.slice("path/".length);
|
|
3017
|
+
const slash = rest.indexOf("/");
|
|
3018
|
+
if (slash <= 0 || slash === rest.length - 1) {
|
|
3019
|
+
return { kind: "not-found", message: "path/ needs two patterns: cat memory/graph/path/<from>/<to> (each a symbol-name substring, no slash)" };
|
|
3020
|
+
}
|
|
3021
|
+
const fromPattern = rest.slice(0, slash);
|
|
3022
|
+
const toPattern = rest.slice(slash + 1);
|
|
3023
|
+
return loadSnapshotOrError(cwd, (snap) => ({ kind: "ok", body: renderPath(snap, fromPattern, toPattern) }));
|
|
3024
|
+
}
|
|
3025
|
+
return {
|
|
3026
|
+
kind: "not-found",
|
|
3027
|
+
message: `Unknown endpoint: graph/${path}
|
|
3028
|
+
Available: index.md, find/<pattern>, query/<pattern>, show/<handle-or-pattern>, impact/<pattern>, neighborhood/<file>, layers, tour, path/<from>/<to>`
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
function loadSnapshotOrError(cwd, fn) {
|
|
3032
|
+
let key;
|
|
3033
|
+
let baseDir;
|
|
3034
|
+
try {
|
|
3035
|
+
key = deriveProjectKey(cwd).key;
|
|
3036
|
+
baseDir = repoDir(key);
|
|
3037
|
+
} catch (e) {
|
|
3038
|
+
return { kind: "no-graph", message: `Cannot derive repo identity: ${e instanceof Error ? e.message : String(e)}` };
|
|
3039
|
+
}
|
|
3040
|
+
const wt = workTreeIdFor(cwd);
|
|
3041
|
+
const last = readLastBuild(baseDir, wt);
|
|
3042
|
+
if (last === null) {
|
|
3043
|
+
return {
|
|
3044
|
+
kind: "no-graph",
|
|
3045
|
+
message: "No local graph for this worktree yet. Run `hivemind graph build` (or `hivemind graph pull` if a teammate has built this commit)."
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
const fileBase = last.commit_sha ?? last.snapshot_sha256;
|
|
3049
|
+
const snapPath = join13(baseDir, "snapshots", `${fileBase}.json`);
|
|
3050
|
+
if (!existsSync7(snapPath)) {
|
|
3051
|
+
return { kind: "no-graph", message: `Snapshot file missing on disk: ${snapPath}` };
|
|
3052
|
+
}
|
|
3053
|
+
let snap;
|
|
3054
|
+
try {
|
|
3055
|
+
snap = JSON.parse(readFileSync9(snapPath, "utf8"));
|
|
3056
|
+
} catch (e) {
|
|
3057
|
+
return { kind: "no-graph", message: `Failed to parse snapshot: ${e instanceof Error ? e.message : String(e)}` };
|
|
3058
|
+
}
|
|
3059
|
+
if (!Array.isArray(snap.nodes) || !Array.isArray(snap.links)) {
|
|
3060
|
+
return { kind: "no-graph", message: "Snapshot schema is invalid (missing nodes/links arrays)." };
|
|
3061
|
+
}
|
|
3062
|
+
try {
|
|
3063
|
+
return fn(snap, baseDir);
|
|
3064
|
+
} catch (e) {
|
|
3065
|
+
return { kind: "no-graph", message: `Failed to render graph view: ${e instanceof Error ? e.message : String(e)}` };
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
function dirListing() {
|
|
3069
|
+
return [
|
|
3070
|
+
"index.md",
|
|
3071
|
+
"find/",
|
|
3072
|
+
"query/",
|
|
3073
|
+
"show/",
|
|
3074
|
+
"impact/",
|
|
3075
|
+
"neighborhood/",
|
|
3076
|
+
"layers",
|
|
3077
|
+
"tour",
|
|
3078
|
+
"path/"
|
|
3079
|
+
].join("\n");
|
|
3080
|
+
}
|
|
3081
|
+
function renderIndex(snap, baseDir, cwd) {
|
|
3082
|
+
const commit = snap.graph.commit_sha?.slice(0, 7) ?? "no-commit";
|
|
3083
|
+
const fullCommit = snap.graph.commit_sha ?? "no-commit";
|
|
3084
|
+
const totalNodes = snap.nodes.length;
|
|
3085
|
+
const totalEdges = snap.links.length;
|
|
3086
|
+
const byFile = {};
|
|
3087
|
+
for (const n of snap.nodes)
|
|
3088
|
+
byFile[n.source_file] = (byFile[n.source_file] ?? 0) + 1;
|
|
3089
|
+
const topFiles = Object.entries(byFile).sort(([, a], [, b]) => b - a).slice(0, 8);
|
|
3090
|
+
const byRel = {};
|
|
3091
|
+
for (const e of snap.links)
|
|
3092
|
+
byRel[e.relation] = (byRel[e.relation] ?? 0) + 1;
|
|
3093
|
+
const byKind = {};
|
|
3094
|
+
for (const n of snap.nodes)
|
|
3095
|
+
byKind[n.kind] = (byKind[n.kind] ?? 0) + 1;
|
|
3096
|
+
const lines = [];
|
|
3097
|
+
lines.push(`# Code Graph \u2014 ${snap.observation.repo_project}`);
|
|
3098
|
+
lines.push("");
|
|
3099
|
+
lines.push(`Commit: ${fullCommit} (built ${snap.observation.ts})`);
|
|
3100
|
+
lines.push(`Branch: ${snap.observation.branch ?? "(detached)"}`);
|
|
3101
|
+
lines.push(`Source: ${join13(baseDir, "snapshots", `${commit ? snap.graph.commit_sha : "?"}.json`)}`);
|
|
3102
|
+
lines.push("");
|
|
3103
|
+
lines.push(`Nodes: ${totalNodes} Edges: ${totalEdges}`);
|
|
3104
|
+
lines.push("");
|
|
3105
|
+
lines.push("## How to query");
|
|
3106
|
+
lines.push(" cat ~/.deeplake/memory/graph/query/<pattern>");
|
|
3107
|
+
lines.push(" 2-in-1: search + expand the top matches with their 1-hop");
|
|
3108
|
+
lines.push(" neighbors (callers/callees/imports/heritage). Start here.");
|
|
3109
|
+
lines.push(" Multi-token AND: query/<a>+<b> requires both tokens.");
|
|
3110
|
+
lines.push("");
|
|
3111
|
+
lines.push(" cat ~/.deeplake/memory/graph/find/<pattern>");
|
|
3112
|
+
lines.push(" Case-insensitive substring match on node id + label.");
|
|
3113
|
+
lines.push(" Emits numbered handles [1] [2] ... saved for this worktree.");
|
|
3114
|
+
lines.push("");
|
|
3115
|
+
lines.push(" cat ~/.deeplake/memory/graph/show/<handle-or-pattern>");
|
|
3116
|
+
lines.push(" <handle>: a digit from a prior `find/`/`query/` (e.g. 3).");
|
|
3117
|
+
lines.push(" <pattern>: a substring; resolves to a unique node if possible,");
|
|
3118
|
+
lines.push(" or shows candidates if ambiguous.");
|
|
3119
|
+
lines.push(" Output: node detail + 1-hop neighbors grouped by edge kind.");
|
|
3120
|
+
lines.push("");
|
|
3121
|
+
lines.push(" Also: neighborhood/<file> \xB7 layers \xB7 tour \xB7 path/<from>/<to>");
|
|
3122
|
+
lines.push("");
|
|
3123
|
+
lines.push("## Node kinds");
|
|
3124
|
+
for (const [k, n] of Object.entries(byKind).sort(([, a], [, b]) => b - a)) {
|
|
3125
|
+
lines.push(` ${k.padEnd(12)} ${n}`);
|
|
3126
|
+
}
|
|
3127
|
+
lines.push("");
|
|
3128
|
+
lines.push("## Edge kinds");
|
|
3129
|
+
for (const [k, n] of Object.entries(byRel).sort(([, a], [, b]) => b - a)) {
|
|
3130
|
+
lines.push(` ${k.padEnd(12)} ${n}`);
|
|
3131
|
+
}
|
|
3132
|
+
lines.push("");
|
|
3133
|
+
lines.push("## Top files by node count");
|
|
3134
|
+
for (const [f, n] of topFiles) {
|
|
3135
|
+
lines.push(` ${String(n).padStart(4)} ${f}`);
|
|
3136
|
+
}
|
|
3137
|
+
lines.push("");
|
|
3138
|
+
lines.push(`Limitations:`);
|
|
3139
|
+
lines.push(` - TypeScript / JavaScript / Python. AST-based, no semantic similarity edges yet.`);
|
|
3140
|
+
lines.push(` - Cross-file 'calls'/'imports'/'extends' ARE resolved for relative named/namespace`);
|
|
3141
|
+
lines.push(` imports; bare (npm)/aliased/barrel/dynamic imports stay unresolved. So a node`);
|
|
3142
|
+
lines.push(` with "Incoming (0)" is not proof of dead code \u2014 a caller may reach it via an`);
|
|
3143
|
+
lines.push(` unresolved import path. (Python cross-file resolution is a follow-up; Python is`);
|
|
3144
|
+
lines.push(` intra-file + structure only for now.)`);
|
|
3145
|
+
lines.push(` - Stale after edits \u2014 if a file's mtime is newer than the build, read the live source.`);
|
|
3146
|
+
void cwd;
|
|
3147
|
+
return lines.join("\n");
|
|
3148
|
+
}
|
|
3149
|
+
function findMatches(snap, pattern) {
|
|
3150
|
+
const tokens = pattern.toLowerCase().split(/[\s+]+/).filter((t) => t.length > 0);
|
|
3151
|
+
if (tokens.length === 0)
|
|
3152
|
+
return [];
|
|
3153
|
+
if (tokens.length === 1) {
|
|
3154
|
+
const needle = tokens[0];
|
|
3155
|
+
const matches2 = [];
|
|
3156
|
+
for (const n of snap.nodes) {
|
|
3157
|
+
if (n.id.toLowerCase().includes(needle) || n.label.toLowerCase().includes(needle))
|
|
3158
|
+
matches2.push(n);
|
|
3159
|
+
}
|
|
3160
|
+
matches2.sort((a, b) => {
|
|
3161
|
+
const ra = rank(a, needle);
|
|
3162
|
+
const rb = rank(b, needle);
|
|
3163
|
+
if (ra !== rb)
|
|
3164
|
+
return ra - rb;
|
|
3165
|
+
return a.id.localeCompare(b.id);
|
|
3166
|
+
});
|
|
3167
|
+
if (matches2.length === 0)
|
|
3168
|
+
return fuzzyMatches(snap, needle);
|
|
3169
|
+
return matches2;
|
|
3170
|
+
}
|
|
3171
|
+
const matches = [];
|
|
3172
|
+
for (const n of snap.nodes) {
|
|
3173
|
+
const id = n.id.toLowerCase();
|
|
3174
|
+
const lbl = n.label.toLowerCase();
|
|
3175
|
+
if (tokens.every((t) => id.includes(t) || lbl.includes(t)))
|
|
3176
|
+
matches.push(n);
|
|
3177
|
+
}
|
|
3178
|
+
const score = (n) => tokens.reduce((s, t) => s + rank(n, t), 0);
|
|
3179
|
+
matches.sort((a, b) => {
|
|
3180
|
+
const sa = score(a);
|
|
3181
|
+
const sb = score(b);
|
|
3182
|
+
if (sa !== sb)
|
|
3183
|
+
return sa - sb;
|
|
3184
|
+
return a.id.localeCompare(b.id);
|
|
3185
|
+
});
|
|
3186
|
+
return matches;
|
|
3187
|
+
}
|
|
3188
|
+
function fuzzyMatches(snap, needle) {
|
|
3189
|
+
if (needle.length < 3)
|
|
3190
|
+
return [];
|
|
3191
|
+
const maxDist = Math.max(1, Math.floor(needle.length / 4));
|
|
3192
|
+
const scored = [];
|
|
3193
|
+
for (const n of snap.nodes) {
|
|
3194
|
+
const d = editDistance(needle, n.label.toLowerCase(), maxDist);
|
|
3195
|
+
if (d <= maxDist)
|
|
3196
|
+
scored.push({ n, d });
|
|
3197
|
+
}
|
|
3198
|
+
scored.sort((a, b) => a.d !== b.d ? a.d - b.d : a.n.id.localeCompare(b.n.id));
|
|
3199
|
+
return scored.slice(0, 25).map((s) => s.n);
|
|
3200
|
+
}
|
|
3201
|
+
function editDistance(a, b, cap) {
|
|
3202
|
+
if (Math.abs(a.length - b.length) > cap)
|
|
3203
|
+
return cap + 1;
|
|
3204
|
+
let prev = new Array(b.length + 1);
|
|
3205
|
+
let cur = new Array(b.length + 1);
|
|
3206
|
+
for (let j = 0; j <= b.length; j++)
|
|
3207
|
+
prev[j] = j;
|
|
3208
|
+
for (let i = 1; i <= a.length; i++) {
|
|
3209
|
+
cur[0] = i;
|
|
3210
|
+
let rowMin = cur[0];
|
|
3211
|
+
for (let j = 1; j <= b.length; j++) {
|
|
3212
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
3213
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
|
|
3214
|
+
if (cur[j] < rowMin)
|
|
3215
|
+
rowMin = cur[j];
|
|
3216
|
+
}
|
|
3217
|
+
if (rowMin > cap)
|
|
3218
|
+
return cap + 1;
|
|
3219
|
+
[prev, cur] = [cur, prev];
|
|
3220
|
+
}
|
|
3221
|
+
return prev[b.length];
|
|
3222
|
+
}
|
|
3223
|
+
function renderFind(snap, pattern, baseDir, worktreeId) {
|
|
3224
|
+
const matches = findMatches(snap, pattern);
|
|
3225
|
+
const capped = matches.slice(0, 50);
|
|
3226
|
+
if (capped.length === 0) {
|
|
3227
|
+
return `No matches for "${pattern}" in ${snap.nodes.length} nodes.
|
|
3228
|
+
Try a shorter or different substring.`;
|
|
3229
|
+
}
|
|
3230
|
+
saveHandles(baseDir, worktreeId, capped.map((n) => n.id), pattern);
|
|
3231
|
+
const lines = [];
|
|
3232
|
+
lines.push(`${matches.length} match${matches.length === 1 ? "" : "es"} for "${pattern}"${matches.length > capped.length ? ` (showing first ${capped.length})` : ""}:`);
|
|
3233
|
+
lines.push("");
|
|
3234
|
+
for (let i = 0; i < capped.length; i++) {
|
|
3235
|
+
const n = capped[i];
|
|
3236
|
+
const tag = n.exported ? "exported" : "internal";
|
|
3237
|
+
lines.push(` [${i + 1}] ${n.id} ${n.kind} (${tag})`);
|
|
3238
|
+
}
|
|
3239
|
+
lines.push("");
|
|
3240
|
+
lines.push("Use: cat ~/.deeplake/memory/graph/show/<N> to see node + 1-hop neighbors");
|
|
3241
|
+
return lines.join("\n");
|
|
3242
|
+
}
|
|
3243
|
+
var QUERY_TOP_N = 5;
|
|
3244
|
+
var QUERY_NEIGHBOR_CAP = 8;
|
|
3245
|
+
function renderQuery(snap, pattern, baseDir, worktreeId) {
|
|
3246
|
+
const matches = findMatches(snap, pattern);
|
|
3247
|
+
if (matches.length === 0) {
|
|
3248
|
+
return `No matches for "${pattern}" in ${snap.nodes.length} nodes.
|
|
3249
|
+
Try a shorter or different substring, or cat memory/graph/find/<pattern>.`;
|
|
3250
|
+
}
|
|
3251
|
+
const top = matches.slice(0, QUERY_TOP_N);
|
|
3252
|
+
saveHandles(baseDir, worktreeId, top.map((n) => n.id), pattern);
|
|
3253
|
+
const topIds = new Set(top.map((n) => n.id));
|
|
3254
|
+
const outByNode = /* @__PURE__ */ new Map();
|
|
3255
|
+
const inByNode = /* @__PURE__ */ new Map();
|
|
3256
|
+
for (const e of snap.links) {
|
|
3257
|
+
if (topIds.has(e.source))
|
|
3258
|
+
(outByNode.get(e.source) ?? setGet(outByNode, e.source)).push(e);
|
|
3259
|
+
if (topIds.has(e.target))
|
|
3260
|
+
(inByNode.get(e.target) ?? setGet(inByNode, e.target)).push(e);
|
|
3261
|
+
}
|
|
3262
|
+
const lines = [];
|
|
3263
|
+
lines.push(`Query "${pattern}" \u2014 ${matches.length} match${matches.length === 1 ? "" : "es"}, expanded top ${top.length} (1 hop)`);
|
|
3264
|
+
lines.push("");
|
|
3265
|
+
for (let i = 0; i < top.length; i++) {
|
|
3266
|
+
const n = top[i];
|
|
3267
|
+
const tags = [n.exported ? "exported" : "internal"];
|
|
3268
|
+
if (n.is_entrypoint)
|
|
3269
|
+
tags.push("entrypoint");
|
|
3270
|
+
if (n.fan_in !== void 0)
|
|
3271
|
+
tags.push(`fan_in=${n.fan_in}`);
|
|
3272
|
+
if (n.fan_out !== void 0)
|
|
3273
|
+
tags.push(`fan_out=${n.fan_out}`);
|
|
3274
|
+
lines.push(`[${i + 1}] ${n.id} ${n.kind} (${tags.join(", ")})`);
|
|
3275
|
+
if (n.signature)
|
|
3276
|
+
lines.push(` ${n.signature}`);
|
|
3277
|
+
renderHopGroup(lines, outByNode.get(n.id) ?? [], "OUT", "target");
|
|
3278
|
+
renderHopGroup(lines, inByNode.get(n.id) ?? [], "IN", "source");
|
|
3279
|
+
lines.push("");
|
|
3280
|
+
}
|
|
3281
|
+
lines.push("Use: cat ~/.deeplake/memory/graph/show/<N> for full detail on a match.");
|
|
3282
|
+
return lines.join("\n");
|
|
3283
|
+
}
|
|
3284
|
+
function setGet(m, key) {
|
|
3285
|
+
const list = [];
|
|
3286
|
+
m.set(key, list);
|
|
3287
|
+
return list;
|
|
3288
|
+
}
|
|
3289
|
+
function renderHopGroup(lines, edges, dir, otherField) {
|
|
3290
|
+
if (edges.length === 0)
|
|
3291
|
+
return;
|
|
3292
|
+
const byRel = /* @__PURE__ */ new Map();
|
|
3293
|
+
for (const e of edges) {
|
|
3294
|
+
let counts = byRel.get(e.relation);
|
|
3295
|
+
if (!counts) {
|
|
3296
|
+
counts = /* @__PURE__ */ new Map();
|
|
3297
|
+
byRel.set(e.relation, counts);
|
|
3298
|
+
}
|
|
3299
|
+
const id = e[otherField];
|
|
3300
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
3301
|
+
}
|
|
3302
|
+
for (const [rel, counts] of [...byRel.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
3303
|
+
const arrow = dir === "OUT" ? `--${rel}-->` : `<--${rel}--`;
|
|
3304
|
+
const ids = [...counts.keys()].sort();
|
|
3305
|
+
const shown = ids.slice(0, QUERY_NEIGHBOR_CAP).map((id) => {
|
|
3306
|
+
const c = counts.get(id);
|
|
3307
|
+
return c > 1 ? `${id} \xD7${c}` : id;
|
|
3308
|
+
});
|
|
3309
|
+
const more = ids.length > shown.length ? ` (+${ids.length - shown.length} more)` : "";
|
|
3310
|
+
lines.push(` ${arrow} ${shown.join(", ")}${more}`);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
function renderShow(snap, key, baseDir, worktreeId) {
|
|
3314
|
+
if (/^\d+$/.test(key)) {
|
|
3315
|
+
const idx = parseInt(key, 10);
|
|
3316
|
+
const handles = loadHandles(baseDir, worktreeId);
|
|
3317
|
+
if (handles === null) {
|
|
3318
|
+
return `Handle [${idx}] not resolvable: no recent find/ in this worktree. Run cat memory/graph/find/<pattern> first.`;
|
|
3319
|
+
}
|
|
3320
|
+
if (idx < 1 || idx > handles.ids.length) {
|
|
3321
|
+
return `Handle [${idx}] out of range. Last find/${handles.pattern} produced ${handles.ids.length} matches.`;
|
|
3322
|
+
}
|
|
3323
|
+
const nodeId = handles.ids[idx - 1];
|
|
3324
|
+
const node = snap.nodes.find((n) => n.id === nodeId);
|
|
3325
|
+
if (!node) {
|
|
3326
|
+
return `Handle [${idx}] points at "${nodeId}" but that node is no longer in the snapshot (graph rebuilt since last find?). Re-run find.`;
|
|
3327
|
+
}
|
|
3328
|
+
return renderNodeDetail(snap, node);
|
|
3329
|
+
}
|
|
3330
|
+
const needle = key.toLowerCase();
|
|
3331
|
+
const matches = snap.nodes.filter((n) => n.id.toLowerCase().includes(needle));
|
|
3332
|
+
if (matches.length === 0) {
|
|
3333
|
+
return `No node matches "${key}". Try cat memory/graph/find/${key} for fuzzy search.`;
|
|
3334
|
+
}
|
|
3335
|
+
if (matches.length === 1) {
|
|
3336
|
+
return renderNodeDetail(snap, matches[0]);
|
|
3337
|
+
}
|
|
3338
|
+
saveHandles(baseDir, worktreeId, matches.slice(0, 50).map((n) => n.id), key);
|
|
3339
|
+
const lines = [];
|
|
3340
|
+
lines.push(`"${key}" matches ${matches.length} nodes. Pick one:`);
|
|
3341
|
+
lines.push("");
|
|
3342
|
+
for (let i = 0; i < Math.min(matches.length, 50); i++) {
|
|
3343
|
+
lines.push(` [${i + 1}] ${matches[i].id}`);
|
|
3344
|
+
}
|
|
3345
|
+
lines.push("");
|
|
3346
|
+
lines.push("Use: cat ~/.deeplake/memory/graph/show/<N>");
|
|
3347
|
+
return lines.join("\n");
|
|
3348
|
+
}
|
|
3349
|
+
function renderNodeDetail(snap, node) {
|
|
3350
|
+
const incoming = [];
|
|
3351
|
+
const outgoing = [];
|
|
3352
|
+
for (const e of snap.links) {
|
|
3353
|
+
if (e.target === node.id)
|
|
3354
|
+
incoming.push(e);
|
|
3355
|
+
if (e.source === node.id)
|
|
3356
|
+
outgoing.push(e);
|
|
3357
|
+
}
|
|
3358
|
+
const groupBy = (es) => {
|
|
3359
|
+
const m = /* @__PURE__ */ new Map();
|
|
3360
|
+
for (const e of es) {
|
|
3361
|
+
const list = m.get(e.relation) ?? [];
|
|
3362
|
+
list.push(e);
|
|
3363
|
+
m.set(e.relation, list);
|
|
3364
|
+
}
|
|
3365
|
+
return m;
|
|
3366
|
+
};
|
|
3367
|
+
const inGrp = groupBy(incoming);
|
|
3368
|
+
const outGrp = groupBy(outgoing);
|
|
3369
|
+
const lines = [];
|
|
3370
|
+
lines.push(`Node: ${node.id}`);
|
|
3371
|
+
lines.push(` source: ${node.source_file}:${node.source_location}`);
|
|
3372
|
+
lines.push(` kind: ${node.kind}`);
|
|
3373
|
+
lines.push(` label: ${node.label}`);
|
|
3374
|
+
if (node.signature)
|
|
3375
|
+
lines.push(` sig: ${node.signature}`);
|
|
3376
|
+
if (node.doc)
|
|
3377
|
+
lines.push(` doc: ${node.doc}`);
|
|
3378
|
+
const tags = [node.exported ? "exported" : "internal"];
|
|
3379
|
+
if (node.is_entrypoint)
|
|
3380
|
+
tags.push("entrypoint");
|
|
3381
|
+
if (node.fan_in !== void 0)
|
|
3382
|
+
tags.push(`fan_in=${node.fan_in}`);
|
|
3383
|
+
if (node.fan_out !== void 0)
|
|
3384
|
+
tags.push(`fan_out=${node.fan_out}`);
|
|
3385
|
+
lines.push(` ${tags.join(" ")}`);
|
|
3386
|
+
lines.push("");
|
|
3387
|
+
const inHint = incoming.length === 0 ? " \u2014 no resolved callers (cross-file resolution is partial; not proof of dead code)" : ":";
|
|
3388
|
+
lines.push(`Incoming (${incoming.length})${inHint}`);
|
|
3389
|
+
for (const [rel, es] of [...inGrp.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
3390
|
+
lines.push(` ${rel} (${es.length}):`);
|
|
3391
|
+
for (const e of es.slice(0, 20)) {
|
|
3392
|
+
lines.push(` ${e.source}`);
|
|
3393
|
+
}
|
|
3394
|
+
if (es.length > 20)
|
|
3395
|
+
lines.push(` ... and ${es.length - 20} more`);
|
|
3396
|
+
}
|
|
3397
|
+
lines.push("");
|
|
3398
|
+
lines.push(`Outgoing (${outgoing.length})${outgoing.length === 0 ? " \u2014 this node has no edges out" : ":"}`);
|
|
3399
|
+
for (const [rel, es] of [...outGrp.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
3400
|
+
lines.push(` ${rel} (${es.length}):`);
|
|
3401
|
+
for (const e of es.slice(0, 20)) {
|
|
3402
|
+
lines.push(` ${e.target}`);
|
|
3403
|
+
}
|
|
3404
|
+
if (es.length > 20)
|
|
3405
|
+
lines.push(` ... and ${es.length - 20} more`);
|
|
3406
|
+
}
|
|
3407
|
+
return lines.join("\n");
|
|
3408
|
+
}
|
|
3409
|
+
function rank(n, needle) {
|
|
3410
|
+
const lbl = n.label.toLowerCase();
|
|
3411
|
+
const id = n.id.toLowerCase();
|
|
3412
|
+
if (lbl === needle)
|
|
3413
|
+
return 0;
|
|
3414
|
+
if (lbl.startsWith(needle))
|
|
3415
|
+
return 1;
|
|
3416
|
+
if (lbl.includes(needle))
|
|
3417
|
+
return 2;
|
|
3418
|
+
if (id.includes(needle))
|
|
3419
|
+
return 3;
|
|
3420
|
+
return 4;
|
|
3421
|
+
}
|
|
3422
|
+
function handlesPath(baseDir, worktreeId) {
|
|
3423
|
+
return join13(baseDir, "worktrees", worktreeId, ".find-handles.json");
|
|
3424
|
+
}
|
|
3425
|
+
function saveHandles(baseDir, worktreeId, ids, pattern) {
|
|
3426
|
+
const path = handlesPath(baseDir, worktreeId);
|
|
3427
|
+
const payload = { pattern, ts: Date.now(), ids };
|
|
3428
|
+
try {
|
|
3429
|
+
mkdirSync8(dirname6(path), { recursive: true });
|
|
3430
|
+
const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
3431
|
+
writeFileSync7(tmp, JSON.stringify(payload));
|
|
3432
|
+
renameSync5(tmp, path);
|
|
3433
|
+
} catch {
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
function loadHandles(baseDir, worktreeId) {
|
|
3437
|
+
const path = handlesPath(baseDir, worktreeId);
|
|
3438
|
+
if (!existsSync7(path))
|
|
3439
|
+
return null;
|
|
3440
|
+
try {
|
|
3441
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
3442
|
+
if (parsed === null || typeof parsed !== "object")
|
|
3443
|
+
return null;
|
|
3444
|
+
const o = parsed;
|
|
3445
|
+
if (typeof o.pattern !== "string")
|
|
3446
|
+
return null;
|
|
3447
|
+
if (typeof o.ts !== "number")
|
|
3448
|
+
return null;
|
|
3449
|
+
if (!Array.isArray(o.ids))
|
|
3450
|
+
return null;
|
|
3451
|
+
if (!o.ids.every((s) => typeof s === "string"))
|
|
3452
|
+
return null;
|
|
3453
|
+
return { pattern: o.pattern, ts: o.ts, ids: o.ids };
|
|
3454
|
+
} catch {
|
|
3455
|
+
return null;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
// dist/src/graph/graph-command.js
|
|
3460
|
+
var GRAPH_ROOT = "/graph";
|
|
3461
|
+
var GRAPH_PREFIX = "/graph/";
|
|
3462
|
+
function tokenize(s) {
|
|
3463
|
+
return s.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
3464
|
+
}
|
|
3465
|
+
function stripQuotes(p) {
|
|
3466
|
+
if (p.length >= 2 && (p[0] === '"' && p[p.length - 1] === '"' || p[0] === "'" && p[p.length - 1] === "'")) {
|
|
3467
|
+
return p.slice(1, -1);
|
|
3468
|
+
}
|
|
3469
|
+
return p;
|
|
3470
|
+
}
|
|
3471
|
+
function parseReadTargetPath(rewrittenCommand) {
|
|
3472
|
+
const cmd = rewrittenCommand.replace(/\s+2>\S+/g, "").trim();
|
|
3473
|
+
const pipeIdx = cmd.indexOf("|");
|
|
3474
|
+
let readPart = cmd;
|
|
3475
|
+
if (pipeIdx >= 0) {
|
|
3476
|
+
readPart = cmd.slice(0, pipeIdx).trim();
|
|
3477
|
+
const after = cmd.slice(pipeIdx + 1).trim();
|
|
3478
|
+
if (after.includes("|"))
|
|
3479
|
+
return null;
|
|
3480
|
+
if (!/^(?:head|tail)\b/.test(after))
|
|
3481
|
+
return null;
|
|
3482
|
+
if (!/^cat\b/.test(readPart))
|
|
3483
|
+
return null;
|
|
3484
|
+
}
|
|
3485
|
+
const tokens = tokenize(readPart);
|
|
3486
|
+
if (tokens.length === 0)
|
|
3487
|
+
return null;
|
|
3488
|
+
const verb = tokens[0];
|
|
3489
|
+
if (verb !== "cat" && verb !== "head" && verb !== "tail")
|
|
3490
|
+
return null;
|
|
3491
|
+
const operands = [];
|
|
3492
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
3493
|
+
const tok = tokens[i];
|
|
3494
|
+
if (tok.startsWith("-")) {
|
|
3495
|
+
if ((verb === "head" || verb === "tail") && (tok === "-n" || tok === "--lines"))
|
|
3496
|
+
i++;
|
|
3497
|
+
continue;
|
|
3498
|
+
}
|
|
3499
|
+
operands.push(tok);
|
|
3500
|
+
if (operands.length > 1)
|
|
3501
|
+
return null;
|
|
3502
|
+
}
|
|
3503
|
+
return operands.length === 1 ? stripQuotes(operands[0]) : null;
|
|
3504
|
+
}
|
|
3505
|
+
function hasTraversal(virtualPath) {
|
|
3506
|
+
return virtualPath.split("/").includes("..");
|
|
3507
|
+
}
|
|
3508
|
+
function tryGraphRead(rewrittenCommand, cwd) {
|
|
3509
|
+
const ls = rewrittenCommand.replace(/\s+2>\S+/g, "").trim().match(/^ls\s+(?:-\S+\s+)*(\S+)\s*$/);
|
|
3510
|
+
if (ls) {
|
|
3511
|
+
const dir = stripQuotes(ls[1]).replace(/\/+$/, "") || "/";
|
|
3512
|
+
if (dir === GRAPH_ROOT)
|
|
3513
|
+
return "index.md\nfind/\nquery/\nshow/\nimpact/\nneighborhood/\nlayers\ntour\npath/\n";
|
|
3514
|
+
return null;
|
|
3515
|
+
}
|
|
3516
|
+
const virtualPath = parseReadTargetPath(rewrittenCommand);
|
|
3517
|
+
if (virtualPath === null)
|
|
3518
|
+
return null;
|
|
3519
|
+
if (hasTraversal(virtualPath))
|
|
3520
|
+
return null;
|
|
3521
|
+
const normalized = virtualPath.replace(/\/+$/, "") || "/";
|
|
3522
|
+
if (normalized === GRAPH_ROOT)
|
|
3523
|
+
return "index.md\nfind/\nquery/\nshow/\nimpact/\nneighborhood/\nlayers\ntour\npath/\n";
|
|
3524
|
+
if (!virtualPath.startsWith(GRAPH_PREFIX))
|
|
3525
|
+
return null;
|
|
3526
|
+
const subpath = virtualPath.slice(GRAPH_PREFIX.length);
|
|
3527
|
+
const result = handleGraphVfs(subpath, cwd);
|
|
3528
|
+
return result.kind === "ok" ? result.body : `(${result.kind}) ${result.message}`;
|
|
3529
|
+
}
|
|
3530
|
+
|
|
2320
3531
|
// dist/src/hooks/virtual-table-query.js
|
|
2321
3532
|
function normalizeSessionPart(path, content) {
|
|
2322
3533
|
return normalizeContent(path, content);
|
|
@@ -2968,20 +4179,20 @@ async function executeCompiledBashCommand(api, memoryTable, sessionsTable, cmd,
|
|
|
2968
4179
|
}
|
|
2969
4180
|
|
|
2970
4181
|
// dist/src/hooks/query-cache.js
|
|
2971
|
-
import { mkdirSync as
|
|
2972
|
-
import { join as
|
|
2973
|
-
import { homedir as
|
|
4182
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync10, rmSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
4183
|
+
import { join as join14 } from "node:path";
|
|
4184
|
+
import { homedir as homedir9 } from "node:os";
|
|
2974
4185
|
var log5 = (msg) => log("query-cache", msg);
|
|
2975
|
-
var DEFAULT_CACHE_ROOT =
|
|
4186
|
+
var DEFAULT_CACHE_ROOT = join14(homedir9(), ".deeplake", "query-cache");
|
|
2976
4187
|
var INDEX_CACHE_FILE = "index.md";
|
|
2977
4188
|
function getSessionQueryCacheDir(sessionId, deps = {}) {
|
|
2978
4189
|
const { cacheRoot = DEFAULT_CACHE_ROOT } = deps;
|
|
2979
|
-
return
|
|
4190
|
+
return join14(cacheRoot, sessionId);
|
|
2980
4191
|
}
|
|
2981
4192
|
function readCachedIndexContent(sessionId, deps = {}) {
|
|
2982
4193
|
const { logFn = log5 } = deps;
|
|
2983
4194
|
try {
|
|
2984
|
-
return
|
|
4195
|
+
return readFileSync10(join14(getSessionQueryCacheDir(sessionId, deps), INDEX_CACHE_FILE), "utf-8");
|
|
2985
4196
|
} catch (e) {
|
|
2986
4197
|
if (e?.code === "ENOENT")
|
|
2987
4198
|
return null;
|
|
@@ -2993,31 +4204,31 @@ function writeCachedIndexContent(sessionId, content, deps = {}) {
|
|
|
2993
4204
|
const { logFn = log5 } = deps;
|
|
2994
4205
|
try {
|
|
2995
4206
|
const dir = getSessionQueryCacheDir(sessionId, deps);
|
|
2996
|
-
|
|
2997
|
-
|
|
4207
|
+
mkdirSync9(dir, { recursive: true });
|
|
4208
|
+
writeFileSync8(join14(dir, INDEX_CACHE_FILE), content, "utf-8");
|
|
2998
4209
|
} catch (e) {
|
|
2999
4210
|
logFn(`write failed for session=${sessionId}: ${e.message}`);
|
|
3000
4211
|
}
|
|
3001
4212
|
}
|
|
3002
4213
|
|
|
3003
4214
|
// dist/src/utils/direct-run.js
|
|
3004
|
-
import { resolve as
|
|
4215
|
+
import { resolve as resolve3 } from "node:path";
|
|
3005
4216
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3006
4217
|
function isDirectRun(metaUrl) {
|
|
3007
4218
|
const entry = process.argv[1];
|
|
3008
4219
|
if (!entry)
|
|
3009
4220
|
return false;
|
|
3010
4221
|
try {
|
|
3011
|
-
return
|
|
4222
|
+
return resolve3(fileURLToPath2(metaUrl)) === resolve3(entry);
|
|
3012
4223
|
} catch {
|
|
3013
4224
|
return false;
|
|
3014
4225
|
}
|
|
3015
4226
|
}
|
|
3016
4227
|
|
|
3017
4228
|
// dist/src/hooks/memory-path-utils.js
|
|
3018
|
-
import { homedir as
|
|
3019
|
-
import { join as
|
|
3020
|
-
var MEMORY_PATH =
|
|
4229
|
+
import { homedir as homedir10 } from "node:os";
|
|
4230
|
+
import { join as join15 } from "node:path";
|
|
4231
|
+
var MEMORY_PATH = join15(homedir10(), ".deeplake", "memory");
|
|
3021
4232
|
var TILDE_PATH = "~/.deeplake/memory";
|
|
3022
4233
|
var HOME_VAR_PATH = "$HOME/.deeplake/memory";
|
|
3023
4234
|
var SAFE_BUILTINS = /* @__PURE__ */ new Set([
|
|
@@ -3184,6 +4395,11 @@ async function processCodexPreToolUse(input, deps = {}) {
|
|
|
3184
4395
|
if (!touchesMemory(cmd))
|
|
3185
4396
|
return { action: "pass" };
|
|
3186
4397
|
const rewritten = rewritePaths(cmd);
|
|
4398
|
+
const graphBody = tryGraphRead(rewritten, input.cwd ?? process.cwd());
|
|
4399
|
+
if (graphBody !== null) {
|
|
4400
|
+
logFn(`graph vfs intercept: ${rewritten}`);
|
|
4401
|
+
return { action: "block", output: graphBody, rewrittenCommand: rewritten };
|
|
4402
|
+
}
|
|
3187
4403
|
if (!isSafe(rewritten)) {
|
|
3188
4404
|
logFn(`unsupported command, blocking with guidance: ${rewritten}`);
|
|
3189
4405
|
return {
|