@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.
Files changed (142) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/insight.js +1 -1
  3. package/dist/agents/insight.js.map +1 -1
  4. package/dist/config/defaults.d.ts.map +1 -1
  5. package/dist/config/defaults.js +1 -0
  6. package/dist/config/defaults.js.map +1 -1
  7. package/dist/config/embedding-factory.d.ts.map +1 -1
  8. package/dist/config/embedding-factory.js +23 -12
  9. package/dist/config/embedding-factory.js.map +1 -1
  10. package/dist/config/loader.d.ts +11 -0
  11. package/dist/config/loader.d.ts.map +1 -1
  12. package/dist/config/loader.js +103 -0
  13. package/dist/config/loader.js.map +1 -1
  14. package/dist/core/agent-assignment.d.ts +39 -0
  15. package/dist/core/agent-assignment.d.ts.map +1 -0
  16. package/dist/core/agent-assignment.js +73 -0
  17. package/dist/core/agent-assignment.js.map +1 -0
  18. package/dist/core/dag-checkpoint.d.ts +83 -0
  19. package/dist/core/dag-checkpoint.d.ts.map +1 -0
  20. package/dist/core/dag-checkpoint.js +164 -0
  21. package/dist/core/dag-checkpoint.js.map +1 -0
  22. package/dist/core/dag-engine.d.ts +9 -0
  23. package/dist/core/dag-engine.d.ts.map +1 -1
  24. package/dist/core/dag-engine.js +42 -0
  25. package/dist/core/dag-engine.js.map +1 -1
  26. package/dist/core/orchestrator-context.d.ts +10 -0
  27. package/dist/core/orchestrator-context.d.ts.map +1 -0
  28. package/dist/core/orchestrator-context.js +112 -0
  29. package/dist/core/orchestrator-context.js.map +1 -0
  30. package/dist/core/orchestrator-episode.d.ts +13 -0
  31. package/dist/core/orchestrator-episode.d.ts.map +1 -0
  32. package/dist/core/orchestrator-episode.js +128 -0
  33. package/dist/core/orchestrator-episode.js.map +1 -0
  34. package/dist/core/orchestrator-route.d.ts +20 -0
  35. package/dist/core/orchestrator-route.d.ts.map +1 -0
  36. package/dist/core/orchestrator-route.js +51 -0
  37. package/dist/core/orchestrator-route.js.map +1 -0
  38. package/dist/core/orchestrator.d.ts +2 -38
  39. package/dist/core/orchestrator.d.ts.map +1 -1
  40. package/dist/core/orchestrator.js +97 -286
  41. package/dist/core/orchestrator.js.map +1 -1
  42. package/dist/core/triage.d.ts +21 -0
  43. package/dist/core/triage.d.ts.map +1 -1
  44. package/dist/core/triage.js +25 -6
  45. package/dist/core/triage.js.map +1 -1
  46. package/dist/core/types.d.ts +53 -0
  47. package/dist/core/types.d.ts.map +1 -1
  48. package/dist/graph/context-slicer-types.d.ts +62 -0
  49. package/dist/graph/context-slicer-types.d.ts.map +1 -0
  50. package/dist/graph/context-slicer-types.js +13 -0
  51. package/dist/graph/context-slicer-types.js.map +1 -0
  52. package/dist/graph/context-slicer-utils.d.ts +24 -0
  53. package/dist/graph/context-slicer-utils.d.ts.map +1 -0
  54. package/dist/graph/context-slicer-utils.js +118 -0
  55. package/dist/graph/context-slicer-utils.js.map +1 -0
  56. package/dist/graph/context-slicer.d.ts +6 -61
  57. package/dist/graph/context-slicer.d.ts.map +1 -1
  58. package/dist/graph/context-slicer.js +73 -170
  59. package/dist/graph/context-slicer.js.map +1 -1
  60. package/dist/graph/file-indexer-cache.d.ts +34 -0
  61. package/dist/graph/file-indexer-cache.d.ts.map +1 -0
  62. package/dist/graph/file-indexer-cache.js +105 -0
  63. package/dist/graph/file-indexer-cache.js.map +1 -0
  64. package/dist/graph/file-indexer-edges.d.ts +55 -0
  65. package/dist/graph/file-indexer-edges.d.ts.map +1 -0
  66. package/dist/graph/file-indexer-edges.js +258 -0
  67. package/dist/graph/file-indexer-edges.js.map +1 -0
  68. package/dist/graph/file-indexer-nodes.d.ts +43 -0
  69. package/dist/graph/file-indexer-nodes.d.ts.map +1 -0
  70. package/dist/graph/file-indexer-nodes.js +137 -0
  71. package/dist/graph/file-indexer-nodes.js.map +1 -0
  72. package/dist/graph/file-indexer-walker.d.ts +35 -0
  73. package/dist/graph/file-indexer-walker.d.ts.map +1 -0
  74. package/dist/graph/file-indexer-walker.js +80 -0
  75. package/dist/graph/file-indexer-walker.js.map +1 -0
  76. package/dist/graph/file-indexer.d.ts +13 -39
  77. package/dist/graph/file-indexer.d.ts.map +1 -1
  78. package/dist/graph/file-indexer.js +167 -600
  79. package/dist/graph/file-indexer.js.map +1 -1
  80. package/dist/index.d.ts +1 -1
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +2 -1
  83. package/dist/index.js.map +1 -1
  84. package/dist/integrations/agent-mcp-installer.d.ts +25 -1
  85. package/dist/integrations/agent-mcp-installer.d.ts.map +1 -1
  86. package/dist/integrations/agent-mcp-installer.js +160 -4
  87. package/dist/integrations/agent-mcp-installer.js.map +1 -1
  88. package/dist/integrations/skill-installer.d.ts +50 -0
  89. package/dist/integrations/skill-installer.d.ts.map +1 -0
  90. package/dist/integrations/skill-installer.js +373 -0
  91. package/dist/integrations/skill-installer.js.map +1 -0
  92. package/dist/learning/embeddings.d.ts +11 -0
  93. package/dist/learning/embeddings.d.ts.map +1 -1
  94. package/dist/learning/embeddings.js +24 -0
  95. package/dist/learning/embeddings.js.map +1 -1
  96. package/dist/learning/episodic-memory.d.ts +3 -2
  97. package/dist/learning/episodic-memory.d.ts.map +1 -1
  98. package/dist/learning/episodic-memory.js +105 -10
  99. package/dist/learning/episodic-memory.js.map +1 -1
  100. package/dist/learning/local-embedding.d.ts.map +1 -1
  101. package/dist/learning/local-embedding.js +16 -0
  102. package/dist/learning/local-embedding.js.map +1 -1
  103. package/dist/learning/seed-skills.d.ts +21 -0
  104. package/dist/learning/seed-skills.d.ts.map +1 -0
  105. package/dist/learning/seed-skills.js +113 -0
  106. package/dist/learning/seed-skills.js.map +1 -0
  107. package/dist/learning/skill-evolution.d.ts +1 -1
  108. package/dist/learning/skill-evolution.d.ts.map +1 -1
  109. package/dist/learning/skill-evolution.js +1 -1
  110. package/dist/learning/skill-evolution.js.map +1 -1
  111. package/dist/learning/skill-package.d.ts +55 -0
  112. package/dist/learning/skill-package.d.ts.map +1 -0
  113. package/dist/learning/skill-package.js +115 -0
  114. package/dist/learning/skill-package.js.map +1 -0
  115. package/dist/learning/triage-telemetry.d.ts +43 -0
  116. package/dist/learning/triage-telemetry.d.ts.map +1 -0
  117. package/dist/learning/triage-telemetry.js +114 -0
  118. package/dist/learning/triage-telemetry.js.map +1 -0
  119. package/dist/surfaces/cli/index.js +65 -0
  120. package/dist/surfaces/cli/index.js.map +1 -1
  121. package/dist/surfaces/cli/init.d.ts +12 -2
  122. package/dist/surfaces/cli/init.d.ts.map +1 -1
  123. package/dist/surfaces/cli/init.js +109 -30
  124. package/dist/surfaces/cli/init.js.map +1 -1
  125. package/dist/surfaces/cli/output.d.ts.map +1 -1
  126. package/dist/surfaces/cli/output.js +5 -0
  127. package/dist/surfaces/cli/output.js.map +1 -1
  128. package/dist/surfaces/cli/runtime/graph.d.ts +23 -0
  129. package/dist/surfaces/cli/runtime/graph.d.ts.map +1 -1
  130. package/dist/surfaces/cli/runtime/graph.js +40 -0
  131. package/dist/surfaces/cli/runtime/graph.js.map +1 -1
  132. package/dist/surfaces/cli/runtime/routing.d.ts.map +1 -1
  133. package/dist/surfaces/cli/runtime/routing.js +8 -1
  134. package/dist/surfaces/cli/runtime/routing.js.map +1 -1
  135. package/dist/surfaces/cli/runtime.d.ts +1 -1
  136. package/dist/surfaces/cli/runtime.d.ts.map +1 -1
  137. package/dist/surfaces/cli/runtime.js +4 -1
  138. package/dist/surfaces/cli/runtime.js.map +1 -1
  139. package/dist/surfaces/mcp/tool-definitions.js +20 -20
  140. package/dist/surfaces/mcp/tool-definitions.js.map +1 -1
  141. package/package.json +3 -3
  142. 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 = 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 index_1 = require("./language-indexers/index");
15
- const BASE_EXTENSIONS = [".md", ".json"];
16
- const DEFAULT_EXTENSIONS = Array.from(new Set([...index_1.ALL_LANGUAGE_EXTENSIONS, ...BASE_EXTENSIONS]));
17
- const DEFAULT_MAX_FILE_SIZE = 200_000;
18
- const IGNORED_DIRS = new Set([
19
- ".git", "node_modules", "dist", "coverage", "tmp", "venv", ".venv", "env", ".env",
20
- "__pycache__", ".vscode", ".idea", ".next", "build", "install", "log",
21
- ".graphflow-cache", "graphflow-out", "graphify-out",
22
- ]);
23
- const REFERENCE_SKIPLIST = new Set([
24
- "if", "for", "let", "const", "var", "this", "new", "return", "true", "false",
25
- "null", "undefined", "console", "process", "require", "module", "exports",
26
- "import", "export", "default", "async", "await", "function", "class",
27
- "interface", "type", "enum", "from", "as", "of", "in", "do", "while",
28
- "switch", "case", "break", "continue", "throw", "try", "catch", "finally",
29
- "void", "yield", "number", "string", "boolean", "object", "any", "unknown",
30
- "never", "Array", "Promise", "Map", "Set", "Date", "Error", "JSON", "Math",
31
- "Object", "String", "Number", "Boolean", "Symbol", "Function", "RegExp",
32
- "def", "fn", "pub", "struct", "trait", "impl", "use", "mod", "func",
33
- "package", "include", "namespace", "typedef", "define", "static", "inline",
34
- "extern", "virtual", "nil", "None", "True", "False", "int", "char", "long",
35
- "short", "float", "double", "bool", "auto", "sizeof", "self", "cls",
36
- "and", "or", "not", "is", "lambda", "global", "nonlocal", "pass", "raise",
37
- "with", "yield", "make", "len", "cap", "append", "range", "chan", "select",
38
- "defer", "goto", "fallthrough", "size_t", "uint", "int8", "int16", "int32",
39
- "int64", "uint8", "uint16", "uint32", "uint64", "byte", "rune", "string",
40
- ]);
41
- const CACHE_DIR = ".graphflow-cache";
42
- const CACHE_FILE = "index-state.json";
43
- /** Remove graph store, index cache, and vector DB for a full rebuild. */
44
- function clearGraphIndexArtifacts(rootDir, graphStorePath) {
45
- const cachePath = (0, node_path_1.join)(rootDir, CACHE_DIR, CACHE_FILE);
46
- const vectorsPath = (0, node_path_1.join)(rootDir, CACHE_DIR, "vectors.db");
47
- (0, node_fs_1.rmSync)(graphStorePath, { force: true });
48
- (0, node_fs_1.rmSync)(cachePath, { force: true });
49
- (0, node_fs_1.rmSync)(vectorsPath, { force: true });
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
- catch (error) {
63
- const err = error;
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
- return {};
69
- }
70
- /** Returns true when workspace files changed since last index (or cache is empty). */
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
- const cachePath = (0, node_path_1.join)(rootDir, CACHE_DIR, CACHE_FILE);
79
- const cacheState = loadCacheState(cachePath, false);
80
- const scanned = walkScannableFiles(rootDir, includeExtensions, maxFileSizeBytes);
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
- for (const file of scanned) {
88
- const prev = cacheState[file.relPath];
89
- if (!prev || prev.mtimeMs !== file.mtimeMs) {
90
- return true;
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
- return false;
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
- const cachePath = (0, node_path_1.join)(rootDir, CACHE_DIR, CACHE_FILE);
100
- let cacheState = loadCacheState(cachePath, forceReindex);
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
- for (const file of scanned) {
119
- const relPath = file.relPath;
120
- const mtimeMs = file.mtimeMs;
121
- const prev = cacheState[relPath];
122
- let content = "";
123
- let currentHash = "";
124
- let isChanged = forceReindex;
125
- if (!prev || prev.mtimeMs !== mtimeMs) {
126
- content = (0, node_fs_1.readFileSync)(file.absPath, "utf8");
127
- currentHash = (0, node_crypto_1.createHash)("md5").update(content).digest("hex");
128
- if (!prev || prev.hash !== currentHash) {
129
- isChanged = true;
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
- if (!isChanged) {
133
- continue;
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 identifierRe = /\b\w{3,}\b/g;
241
- let referenceCount = 0;
242
- for (const file of parsed) {
243
- if (!file.scannable) {
244
- continue;
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
- try {
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
- async function pruneFileFromGraph(client, relPath) {
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 fileNodeId = `file:${relPath}`;
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, hash_1.hashTextHex)(sym.name)}`,
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 exportNames = declared
527
- .filter((s) => s.exported && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s.name))
528
- .map((s) => s.name);
529
- const uniqueExports = Array.from(new Set(exportNames));
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 ownNames = new Set(declared.map((s) => s.name));
586
- const identifierRe = /\b\w{3,}\b/g;
587
- const matches = content.match(identifierRe);
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
- for (const node of snapshot.nodes) {
629
- if (node.type !== "Symbol" || !node.metadata?.name)
630
- continue;
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
- try {
683
- const dir = (0, node_path_1.dirname)(cachePath);
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
- * Quick check whether the index cache exists and is non-empty.
700
- * Cheaper than hasPendingGraphIndexWork (no full workspace walk).
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
- catch {
711
- return false;
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
- function extOf(relPath) {
715
- const idx = relPath.lastIndexOf(".");
716
- if (idx < 0) {
717
- return "";
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