@roarpeng/graphflow 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agents/insight.js +1 -1
- package/dist/agents/insight.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/embedding-factory.d.ts.map +1 -1
- package/dist/config/embedding-factory.js +23 -12
- package/dist/config/embedding-factory.js.map +1 -1
- package/dist/config/loader.d.ts +11 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +103 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/core/agent-assignment.d.ts +39 -0
- package/dist/core/agent-assignment.d.ts.map +1 -0
- package/dist/core/agent-assignment.js +73 -0
- package/dist/core/agent-assignment.js.map +1 -0
- package/dist/core/dag-checkpoint.d.ts +83 -0
- package/dist/core/dag-checkpoint.d.ts.map +1 -0
- package/dist/core/dag-checkpoint.js +164 -0
- package/dist/core/dag-checkpoint.js.map +1 -0
- package/dist/core/dag-engine.d.ts +9 -0
- package/dist/core/dag-engine.d.ts.map +1 -1
- package/dist/core/dag-engine.js +42 -0
- package/dist/core/dag-engine.js.map +1 -1
- package/dist/core/orchestrator-context.d.ts +10 -0
- package/dist/core/orchestrator-context.d.ts.map +1 -0
- package/dist/core/orchestrator-context.js +112 -0
- package/dist/core/orchestrator-context.js.map +1 -0
- package/dist/core/orchestrator-episode.d.ts +13 -0
- package/dist/core/orchestrator-episode.d.ts.map +1 -0
- package/dist/core/orchestrator-episode.js +128 -0
- package/dist/core/orchestrator-episode.js.map +1 -0
- package/dist/core/orchestrator-route.d.ts +20 -0
- package/dist/core/orchestrator-route.d.ts.map +1 -0
- package/dist/core/orchestrator-route.js +51 -0
- package/dist/core/orchestrator-route.js.map +1 -0
- package/dist/core/orchestrator.d.ts +2 -38
- package/dist/core/orchestrator.d.ts.map +1 -1
- package/dist/core/orchestrator.js +97 -286
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/triage.d.ts +21 -0
- package/dist/core/triage.d.ts.map +1 -1
- package/dist/core/triage.js +25 -6
- package/dist/core/triage.js.map +1 -1
- package/dist/core/types.d.ts +53 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/graph/context-slicer-types.d.ts +62 -0
- package/dist/graph/context-slicer-types.d.ts.map +1 -0
- package/dist/graph/context-slicer-types.js +13 -0
- package/dist/graph/context-slicer-types.js.map +1 -0
- package/dist/graph/context-slicer-utils.d.ts +24 -0
- package/dist/graph/context-slicer-utils.d.ts.map +1 -0
- package/dist/graph/context-slicer-utils.js +118 -0
- package/dist/graph/context-slicer-utils.js.map +1 -0
- package/dist/graph/context-slicer.d.ts +6 -61
- package/dist/graph/context-slicer.d.ts.map +1 -1
- package/dist/graph/context-slicer.js +73 -170
- package/dist/graph/context-slicer.js.map +1 -1
- package/dist/graph/file-indexer-cache.d.ts +34 -0
- package/dist/graph/file-indexer-cache.d.ts.map +1 -0
- package/dist/graph/file-indexer-cache.js +105 -0
- package/dist/graph/file-indexer-cache.js.map +1 -0
- package/dist/graph/file-indexer-edges.d.ts +55 -0
- package/dist/graph/file-indexer-edges.d.ts.map +1 -0
- package/dist/graph/file-indexer-edges.js +258 -0
- package/dist/graph/file-indexer-edges.js.map +1 -0
- package/dist/graph/file-indexer-nodes.d.ts +43 -0
- package/dist/graph/file-indexer-nodes.d.ts.map +1 -0
- package/dist/graph/file-indexer-nodes.js +137 -0
- package/dist/graph/file-indexer-nodes.js.map +1 -0
- package/dist/graph/file-indexer-walker.d.ts +35 -0
- package/dist/graph/file-indexer-walker.d.ts.map +1 -0
- package/dist/graph/file-indexer-walker.js +80 -0
- package/dist/graph/file-indexer-walker.js.map +1 -0
- package/dist/graph/file-indexer.d.ts +13 -39
- package/dist/graph/file-indexer.d.ts.map +1 -1
- package/dist/graph/file-indexer.js +167 -600
- package/dist/graph/file-indexer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/agent-mcp-installer.d.ts +25 -1
- package/dist/integrations/agent-mcp-installer.d.ts.map +1 -1
- package/dist/integrations/agent-mcp-installer.js +160 -4
- package/dist/integrations/agent-mcp-installer.js.map +1 -1
- package/dist/integrations/skill-installer.d.ts +50 -0
- package/dist/integrations/skill-installer.d.ts.map +1 -0
- package/dist/integrations/skill-installer.js +373 -0
- package/dist/integrations/skill-installer.js.map +1 -0
- package/dist/learning/embeddings.d.ts +11 -0
- package/dist/learning/embeddings.d.ts.map +1 -1
- package/dist/learning/embeddings.js +24 -0
- package/dist/learning/embeddings.js.map +1 -1
- package/dist/learning/episodic-memory.d.ts +3 -2
- package/dist/learning/episodic-memory.d.ts.map +1 -1
- package/dist/learning/episodic-memory.js +105 -10
- package/dist/learning/episodic-memory.js.map +1 -1
- package/dist/learning/local-embedding.d.ts.map +1 -1
- package/dist/learning/local-embedding.js +16 -0
- package/dist/learning/local-embedding.js.map +1 -1
- package/dist/learning/seed-skills.d.ts +21 -0
- package/dist/learning/seed-skills.d.ts.map +1 -0
- package/dist/learning/seed-skills.js +113 -0
- package/dist/learning/seed-skills.js.map +1 -0
- package/dist/learning/skill-evolution.d.ts +1 -1
- package/dist/learning/skill-evolution.d.ts.map +1 -1
- package/dist/learning/skill-evolution.js +1 -1
- package/dist/learning/skill-evolution.js.map +1 -1
- package/dist/learning/skill-package.d.ts +55 -0
- package/dist/learning/skill-package.d.ts.map +1 -0
- package/dist/learning/skill-package.js +115 -0
- package/dist/learning/skill-package.js.map +1 -0
- package/dist/learning/triage-telemetry.d.ts +43 -0
- package/dist/learning/triage-telemetry.d.ts.map +1 -0
- package/dist/learning/triage-telemetry.js +114 -0
- package/dist/learning/triage-telemetry.js.map +1 -0
- package/dist/surfaces/cli/index.js +65 -0
- package/dist/surfaces/cli/index.js.map +1 -1
- package/dist/surfaces/cli/init.d.ts +12 -2
- package/dist/surfaces/cli/init.d.ts.map +1 -1
- package/dist/surfaces/cli/init.js +109 -30
- package/dist/surfaces/cli/init.js.map +1 -1
- package/dist/surfaces/cli/output.d.ts.map +1 -1
- package/dist/surfaces/cli/output.js +5 -0
- package/dist/surfaces/cli/output.js.map +1 -1
- package/dist/surfaces/cli/runtime/graph.d.ts +23 -0
- package/dist/surfaces/cli/runtime/graph.d.ts.map +1 -1
- package/dist/surfaces/cli/runtime/graph.js +40 -0
- package/dist/surfaces/cli/runtime/graph.js.map +1 -1
- package/dist/surfaces/cli/runtime/routing.d.ts.map +1 -1
- package/dist/surfaces/cli/runtime/routing.js +8 -1
- package/dist/surfaces/cli/runtime/routing.js.map +1 -1
- package/dist/surfaces/cli/runtime.d.ts +1 -1
- package/dist/surfaces/cli/runtime.d.ts.map +1 -1
- package/dist/surfaces/cli/runtime.js +4 -1
- package/dist/surfaces/cli/runtime.js.map +1 -1
- package/dist/surfaces/mcp/tool-definitions.js +20 -20
- package/dist/surfaces/mcp/tool-definitions.js.map +1 -1
- package/package.json +3 -3
- package/scripts/safe-postinstall.cjs +323 -5
|
@@ -1,108 +1,120 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* file-indexer.ts — Main entry point for file indexing
|
|
4
|
+
*
|
|
5
|
+
* Re-exports public API and implements the two main indexing functions:
|
|
6
|
+
* - indexWorkspaceFiles: full workspace batch indexing
|
|
7
|
+
* - indexSingleFile: incremental single-file indexing
|
|
8
|
+
*/
|
|
2
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.clearGraphIndexArtifacts =
|
|
4
|
-
exports.hasPendingGraphIndexWork = hasPendingGraphIndexWork;
|
|
10
|
+
exports.resolveCallerAtLine = exports.hasIndexCache = exports.hasPendingGraphIndexWork = exports.clearGraphIndexArtifacts = exports.DEFAULT_MAX_FILE_SIZE = exports.DEFAULT_EXTENSIONS = void 0;
|
|
5
11
|
exports.indexWorkspaceFiles = indexWorkspaceFiles;
|
|
6
|
-
exports.resolveCallerAtLine = resolveCallerAtLine;
|
|
7
12
|
exports.indexSingleFile = indexSingleFile;
|
|
8
|
-
exports.hasIndexCache = hasIndexCache;
|
|
9
|
-
const logger_1 = require("../utils/logger");
|
|
10
|
-
const hash_1 = require("../utils/hash");
|
|
11
13
|
const node_fs_1 = require("node:fs");
|
|
12
14
|
const node_path_1 = require("node:path");
|
|
13
15
|
const node_crypto_1 = require("node:crypto");
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
function loadCacheState(cachePath, forceReindex) {
|
|
52
|
-
if (forceReindex) {
|
|
53
|
-
return {};
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
const raw = (0, node_fs_1.readFileSync)(cachePath, "utf8");
|
|
57
|
-
const parsedCache = JSON.parse(raw);
|
|
58
|
-
if (parsedCache.version === 2 && parsedCache.state) {
|
|
59
|
-
return parsedCache.state;
|
|
16
|
+
const hash_js_1 = require("../utils/hash.js");
|
|
17
|
+
const logger_js_1 = require("../utils/logger.js");
|
|
18
|
+
const index_js_1 = require("./language-indexers/index.js");
|
|
19
|
+
var file_indexer_walker_js_1 = require("./file-indexer-walker.js");
|
|
20
|
+
Object.defineProperty(exports, "DEFAULT_EXTENSIONS", { enumerable: true, get: function () { return file_indexer_walker_js_1.DEFAULT_EXTENSIONS; } });
|
|
21
|
+
Object.defineProperty(exports, "DEFAULT_MAX_FILE_SIZE", { enumerable: true, get: function () { return file_indexer_walker_js_1.DEFAULT_MAX_FILE_SIZE; } });
|
|
22
|
+
var file_indexer_cache_js_1 = require("./file-indexer-cache.js");
|
|
23
|
+
Object.defineProperty(exports, "clearGraphIndexArtifacts", { enumerable: true, get: function () { return file_indexer_cache_js_1.clearGraphIndexArtifacts; } });
|
|
24
|
+
Object.defineProperty(exports, "hasPendingGraphIndexWork", { enumerable: true, get: function () { return file_indexer_cache_js_1.hasPendingGraphIndexWork; } });
|
|
25
|
+
Object.defineProperty(exports, "hasIndexCache", { enumerable: true, get: function () { return file_indexer_cache_js_1.hasIndexCache; } });
|
|
26
|
+
var file_indexer_nodes_js_1 = require("./file-indexer-nodes.js");
|
|
27
|
+
Object.defineProperty(exports, "resolveCallerAtLine", { enumerable: true, get: function () { return file_indexer_nodes_js_1.resolveCallerAtLine; } });
|
|
28
|
+
// ── Internal imports ─────────────────────────────────────────────────
|
|
29
|
+
const file_indexer_walker_js_2 = require("./file-indexer-walker.js");
|
|
30
|
+
const file_indexer_cache_js_2 = require("./file-indexer-cache.js");
|
|
31
|
+
const file_indexer_nodes_js_2 = require("./file-indexer-nodes.js");
|
|
32
|
+
const file_indexer_edges_js_1 = require("./file-indexer-edges.js");
|
|
33
|
+
/**
|
|
34
|
+
* 处理单个文件:读取内容、检查缓存、调用语言索引器提取符号/边。
|
|
35
|
+
* 返回 null 表示该文件未变更、被跳过。
|
|
36
|
+
*
|
|
37
|
+
* 该函数是并行安全:不修改共享的 nodes/edges/parsed 数组,
|
|
38
|
+
* 只返回本文件的局部结果,由调用方顺序合并。
|
|
39
|
+
*/
|
|
40
|
+
async function processFile(file, cacheState, forceReindex, client) {
|
|
41
|
+
const relPath = file.relPath;
|
|
42
|
+
const mtimeMs = file.mtimeMs;
|
|
43
|
+
const prev = cacheState[relPath];
|
|
44
|
+
let content = "";
|
|
45
|
+
let currentHash = "";
|
|
46
|
+
let isChanged = forceReindex;
|
|
47
|
+
if (!prev || prev.mtimeMs !== mtimeMs) {
|
|
48
|
+
content = (0, node_fs_1.readFileSync)(file.absPath, "utf8");
|
|
49
|
+
currentHash = (0, node_crypto_1.createHash)("md5").update(content).digest("hex");
|
|
50
|
+
if (!prev || prev.hash !== currentHash) {
|
|
51
|
+
isChanged = true;
|
|
60
52
|
}
|
|
61
53
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (err.code !== "ENOENT") {
|
|
65
|
-
logger_1.logger.warn({ error: err.message }, "Failed to read index cache");
|
|
66
|
-
}
|
|
54
|
+
if (!isChanged) {
|
|
55
|
+
return null;
|
|
67
56
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
function hasPendingGraphIndexWork(rootDir, options) {
|
|
72
|
-
const includeExtensions = options?.includeExtensions ?? DEFAULT_EXTENSIONS;
|
|
73
|
-
const maxFileSizeBytes = options?.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE;
|
|
74
|
-
const forceReindex = options?.forceReindex ?? false;
|
|
75
|
-
if (forceReindex) {
|
|
76
|
-
return true;
|
|
57
|
+
// 清理该文件在图存储中的旧节点(不同 relPath 的节点互不重叠,并行安全)
|
|
58
|
+
if (client.deleteNode) {
|
|
59
|
+
await pruneFileFromGraph(client, relPath);
|
|
77
60
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const currentRelPaths = new Set(scanned.map((file) => file.relPath));
|
|
82
|
-
for (const relPath of Object.keys(cacheState)) {
|
|
83
|
-
if (!currentRelPaths.has(relPath)) {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
61
|
+
if (!content) {
|
|
62
|
+
content = (0, node_fs_1.readFileSync)(file.absPath, "utf8");
|
|
63
|
+
currentHash = (0, node_crypto_1.createHash)("md5").update(content).digest("hex");
|
|
86
64
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
65
|
+
const fileNodeId = `file:${relPath}`;
|
|
66
|
+
const moduleNodeId = `module:${(0, file_indexer_nodes_js_2.moduleKey)(relPath)}`;
|
|
67
|
+
const indexer = (0, index_js_1.getIndexerForFile)(relPath);
|
|
68
|
+
const language = indexer?.language ?? ((0, file_indexer_walker_js_2.extOf)(relPath).replace(/^\./, "") || "text");
|
|
69
|
+
let declared = [];
|
|
70
|
+
let imports = [];
|
|
71
|
+
let fileCalls = [];
|
|
72
|
+
let fileInherits = [];
|
|
73
|
+
if (indexer) {
|
|
74
|
+
const extracted = await indexer.extract(relPath, content);
|
|
75
|
+
declared = extracted.symbols.map((sym) => ({
|
|
76
|
+
...sym,
|
|
77
|
+
nodeId: `symbol:${relPath}:${(0, hash_js_1.hashTextHex)(sym.name)}`,
|
|
78
|
+
}));
|
|
79
|
+
imports = extracted.imports.map((imp) => imp.module);
|
|
80
|
+
fileCalls = extracted.calls ?? [];
|
|
81
|
+
fileInherits = extracted.inherits ?? [];
|
|
92
82
|
}
|
|
93
|
-
|
|
83
|
+
const { nodes: fileNodes, edges: fileEdges } = (0, file_indexer_nodes_js_2.buildFileNodesAndEdges)(relPath, file.size, language, declared, imports);
|
|
84
|
+
return {
|
|
85
|
+
relPath,
|
|
86
|
+
fileNodes,
|
|
87
|
+
fileEdges,
|
|
88
|
+
parsedEntry: {
|
|
89
|
+
relPath,
|
|
90
|
+
fileNodeId,
|
|
91
|
+
moduleNodeId,
|
|
92
|
+
declared,
|
|
93
|
+
content,
|
|
94
|
+
scannable: Boolean(indexer),
|
|
95
|
+
calls: fileCalls,
|
|
96
|
+
inherits: fileInherits,
|
|
97
|
+
},
|
|
98
|
+
cacheEntry: {
|
|
99
|
+
mtimeMs,
|
|
100
|
+
hash: currentHash,
|
|
101
|
+
numNodes: fileNodes.length,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
94
104
|
}
|
|
95
105
|
async function indexWorkspaceFiles(client, rootDir, options) {
|
|
96
|
-
const includeExtensions = options?.includeExtensions ?? DEFAULT_EXTENSIONS;
|
|
97
|
-
const maxFileSizeBytes = options?.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE;
|
|
106
|
+
const includeExtensions = options?.includeExtensions ?? file_indexer_walker_js_2.DEFAULT_EXTENSIONS;
|
|
107
|
+
const maxFileSizeBytes = options?.maxFileSizeBytes ?? file_indexer_walker_js_2.DEFAULT_MAX_FILE_SIZE;
|
|
98
108
|
const forceReindex = options?.forceReindex ?? false;
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
// 并行索引并发数,默认每批 10 个文件
|
|
110
|
+
const concurrency = Math.max(1, options?.concurrency ?? 10);
|
|
111
|
+
const cachePath = (0, node_path_1.join)(rootDir, file_indexer_cache_js_2.CACHE_DIR, file_indexer_cache_js_2.CACHE_FILE);
|
|
112
|
+
let cacheState = (0, file_indexer_cache_js_2.loadCacheState)(cachePath, forceReindex);
|
|
101
113
|
const snapshot = client.readSnapshot?.();
|
|
102
114
|
if (snapshot && snapshot.nodes.length === 0 && snapshot.edges.length === 0 && Object.keys(cacheState).length > 0) {
|
|
103
115
|
cacheState = {};
|
|
104
116
|
}
|
|
105
|
-
const scanned = walkScannableFiles(rootDir, includeExtensions, maxFileSizeBytes);
|
|
117
|
+
const scanned = (0, file_indexer_walker_js_2.walkScannableFiles)(rootDir, includeExtensions, maxFileSizeBytes);
|
|
106
118
|
const currentRelPaths = new Set(scanned.map((file) => file.relPath));
|
|
107
119
|
if (client.deleteNode) {
|
|
108
120
|
for (const relPath of Object.keys(cacheState)) {
|
|
@@ -115,119 +127,25 @@ async function indexWorkspaceFiles(client, rootDir, options) {
|
|
|
115
127
|
const nodes = [];
|
|
116
128
|
const edges = [];
|
|
117
129
|
const parsed = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
// ── 并行分批索引:每批 concurrency 个文件并行处理 ──
|
|
131
|
+
let processedCount = 0;
|
|
132
|
+
for (let i = 0; i < scanned.length; i += concurrency) {
|
|
133
|
+
const batch = scanned.slice(i, i + concurrency);
|
|
134
|
+
const batchResults = await Promise.all(batch.map((file) => processFile(file, cacheState, forceReindex, client)));
|
|
135
|
+
// 顺序合并本批结果到共享数组(避免并行写冲突)
|
|
136
|
+
for (const result of batchResults) {
|
|
137
|
+
if (result) {
|
|
138
|
+
nodes.push(...result.fileNodes);
|
|
139
|
+
edges.push(...result.fileEdges);
|
|
140
|
+
parsed.push(result.parsedEntry);
|
|
141
|
+
cacheState[result.relPath] = result.cacheEntry;
|
|
130
142
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (client.deleteNode) {
|
|
136
|
-
await pruneFileFromGraph(client, relPath);
|
|
137
|
-
}
|
|
138
|
-
if (!content) {
|
|
139
|
-
content = (0, node_fs_1.readFileSync)(file.absPath, "utf8");
|
|
140
|
-
currentHash = (0, node_crypto_1.createHash)("md5").update(content).digest("hex");
|
|
141
|
-
}
|
|
142
|
-
const nodesStartLen = nodes.length;
|
|
143
|
-
const fileNodeId = `file:${relPath}`;
|
|
144
|
-
const moduleNodeId = `module:${moduleKey(relPath)}`;
|
|
145
|
-
const indexer = (0, index_1.getIndexerForFile)(relPath);
|
|
146
|
-
const language = indexer?.language ?? (extOf(relPath).replace(/^\./, "") || "text");
|
|
147
|
-
let declared = [];
|
|
148
|
-
let imports = [];
|
|
149
|
-
let fileCalls = [];
|
|
150
|
-
let fileInherits = [];
|
|
151
|
-
if (indexer) {
|
|
152
|
-
const extracted = await indexer.extract(relPath, content);
|
|
153
|
-
declared = extracted.symbols.map((sym) => ({
|
|
154
|
-
...sym,
|
|
155
|
-
nodeId: `symbol:${relPath}:${(0, hash_1.hashTextHex)(sym.name)}`,
|
|
156
|
-
}));
|
|
157
|
-
imports = extracted.imports.map((imp) => imp.module);
|
|
158
|
-
fileCalls = extracted.calls ?? [];
|
|
159
|
-
fileInherits = extracted.inherits ?? [];
|
|
160
|
-
}
|
|
161
|
-
const exportNames = declared
|
|
162
|
-
.filter((s) => s.exported && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s.name))
|
|
163
|
-
.map((s) => s.name);
|
|
164
|
-
const uniqueExports = Array.from(new Set(exportNames));
|
|
165
|
-
const exportsSuffix = uniqueExports.length > 0
|
|
166
|
-
? ` # exports: ${uniqueExports.slice(0, 8).join(", ")}`
|
|
167
|
-
: "";
|
|
168
|
-
nodes.push({
|
|
169
|
-
id: fileNodeId,
|
|
170
|
-
type: "File",
|
|
171
|
-
content: `${relPath}${exportsSuffix}`,
|
|
172
|
-
metadata: {
|
|
173
|
-
path: relPath,
|
|
174
|
-
language,
|
|
175
|
-
exports: uniqueExports,
|
|
176
|
-
symbolCount: declared.length,
|
|
177
|
-
sizeBytes: file.size,
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
nodes.push({ id: moduleNodeId, type: "Module", content: moduleKey(relPath) });
|
|
181
|
-
edges.push({ from: fileNodeId, to: moduleNodeId, relation: "depends_on" });
|
|
182
|
-
for (const symbol of declared) {
|
|
183
|
-
const compactSig = `${symbol.kind} ${symbol.name}${symbol.exported ? " (exported)" : ""} @${relPath}:${symbol.line}`;
|
|
184
|
-
const signature = symbol.signature ?? compactSig;
|
|
185
|
-
const signatureHash = (0, hash_1.hashTextHex)(`${signature}\n${symbol.jsdoc ?? ""}\n${symbol.returnType ?? ""}`);
|
|
186
|
-
nodes.push({
|
|
187
|
-
id: symbol.nodeId,
|
|
188
|
-
type: "Symbol",
|
|
189
|
-
content: compactSig,
|
|
190
|
-
metadata: {
|
|
191
|
-
name: symbol.name,
|
|
192
|
-
kind: symbol.kind,
|
|
193
|
-
exported: symbol.exported,
|
|
194
|
-
line: symbol.line,
|
|
195
|
-
file: relPath,
|
|
196
|
-
signature,
|
|
197
|
-
signatureHash,
|
|
198
|
-
...(symbol.jsdoc ? { jsdoc: symbol.jsdoc } : {}),
|
|
199
|
-
...(symbol.visibility ? { visibility: symbol.visibility } : {}),
|
|
200
|
-
...(symbol.paramsCount !== undefined ? { paramsCount: symbol.paramsCount } : {}),
|
|
201
|
-
...(symbol.returnType ? { returnType: symbol.returnType } : {}),
|
|
202
|
-
...(symbol.complexity !== undefined ? { complexity: symbol.complexity } : {}),
|
|
203
|
-
},
|
|
204
|
-
});
|
|
205
|
-
edges.push({ from: fileNodeId, to: symbol.nodeId, relation: "defines" });
|
|
206
|
-
}
|
|
207
|
-
for (const target of imports) {
|
|
208
|
-
const targetModule = normalizeImportTarget(target, relPath);
|
|
209
|
-
if (!targetModule) {
|
|
210
|
-
continue;
|
|
143
|
+
processedCount += 1;
|
|
144
|
+
// 每 100 个文件输出一次进度日志
|
|
145
|
+
if (processedCount > 0 && processedCount % 100 === 0) {
|
|
146
|
+
logger_js_1.logger.info({ processed: processedCount, total: scanned.length, percent: `${((processedCount / scanned.length) * 100).toFixed(1)}%` }, "工作区索引进度");
|
|
211
147
|
}
|
|
212
|
-
const importNodeId = `module:${targetModule}`;
|
|
213
|
-
nodes.push({ id: importNodeId, type: "Module", content: targetModule });
|
|
214
|
-
edges.push({ from: moduleNodeId, to: importNodeId, relation: "imports" });
|
|
215
148
|
}
|
|
216
|
-
parsed.push({
|
|
217
|
-
relPath,
|
|
218
|
-
fileNodeId,
|
|
219
|
-
moduleNodeId,
|
|
220
|
-
declared,
|
|
221
|
-
content,
|
|
222
|
-
scannable: Boolean(indexer),
|
|
223
|
-
calls: fileCalls,
|
|
224
|
-
inherits: fileInherits,
|
|
225
|
-
});
|
|
226
|
-
cacheState[relPath] = {
|
|
227
|
-
mtimeMs,
|
|
228
|
-
hash: currentHash,
|
|
229
|
-
numNodes: nodes.length - nodesStartLen,
|
|
230
|
-
};
|
|
231
149
|
}
|
|
232
150
|
const symbolIndex = new Map();
|
|
233
151
|
for (const file of parsed) {
|
|
@@ -237,242 +155,25 @@ async function indexWorkspaceFiles(client, rootDir, options) {
|
|
|
237
155
|
symbolIndex.set(symbol.name, list);
|
|
238
156
|
}
|
|
239
157
|
}
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const ownNames = new Set(file.declared.map((s) => s.name));
|
|
247
|
-
const seenThisFile = new Set();
|
|
248
|
-
const matches = file.content.match(identifierRe);
|
|
249
|
-
if (!matches)
|
|
250
|
-
continue;
|
|
251
|
-
const seenIdent = new Set();
|
|
252
|
-
for (const name of matches) {
|
|
253
|
-
if (seenIdent.has(name))
|
|
254
|
-
continue;
|
|
255
|
-
seenIdent.add(name);
|
|
256
|
-
if (name.length < 3 || REFERENCE_SKIPLIST.has(name)) {
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
const defs = symbolIndex.get(name);
|
|
260
|
-
if (!defs || defs.length === 0) {
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
for (const def of defs) {
|
|
264
|
-
if (ownNames.has(name) && def.nodeId.startsWith(`symbol:${file.relPath}:`)) {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
const key = `${file.fileNodeId}|${def.nodeId}`;
|
|
268
|
-
if (seenThisFile.has(key)) {
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
seenThisFile.add(key);
|
|
272
|
-
edges.push({ from: file.fileNodeId, to: def.nodeId, relation: "references" });
|
|
273
|
-
referenceCount += 1;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
// ── Build call graph edges (caller → callee) ──────────────────────
|
|
278
|
-
let callEdgeCount = 0;
|
|
279
|
-
for (const file of parsed) {
|
|
280
|
-
if (file.calls.length === 0)
|
|
281
|
-
continue;
|
|
282
|
-
// Map symbol names to node ids within this file for caller resolution
|
|
283
|
-
const localByName = new Map();
|
|
284
|
-
for (const sym of file.declared) {
|
|
285
|
-
localByName.set(sym.name, sym.nodeId);
|
|
286
|
-
}
|
|
287
|
-
const seenCalls = new Set();
|
|
288
|
-
for (const call of file.calls) {
|
|
289
|
-
// Resolve callee via global symbol index (may be in another file)
|
|
290
|
-
const calleeDefs = symbolIndex.get(call.callee);
|
|
291
|
-
if (!calleeDefs || calleeDefs.length === 0)
|
|
292
|
-
continue;
|
|
293
|
-
// Prefer same-file callee, then first match
|
|
294
|
-
const calleeDef = calleeDefs.find((d) => d.nodeId.startsWith(`symbol:${file.relPath}:`)) ?? calleeDefs[0];
|
|
295
|
-
if (!calleeDef)
|
|
296
|
-
continue;
|
|
297
|
-
// Resolve caller: prefer the caller name from extraction, then fall back
|
|
298
|
-
// to the innermost local symbol at or before the call line.
|
|
299
|
-
let callerNodeId;
|
|
300
|
-
if (call.caller) {
|
|
301
|
-
callerNodeId = localByName.get(call.caller);
|
|
302
|
-
}
|
|
303
|
-
if (!callerNodeId) {
|
|
304
|
-
callerNodeId = resolveCallerAtLine(file.declared, call.line);
|
|
305
|
-
}
|
|
306
|
-
if (!callerNodeId) {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
const edgeKey = `${callerNodeId}|${calleeDef.nodeId}|calls`;
|
|
310
|
-
if (seenCalls.has(edgeKey))
|
|
311
|
-
continue;
|
|
312
|
-
seenCalls.add(edgeKey);
|
|
313
|
-
edges.push({ from: callerNodeId, to: calleeDef.nodeId, relation: "calls" });
|
|
314
|
-
callEdgeCount += 1;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
// ── Build inheritance edges (child → parent) ──────────────────────
|
|
318
|
-
let inheritEdgeCount = 0;
|
|
319
|
-
for (const file of parsed) {
|
|
320
|
-
if (file.inherits.length === 0)
|
|
321
|
-
continue;
|
|
322
|
-
const localByName = new Map();
|
|
323
|
-
for (const sym of file.declared) {
|
|
324
|
-
localByName.set(sym.name, sym.nodeId);
|
|
325
|
-
}
|
|
326
|
-
const seenInherits = new Set();
|
|
327
|
-
for (const rel of file.inherits) {
|
|
328
|
-
const childNodeId = localByName.get(rel.child);
|
|
329
|
-
if (!childNodeId)
|
|
330
|
-
continue;
|
|
331
|
-
// Resolve parent via global symbol index
|
|
332
|
-
const parentDefs = symbolIndex.get(rel.parent);
|
|
333
|
-
if (!parentDefs || parentDefs.length === 0)
|
|
334
|
-
continue;
|
|
335
|
-
const parentDef = parentDefs.find((d) => d.nodeId.startsWith(`symbol:${file.relPath}:`)) ?? parentDefs[0];
|
|
336
|
-
if (!parentDef)
|
|
337
|
-
continue;
|
|
338
|
-
const edgeKey = `${childNodeId}|${parentDef.nodeId}|inherits`;
|
|
339
|
-
if (seenInherits.has(edgeKey))
|
|
340
|
-
continue;
|
|
341
|
-
seenInherits.add(edgeKey);
|
|
342
|
-
edges.push({ from: childNodeId, to: parentDef.nodeId, relation: "inherits" });
|
|
343
|
-
inheritEdgeCount += 1;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
158
|
+
const { edges: refEdges, referenceCount } = (0, file_indexer_edges_js_1.buildBatchReferenceEdges)(parsed, symbolIndex);
|
|
159
|
+
edges.push(...refEdges);
|
|
160
|
+
const { edges: callEdges, callEdgeCount } = (0, file_indexer_edges_js_1.buildBatchCallEdges)(parsed, symbolIndex);
|
|
161
|
+
edges.push(...callEdges);
|
|
162
|
+
const { edges: inheritEdges, inheritEdgeCount } = (0, file_indexer_edges_js_1.buildBatchInheritEdges)(parsed, symbolIndex);
|
|
163
|
+
edges.push(...inheritEdges);
|
|
346
164
|
await client.upsertNodes(nodes);
|
|
347
|
-
await client.upsertEdges(dedupEdges(edges));
|
|
348
|
-
|
|
349
|
-
const dir = (0, node_path_1.dirname)(cachePath);
|
|
350
|
-
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
351
|
-
(0, node_fs_1.writeFileSync)(cachePath, JSON.stringify({ version: 2, state: cacheState }, null, 2), "utf8");
|
|
352
|
-
}
|
|
353
|
-
catch (error) {
|
|
354
|
-
logger_1.logger.warn({ error }, "Failed to write index cache");
|
|
355
|
-
}
|
|
165
|
+
await client.upsertEdges((0, file_indexer_edges_js_1.dedupEdges)(edges));
|
|
166
|
+
(0, file_indexer_cache_js_2.saveCacheState)(cachePath, cacheState);
|
|
356
167
|
return {
|
|
357
168
|
indexedFiles: nodes.filter((node) => node.type === "File").length,
|
|
358
169
|
indexedSymbols: nodes.filter((node) => node.type === "Symbol").length,
|
|
359
170
|
indexedReferences: referenceCount + callEdgeCount + inheritEdgeCount,
|
|
360
171
|
};
|
|
361
172
|
}
|
|
362
|
-
|
|
363
|
-
if (!client.readSnapshot || !client.deleteNode) {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
const snapshot = client.readSnapshot();
|
|
367
|
-
const fileNodeId = `file:${relPath}`;
|
|
368
|
-
const moduleNodeId = `module:${moduleKey(relPath)}`;
|
|
369
|
-
const symbolPrefix = `symbol:${relPath}:`;
|
|
370
|
-
const toDelete = snapshot.nodes.filter((node) => node.id === fileNodeId || node.id === moduleNodeId || node.id.startsWith(symbolPrefix));
|
|
371
|
-
for (const node of toDelete) {
|
|
372
|
-
await client.deleteNode(node.id);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
function walkScannableFiles(rootDir, includeExtensions, maxFileSizeBytes) {
|
|
376
|
-
const files = walkFiles(rootDir, includeExtensions);
|
|
377
|
-
const scanned = [];
|
|
378
|
-
for (const absPath of files) {
|
|
379
|
-
const stat = (0, node_fs_1.statSync)(absPath);
|
|
380
|
-
if (stat.size > maxFileSizeBytes) {
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
scanned.push({
|
|
384
|
-
absPath,
|
|
385
|
-
relPath: normalizePath((0, node_path_1.relative)(rootDir, absPath)),
|
|
386
|
-
size: stat.size,
|
|
387
|
-
mtimeMs: stat.mtimeMs,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
return scanned;
|
|
391
|
-
}
|
|
392
|
-
function walkFiles(rootDir, includeExtensions) {
|
|
393
|
-
const entries = (0, node_fs_1.readdirSync)(rootDir, { withFileTypes: true });
|
|
394
|
-
const files = [];
|
|
395
|
-
for (const entry of entries) {
|
|
396
|
-
const full = (0, node_path_1.join)(rootDir, entry.name);
|
|
397
|
-
if (entry.isSymbolicLink()) {
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
if (entry.isDirectory()) {
|
|
401
|
-
if (IGNORED_DIRS.has(entry.name)) {
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
files.push(...walkFiles(full, includeExtensions));
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
if (includeExtensions.some((ext) => entry.name.endsWith(ext))) {
|
|
408
|
-
files.push(full);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
return files;
|
|
412
|
-
}
|
|
413
|
-
function normalizeImportTarget(target, importerRelPath) {
|
|
414
|
-
let cleaned = target
|
|
415
|
-
.replace(/\\/g, "/")
|
|
416
|
-
.replace(/\.(ts|tsx|js|jsx|py|rs|go|hpp|hxx|cpp|cxx|cc|h|c|java|rb|rake|gemspec)$/i, "");
|
|
417
|
-
if (!cleaned) {
|
|
418
|
-
return undefined;
|
|
419
|
-
}
|
|
420
|
-
if (cleaned.startsWith(".")) {
|
|
421
|
-
const dir = node_path_1.posix.dirname(importerRelPath.replace(/\\/g, "/"));
|
|
422
|
-
cleaned = node_path_1.posix.join(dir, cleaned);
|
|
423
|
-
if (cleaned.startsWith("./")) {
|
|
424
|
-
cleaned = cleaned.slice(2);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
return cleaned;
|
|
428
|
-
}
|
|
429
|
-
function moduleKey(relPath) {
|
|
430
|
-
return relPath.replace(/\.(ts|tsx|js|jsx|md|json|py|rs|go|hpp|hxx|cpp|cxx|cc|h|c|java|rb|rake|gemspec)$/i, "");
|
|
431
|
-
}
|
|
432
|
-
/** Innermost declared symbol at or before `line` (for call attribution when caller name is missing). */
|
|
433
|
-
function resolveCallerAtLine(declared, line) {
|
|
434
|
-
const sorted = [...declared].sort((a, b) => b.line - a.line);
|
|
435
|
-
for (const sym of sorted) {
|
|
436
|
-
if (sym.line <= line) {
|
|
437
|
-
return sym.nodeId;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return undefined;
|
|
441
|
-
}
|
|
442
|
-
function dedupEdges(edges) {
|
|
443
|
-
const seen = new Set();
|
|
444
|
-
const result = [];
|
|
445
|
-
for (const edge of edges) {
|
|
446
|
-
const key = `${edge.from}|${edge.relation}|${edge.to}`;
|
|
447
|
-
if (!seen.has(key)) {
|
|
448
|
-
seen.add(key);
|
|
449
|
-
result.push(edge);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return result;
|
|
453
|
-
}
|
|
454
|
-
function normalizePath(pathText) {
|
|
455
|
-
return pathText.replace(/\\/g, "/");
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Incremental single-file indexing — borrowed from codebase-memory-mcp's
|
|
459
|
-
* file-watcher pattern. Allows VS Code onSave hooks (or any file watcher)
|
|
460
|
-
* to update the graph for one changed file without walking the entire workspace.
|
|
461
|
-
*
|
|
462
|
-
* Steps:
|
|
463
|
-
* 1. Prune existing nodes/edges for the file (if any)
|
|
464
|
-
* 2. Extract symbols + imports via the language indexer
|
|
465
|
-
* 3. Upsert new nodes/edges
|
|
466
|
-
* 4. Update the index cache entry for this file
|
|
467
|
-
*
|
|
468
|
-
* @param client Graph client
|
|
469
|
-
* @param rootDir Workspace root (for relative path + cache location)
|
|
470
|
-
* @param absPath Absolute path to the file to index
|
|
471
|
-
* @param options Optional config
|
|
472
|
-
*/
|
|
173
|
+
// ── Single-file incremental indexing ─────────────────────────────────
|
|
473
174
|
async function indexSingleFile(client, rootDir, absPath, options) {
|
|
474
|
-
const includeExtensions = options?.includeExtensions ?? DEFAULT_EXTENSIONS;
|
|
475
|
-
const maxFileSizeBytes = options?.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE;
|
|
175
|
+
const includeExtensions = options?.includeExtensions ?? file_indexer_walker_js_2.DEFAULT_EXTENSIONS;
|
|
176
|
+
const maxFileSizeBytes = options?.maxFileSizeBytes ?? file_indexer_walker_js_2.DEFAULT_MAX_FILE_SIZE;
|
|
476
177
|
// Validate extension
|
|
477
178
|
if (!includeExtensions.some((ext) => absPath.toLowerCase().endsWith(ext))) {
|
|
478
179
|
return { indexedFiles: 0, indexedSymbols: 0, indexedReferences: 0, skipped: true, reason: "extension not in includeExtensions" };
|
|
@@ -488,12 +189,12 @@ async function indexSingleFile(client, rootDir, absPath, options) {
|
|
|
488
189
|
if (stat.size > maxFileSizeBytes) {
|
|
489
190
|
return { indexedFiles: 0, indexedSymbols: 0, indexedReferences: 0, skipped: true, reason: "file exceeds maxFileSizeBytes" };
|
|
490
191
|
}
|
|
491
|
-
const relPath = normalizePath((0, node_path_1.relative)(rootDir, absPath));
|
|
192
|
+
const relPath = (0, file_indexer_walker_js_2.normalizePath)((0, node_path_1.relative)(rootDir, absPath));
|
|
492
193
|
const mtimeMs = stat.mtimeMs;
|
|
493
194
|
const content = (0, node_fs_1.readFileSync)(absPath, "utf8");
|
|
494
195
|
const currentHash = (0, node_crypto_1.createHash)("md5").update(content).digest("hex");
|
|
495
|
-
const cachePath = (0, node_path_1.join)(rootDir, CACHE_DIR, CACHE_FILE);
|
|
496
|
-
const cacheState = loadCacheState(cachePath, false);
|
|
196
|
+
const cachePath = (0, node_path_1.join)(rootDir, file_indexer_cache_js_2.CACHE_DIR, file_indexer_cache_js_2.CACHE_FILE);
|
|
197
|
+
const cacheState = (0, file_indexer_cache_js_2.loadCacheState)(cachePath, false);
|
|
497
198
|
const prev = cacheState[relPath];
|
|
498
199
|
// Skip if unchanged
|
|
499
200
|
if (prev && prev.mtimeMs === mtimeMs && prev.hash === currentHash) {
|
|
@@ -505,9 +206,7 @@ async function indexSingleFile(client, rootDir, absPath, options) {
|
|
|
505
206
|
}
|
|
506
207
|
const nodes = [];
|
|
507
208
|
const edges = [];
|
|
508
|
-
const
|
|
509
|
-
const moduleNodeId = `module:${moduleKey(relPath)}`;
|
|
510
|
-
const indexer = (0, index_1.getIndexerForFile)(relPath);
|
|
209
|
+
const indexer = (0, index_js_1.getIndexerForFile)(relPath);
|
|
511
210
|
const language = indexer?.language ?? (relPath.split(".").pop() ?? "text");
|
|
512
211
|
let declared = [];
|
|
513
212
|
let imports = [];
|
|
@@ -517,177 +216,40 @@ async function indexSingleFile(client, rootDir, absPath, options) {
|
|
|
517
216
|
const extracted = await indexer.extract(relPath, content);
|
|
518
217
|
declared = extracted.symbols.map((sym) => ({
|
|
519
218
|
...sym,
|
|
520
|
-
nodeId: `symbol:${relPath}:${(0,
|
|
219
|
+
nodeId: `symbol:${relPath}:${(0, hash_js_1.hashTextHex)(sym.name)}`,
|
|
521
220
|
}));
|
|
522
221
|
imports = extracted.imports.map((imp) => imp.module);
|
|
523
222
|
fileCalls = extracted.calls ?? [];
|
|
524
223
|
fileInherits = extracted.inherits ?? [];
|
|
525
224
|
}
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
const exportsSuffix = uniqueExports.length > 0
|
|
531
|
-
? ` # exports: ${uniqueExports.slice(0, 8).join(", ")}`
|
|
532
|
-
: "";
|
|
533
|
-
nodes.push({
|
|
534
|
-
id: fileNodeId,
|
|
535
|
-
type: "File",
|
|
536
|
-
content: `${relPath}${exportsSuffix}`,
|
|
537
|
-
metadata: {
|
|
538
|
-
path: relPath,
|
|
539
|
-
language,
|
|
540
|
-
exports: uniqueExports,
|
|
541
|
-
symbolCount: declared.length,
|
|
542
|
-
sizeBytes: stat.size,
|
|
543
|
-
},
|
|
544
|
-
});
|
|
545
|
-
nodes.push({ id: moduleNodeId, type: "Module", content: moduleKey(relPath) });
|
|
546
|
-
edges.push({ from: fileNodeId, to: moduleNodeId, relation: "depends_on" });
|
|
547
|
-
for (const symbol of declared) {
|
|
548
|
-
const compactSig = `${symbol.kind} ${symbol.name}${symbol.exported ? " (exported)" : ""} @${relPath}:${symbol.line}`;
|
|
549
|
-
const signature = symbol.signature ?? compactSig;
|
|
550
|
-
const signatureHash = (0, hash_1.hashTextHex)(`${signature}\n${symbol.jsdoc ?? ""}\n${symbol.returnType ?? ""}`);
|
|
551
|
-
nodes.push({
|
|
552
|
-
id: symbol.nodeId,
|
|
553
|
-
type: "Symbol",
|
|
554
|
-
content: compactSig,
|
|
555
|
-
metadata: {
|
|
556
|
-
name: symbol.name,
|
|
557
|
-
kind: symbol.kind,
|
|
558
|
-
exported: symbol.exported,
|
|
559
|
-
line: symbol.line,
|
|
560
|
-
file: relPath,
|
|
561
|
-
signature,
|
|
562
|
-
signatureHash,
|
|
563
|
-
...(symbol.jsdoc ? { jsdoc: symbol.jsdoc } : {}),
|
|
564
|
-
...(symbol.visibility ? { visibility: symbol.visibility } : {}),
|
|
565
|
-
...(symbol.paramsCount !== undefined ? { paramsCount: symbol.paramsCount } : {}),
|
|
566
|
-
...(symbol.returnType ? { returnType: symbol.returnType } : {}),
|
|
567
|
-
...(symbol.complexity !== undefined ? { complexity: symbol.complexity } : {}),
|
|
568
|
-
},
|
|
569
|
-
});
|
|
570
|
-
edges.push({ from: fileNodeId, to: symbol.nodeId, relation: "defines" });
|
|
571
|
-
}
|
|
572
|
-
for (const target of imports) {
|
|
573
|
-
const targetModule = normalizeImportTarget(target, relPath);
|
|
574
|
-
if (!targetModule) {
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
const importNodeId = `module:${targetModule}`;
|
|
578
|
-
nodes.push({ id: importNodeId, type: "Module", content: targetModule });
|
|
579
|
-
edges.push({ from: moduleNodeId, to: importNodeId, relation: "imports" });
|
|
580
|
-
}
|
|
225
|
+
const { nodes: fileNodes, edges: fileEdges } = (0, file_indexer_nodes_js_2.buildFileNodesAndEdges)(relPath, stat.size, language, declared, imports);
|
|
226
|
+
nodes.push(...fileNodes);
|
|
227
|
+
edges.push(...fileEdges);
|
|
228
|
+
const fileNodeId = `file:${relPath}`;
|
|
581
229
|
// Cross-file references: scan content against existing graph symbols
|
|
582
230
|
let referenceCount = 0;
|
|
583
231
|
const snapshot = client.readSnapshot?.();
|
|
584
232
|
if (snapshot && indexer) {
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (matches) {
|
|
589
|
-
const seenIdent = new Set();
|
|
590
|
-
// Build symbol index from existing snapshot (excluding this file's own symbols)
|
|
591
|
-
const symbolIndex = new Map();
|
|
592
|
-
for (const node of snapshot.nodes) {
|
|
593
|
-
if (node.type !== "Symbol" || !node.metadata?.name)
|
|
594
|
-
continue;
|
|
595
|
-
if (typeof node.metadata.file === "string" && node.metadata.file === relPath)
|
|
596
|
-
continue;
|
|
597
|
-
const list = symbolIndex.get(node.metadata.name) ?? [];
|
|
598
|
-
list.push({ nodeId: node.id, file: node.metadata.file });
|
|
599
|
-
symbolIndex.set(node.metadata.name, list);
|
|
600
|
-
}
|
|
601
|
-
for (const name of matches) {
|
|
602
|
-
if (seenIdent.has(name))
|
|
603
|
-
continue;
|
|
604
|
-
seenIdent.add(name);
|
|
605
|
-
if (name.length < 3 || REFERENCE_SKIPLIST.has(name))
|
|
606
|
-
continue;
|
|
607
|
-
if (ownNames.has(name))
|
|
608
|
-
continue;
|
|
609
|
-
const defs = symbolIndex.get(name);
|
|
610
|
-
if (!defs || defs.length === 0)
|
|
611
|
-
continue;
|
|
612
|
-
for (const def of defs) {
|
|
613
|
-
edges.push({ from: fileNodeId, to: def.nodeId, relation: "references" });
|
|
614
|
-
referenceCount += 1;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
// ── Call graph + inheritance edges (single-file incremental) ───────
|
|
620
|
-
// Build a combined symbol index: local symbols + snapshot symbols
|
|
621
|
-
const combinedSymbolIndex = new Map();
|
|
622
|
-
for (const sym of declared) {
|
|
623
|
-
const list = combinedSymbolIndex.get(sym.name) ?? [];
|
|
624
|
-
list.push({ nodeId: sym.nodeId, file: relPath });
|
|
625
|
-
combinedSymbolIndex.set(sym.name, list);
|
|
233
|
+
const { edges: refEdges, referenceCount: refCount } = (0, file_indexer_edges_js_1.buildSingleFileReferenceEdges)(fileNodeId, relPath, content, declared, snapshot.nodes);
|
|
234
|
+
edges.push(...refEdges);
|
|
235
|
+
referenceCount = refCount;
|
|
626
236
|
}
|
|
237
|
+
// Call graph + inheritance edges
|
|
627
238
|
if (snapshot) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const name = node.metadata.name;
|
|
632
|
-
const file = typeof node.metadata.file === "string" ? node.metadata.file : "";
|
|
633
|
-
const list = combinedSymbolIndex.get(name) ?? [];
|
|
634
|
-
list.push({ nodeId: node.id, file });
|
|
635
|
-
combinedSymbolIndex.set(name, list);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
const localByName = new Map();
|
|
639
|
-
for (const sym of declared) {
|
|
640
|
-
localByName.set(sym.name, sym.nodeId);
|
|
641
|
-
}
|
|
642
|
-
// Call edges
|
|
643
|
-
for (const call of fileCalls) {
|
|
644
|
-
const calleeDefs = combinedSymbolIndex.get(call.callee);
|
|
645
|
-
if (!calleeDefs || calleeDefs.length === 0)
|
|
646
|
-
continue;
|
|
647
|
-
const calleeDef = calleeDefs.find((d) => d.file === relPath) ?? calleeDefs[0];
|
|
648
|
-
if (!calleeDef)
|
|
649
|
-
continue;
|
|
650
|
-
let callerNodeId;
|
|
651
|
-
if (call.caller) {
|
|
652
|
-
callerNodeId = localByName.get(call.caller);
|
|
653
|
-
}
|
|
654
|
-
if (!callerNodeId) {
|
|
655
|
-
callerNodeId = resolveCallerAtLine(declared, call.line);
|
|
656
|
-
}
|
|
657
|
-
if (!callerNodeId)
|
|
658
|
-
continue;
|
|
659
|
-
edges.push({ from: callerNodeId, to: calleeDef.nodeId, relation: "calls" });
|
|
660
|
-
}
|
|
661
|
-
// Inheritance edges
|
|
662
|
-
for (const rel of fileInherits) {
|
|
663
|
-
const childNodeId = localByName.get(rel.child);
|
|
664
|
-
if (!childNodeId)
|
|
665
|
-
continue;
|
|
666
|
-
const parentDefs = combinedSymbolIndex.get(rel.parent);
|
|
667
|
-
if (!parentDefs || parentDefs.length === 0)
|
|
668
|
-
continue;
|
|
669
|
-
const parentDef = parentDefs.find((d) => d.file === relPath) ?? parentDefs[0];
|
|
670
|
-
if (!parentDef)
|
|
671
|
-
continue;
|
|
672
|
-
edges.push({ from: childNodeId, to: parentDef.nodeId, relation: "inherits" });
|
|
239
|
+
const { edges: ciEdges, callCount, inheritCount } = (0, file_indexer_edges_js_1.buildSingleFileCallAndInheritEdges)(relPath, declared, fileCalls, fileInherits, snapshot.nodes);
|
|
240
|
+
edges.push(...ciEdges);
|
|
241
|
+
referenceCount += callCount + inheritCount;
|
|
673
242
|
}
|
|
674
243
|
await client.upsertNodes(nodes);
|
|
675
|
-
await client.upsertEdges(dedupEdges(edges));
|
|
244
|
+
await client.upsertEdges((0, file_indexer_edges_js_1.dedupEdges)(edges));
|
|
676
245
|
// Update cache entry for this file
|
|
677
246
|
cacheState[relPath] = {
|
|
678
247
|
mtimeMs,
|
|
679
248
|
hash: currentHash,
|
|
680
249
|
numNodes: nodes.length,
|
|
681
250
|
};
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
685
|
-
(0, node_fs_1.writeFileSync)(cachePath, JSON.stringify({ version: 2, state: cacheState }, null, 2), "utf8");
|
|
686
|
-
}
|
|
687
|
-
catch (error) {
|
|
688
|
-
logger_1.logger.warn({ error }, "Failed to write index cache after single-file index");
|
|
689
|
-
}
|
|
690
|
-
logger_1.logger.info({ relPath, symbols: declared.length, references: referenceCount }, "Single file indexed");
|
|
251
|
+
(0, file_indexer_cache_js_2.saveCacheState)(cachePath, cacheState);
|
|
252
|
+
logger_js_1.logger.info({ relPath, symbols: declared.length, references: referenceCount }, "Single file indexed");
|
|
691
253
|
return {
|
|
692
254
|
indexedFiles: 1,
|
|
693
255
|
indexedSymbols: declared.length,
|
|
@@ -695,27 +257,32 @@ async function indexSingleFile(client, rootDir, absPath, options) {
|
|
|
695
257
|
skipped: false,
|
|
696
258
|
};
|
|
697
259
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
function hasIndexCache(rootDir) {
|
|
703
|
-
const cachePath = (0, node_path_1.join)(rootDir, CACHE_DIR, CACHE_FILE);
|
|
704
|
-
if (!(0, node_fs_1.existsSync)(cachePath))
|
|
705
|
-
return false;
|
|
706
|
-
try {
|
|
707
|
-
const cacheState = loadCacheState(cachePath, false);
|
|
708
|
-
return Object.keys(cacheState).length > 0;
|
|
260
|
+
// ── Internal helpers ─────────────────────────────────────────────────
|
|
261
|
+
async function pruneFileFromGraph(client, relPath) {
|
|
262
|
+
if (!client.readSnapshot || !client.deleteNode) {
|
|
263
|
+
return;
|
|
709
264
|
}
|
|
710
|
-
|
|
711
|
-
|
|
265
|
+
const snapshot = client.readSnapshot();
|
|
266
|
+
const fileNodeId = `file:${relPath}`;
|
|
267
|
+
const moduleNodeId = `module:${(0, file_indexer_nodes_js_2.moduleKey)(relPath)}`;
|
|
268
|
+
const symbolPrefix = `symbol:${relPath}:`;
|
|
269
|
+
// 先收集要删除的节点 ID 集合,便于后续 O(1) 查找悬空边
|
|
270
|
+
const deletedIds = new Set();
|
|
271
|
+
const toDelete = snapshot.nodes.filter((node) => node.id === fileNodeId || node.id === moduleNodeId || node.id.startsWith(symbolPrefix));
|
|
272
|
+
for (const node of toDelete) {
|
|
273
|
+
deletedIds.add(node.id);
|
|
274
|
+
await client.deleteNode(node.id);
|
|
712
275
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
276
|
+
// 清理跨文件的悬空引用边:删除任何 from 或 to 指向已删除节点 ID 的边。
|
|
277
|
+
// 某些传输后端(如 MCP HTTP)的 deleteNode 不保证级联清理边,因此显式清理以确保一致。
|
|
278
|
+
if (client.deleteEdge && deletedIds.size > 0) {
|
|
279
|
+
// 重新读取快照以获取当前边状态(部分后端的 deleteNode 可能已级联删除相关边)
|
|
280
|
+
const currentEdges = client.readSnapshot().edges;
|
|
281
|
+
for (const edge of currentEdges) {
|
|
282
|
+
if (deletedIds.has(edge.from) || deletedIds.has(edge.to)) {
|
|
283
|
+
await client.deleteEdge(edge.from, edge.to, edge.relation);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
718
286
|
}
|
|
719
|
-
return relPath.slice(idx).toLowerCase();
|
|
720
287
|
}
|
|
721
288
|
//# sourceMappingURL=file-indexer.js.map
|