dsh-codegraph-visualizer 0.1.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +391 -226
  2. package/assets/icon-180.png +0 -0
  3. package/assets/icon.svg +30 -0
  4. package/cordis.patch.yml +22 -17
  5. package/dist/CodeGraphAdapter-BNIK0xHU.js +422 -0
  6. package/dist/CodeGraphAdapter-BNIK0xHU.js.map +1 -0
  7. package/dist/CodeGraphAdapter-CXj1hbdm.js +10 -0
  8. package/dist/FileSystemAdapter-CgasSGbB.js +524 -0
  9. package/dist/FileSystemAdapter-CgasSGbB.js.map +1 -0
  10. package/dist/LensAdapter-C9Saf_i_.js +89 -0
  11. package/dist/LensAdapter-C9Saf_i_.js.map +1 -0
  12. package/dist/Logger-DUIJSeNu.js +80 -0
  13. package/dist/Logger-DUIJSeNu.js.map +1 -0
  14. package/dist/TreeSitterAdapter-C-EWy8br.js +631 -0
  15. package/dist/TreeSitterAdapter-C-EWy8br.js.map +1 -0
  16. package/dist/TreeSitterAdapter-C6FcO2_s.js +8 -0
  17. package/dist/client/index.css +8963 -1724
  18. package/dist/client/index.css.map +1 -0
  19. package/dist/client/index.d.ts +634 -0
  20. package/dist/client/index.d.ts.map +1 -0
  21. package/dist/client/index.esm.js +57197 -20713
  22. package/dist/client/index.js.map +1 -0
  23. package/dist/client/{index.js → worker/layoutWorker.js} +7657 -8787
  24. package/dist/client.js +90426 -0
  25. package/dist/codeGraphDb-B-vpZmpm.js +9 -0
  26. package/dist/codeGraphDb-T6K3dK5D.js +357 -0
  27. package/dist/codeGraphDb-T6K3dK5D.js.map +1 -0
  28. package/dist/codeGraphRows-BhZX34uB.js +6 -0
  29. package/dist/codeGraphRows-C7hqu35E.js +246 -0
  30. package/dist/codeGraphRows-C7hqu35E.js.map +1 -0
  31. package/dist/constants-CQq93l3e.js +399 -0
  32. package/dist/constants-CQq93l3e.js.map +1 -0
  33. package/dist/graphFingerprint-CeARo7jG.js +4 -0
  34. package/dist/graphFingerprint-PuxORZUF.js +277 -0
  35. package/dist/graphFingerprint-PuxORZUF.js.map +1 -0
  36. package/dist/graphMetrics-C0F30730.js +555 -0
  37. package/dist/graphMetrics-C0F30730.js.map +1 -0
  38. package/dist/index.d.ts +572 -9
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +14819 -2009
  41. package/dist/index.js.map +1 -1
  42. package/dist/inheritanceEnhancer-Bi3UoYJS.js +291 -0
  43. package/dist/inheritanceEnhancer-Bi3UoYJS.js.map +1 -0
  44. package/dist/layerUtils-DDm-EsaD.js +72 -0
  45. package/dist/layerUtils-DDm-EsaD.js.map +1 -0
  46. package/dist/nativeIndex-CABbgERW.js +347 -0
  47. package/dist/nativeIndex-CABbgERW.js.map +1 -0
  48. package/dist/nativeIndex-esaDOsq8.js +5 -0
  49. package/dist/treeSitterExtractors-B1ybKNrC.js +394 -0
  50. package/dist/treeSitterExtractors-B1ybKNrC.js.map +1 -0
  51. package/dist/types-BUDxI_00.js +23 -0
  52. package/dist/types-BUDxI_00.js.map +1 -0
  53. package/locale/en.json +6 -0
  54. package/locale/zh-CN.json +6 -0
  55. package/locale/zh.json +6 -0
  56. package/package.json +100 -20
  57. package/dist/client-cjs/index.cjs +0 -44283
  58. package/dist/client-cjs/index.css +0 -2404
@@ -0,0 +1,524 @@
1
+ import { EdgeId, NodeId } from "./types-BUDxI_00.js";
2
+ import { FILE_SYSTEM } from "./constants-CQq93l3e.js";
3
+ import { scoped } from "./Logger-DUIJSeNu.js";
4
+ import { parseTsconfigPaths, resolveAliasModule, resolveCallEdges } from "./treeSitterExtractors-B1ybKNrC.js";
5
+
6
+ //#region src/adapters/gitignoreFilter.ts
7
+ const log$1 = scoped("gitignore");
8
+ /**
9
+ * 把一条 gitignore 模式编译成正则。
10
+ *
11
+ * 转译规则(gitignore → RegExp,示例用「双星」指代连续两个星号):
12
+ * - 双星加斜杠的**前缀**形式 ⇒ 可选的任意层级前缀组;
13
+ * - 双星加斜杠的**尾缀**形式 ⇒ 路径前缀匹配(目录及其全部内容);
14
+ * - 单星 ⇒ 不跨段的任意字符序列;
15
+ * - 问号 ⇒ 不跨段的单字符;
16
+ * - 其余字符字面量转义。
17
+ */
18
+ function compilePattern(pattern, anchored) {
19
+ let re = "";
20
+ let i = 0;
21
+ if (pattern.endsWith("/")) pattern = pattern.slice(0, -1);
22
+ while (i < pattern.length) {
23
+ const two = pattern.slice(i, i + 3);
24
+ if (two === "**/") {
25
+ re += "(?:.*/)?";
26
+ i += 3;
27
+ continue;
28
+ }
29
+ const ch = pattern[i];
30
+ if (ch === "*") if (pattern[i + 1] === "*") {
31
+ re += ".*";
32
+ i += 2;
33
+ } else {
34
+ re += "[^/]*";
35
+ i += 1;
36
+ }
37
+ else if (ch === "?") {
38
+ re += "[^/]";
39
+ i += 1;
40
+ } else if (ch === "/" && i === 0) i += 1;
41
+ else {
42
+ re += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
43
+ i += 1;
44
+ }
45
+ }
46
+ if (!re) return null;
47
+ const body = anchored ? `^${re}` : `(?:^|/)${re}`;
48
+ return new RegExp(`${body}(?:$|/)`);
49
+ }
50
+ /** 解析 .gitignore 文本为规则列表(注释/空行丢弃;语法不可能错——坏行按字面量降级)。 */
51
+ function parseGitignore(text) {
52
+ const rules = [];
53
+ for (const rawLine of text.split(/\r?\n/)) {
54
+ const line = rawLine.trim();
55
+ if (!line || line.startsWith("#")) continue;
56
+ let pattern = line;
57
+ let negated = false;
58
+ if (pattern.startsWith("!")) {
59
+ negated = true;
60
+ pattern = pattern.slice(1);
61
+ }
62
+ if (!pattern) continue;
63
+ const dirOnly = pattern.endsWith("/");
64
+ const barePattern = dirOnly ? pattern.slice(0, -1) : pattern;
65
+ const anchored = barePattern.includes("/");
66
+ const re = compilePattern(pattern, anchored);
67
+ if (!re) continue;
68
+ rules.push({
69
+ raw: line,
70
+ negated,
71
+ dirOnly,
72
+ anchored,
73
+ re
74
+ });
75
+ }
76
+ return rules;
77
+ }
78
+ /**
79
+ * 对「相对仓库根、/ 分隔」的路径做匹配裁决。
80
+ *
81
+ * 顺序语义与 git 一致:**最后一条命中的规则赢**(后写的 `!` 取反能覆盖先前的排除)。
82
+ * dirOnly 规则对文件不跳过:尾部 `(?:$|/)` 边界让「目录排除」覆盖目录之下的全部
83
+ * 内容(artifacts/ 排除 artifacts 目录与其下每个文件)——编译层已实现该语义,
84
+ * 这里不做 isDir 分流。
85
+ */
86
+ function isIgnoredByRules(rules, relPath, _isDir) {
87
+ let ignored = false;
88
+ for (const rule of rules) if (rule.re.test(relPath)) ignored = !rule.negated;
89
+ return ignored;
90
+ }
91
+ /** 硬编码兜底目录表(= FileSystemAdapter.IGNORED_DIRS 的同源常量,单一真源在此)。 */
92
+ const DEFAULT_IGNORED_DIRS = new Set([
93
+ "node_modules",
94
+ ".git",
95
+ "dist",
96
+ "build",
97
+ "target",
98
+ "__pycache__",
99
+ ".venv",
100
+ "venv",
101
+ ".next",
102
+ "coverage",
103
+ ".cache",
104
+ ".codegraph",
105
+ ".codegraph-visualizer"
106
+ ]);
107
+ /**
108
+ * 聚合裁决器:硬编码目录兜底 + .gitignore 规则(并集)。
109
+ * 返回给 FileSystemAdapter.scanDir 的统一判定入口。
110
+ */
111
+ var GitignoreFilter = class {
112
+ rules;
113
+ hasNegationRules;
114
+ /** 硬编码兜底是否生效(.gitignore 在场时不减——两者并集)。 */
115
+ defaultDirs;
116
+ constructor(rules, defaultDirs = DEFAULT_IGNORED_DIRS) {
117
+ this.rules = rules;
118
+ this.hasNegationRules = rules.some((r) => r.negated);
119
+ this.defaultDirs = defaultDirs;
120
+ }
121
+ /** 规则数(管理面板「索引覆盖范围」的白盒数据)。 */
122
+ get ruleCount() {
123
+ return this.rules.length;
124
+ }
125
+ /**
126
+ * 是否含取反规则(scanDir 的剪枝策略开关)。
127
+ *
128
+ * 取反在场时目录剪枝必须降级为「不生成节点但仍递归」——否则
129
+ * `!dir/white-listed.ext` 永远救不回被排除目录里的白名单文件
130
+ *(目录级剪枝发生在遍历之前,白名单文件根本不会被看到)。
131
+ */
132
+ get hasNegation() {
133
+ return this.hasNegationRules;
134
+ }
135
+ /** 目录名是否被硬编码兜底排除(scanDir 剪枝用,O(1))。 */
136
+ isDefaultIgnoredDir(name) {
137
+ return this.defaultDirs.has(name);
138
+ }
139
+ /** 统一裁决:相对路径(/ 分隔)+ 是否目录。 */
140
+ isIgnored(relPath, isDir) {
141
+ if (this.rules.length === 0) return false;
142
+ return isIgnoredByRules(this.rules, relPath, isDir);
143
+ }
144
+ };
145
+ /** 便捷构造:从文件系统读 .gitignore 并连同兜底表一起编译。读失败 = 纯兜底。 */
146
+ function createGitignoreFilter(readFile, repoRoot) {
147
+ let rules = [];
148
+ let present = false;
149
+ try {
150
+ const text = readFile(`${repoRoot}/.gitignore`);
151
+ if (typeof text === "string") {
152
+ rules = parseGitignore(text);
153
+ present = true;
154
+ }
155
+ } catch (e) {
156
+ log$1.debug("gitignore read failed — default dirs only", {
157
+ repoRoot,
158
+ error: String(e)
159
+ });
160
+ }
161
+ return {
162
+ filter: new GitignoreFilter(rules),
163
+ gitignorePresent: present
164
+ };
165
+ }
166
+
167
+ //#endregion
168
+ //#region src/adapters/FileSystemAdapter.ts
169
+ const log = scoped("fs-adapter");
170
+ const SUPPORTED_EXTENSIONS = new Set([
171
+ ".ts",
172
+ ".tsx",
173
+ ".js",
174
+ ".jsx",
175
+ ".mjs",
176
+ ".py",
177
+ ".java",
178
+ ".rs",
179
+ ".go",
180
+ ".json",
181
+ ".toml",
182
+ ".yaml",
183
+ ".yml"
184
+ ]);
185
+ const IGNORED_DIRS = DEFAULT_IGNORED_DIRS;
186
+ function getLanguageFromExt(ext) {
187
+ const langMap = {
188
+ ".ts": "typescript",
189
+ ".tsx": "typescript",
190
+ ".js": "javascript",
191
+ ".jsx": "javascript",
192
+ ".mjs": "javascript",
193
+ ".py": "python",
194
+ ".java": "java",
195
+ ".rs": "rust",
196
+ ".go": "go"
197
+ };
198
+ return langMap[ext] ?? "unknown";
199
+ }
200
+ function getNodeTypeFromExt(ext) {
201
+ if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" || ext === ".mjs") return "module";
202
+ if (ext === ".py" || ext === ".java" || ext === ".rs" || ext === ".go") return "module";
203
+ return "variable";
204
+ }
205
+ var FileSystemAdapter = class {
206
+ source = "tree-sitter";
207
+ fs = null;
208
+ treeSitterAdapter = null;
209
+ setFileSystem(fs) {
210
+ this.fs = fs;
211
+ }
212
+ setTreeSitterAdapter(adapter) {
213
+ this.treeSitterAdapter = adapter;
214
+ }
215
+ async fetchData(repoId, _invoke) {
216
+ if (!this.fs) {
217
+ log.warn("no filesystem available");
218
+ return {
219
+ nodes: [],
220
+ edges: [],
221
+ source: this.source,
222
+ timestamp: Date.now()
223
+ };
224
+ }
225
+ const nodes = [];
226
+ const edges = [];
227
+ const visitedDirs = new Set();
228
+ let fileCount = 0;
229
+ let ignoredCount = 0;
230
+ const { filter: gitignore, gitignorePresent } = createGitignoreFilter((p) => this.fs.readFile(p), repoId);
231
+ const repoPrefix = repoId.endsWith("/") ? repoId.length : repoId.length + 1;
232
+ const relPathOf = (absPath) => absPath.slice(repoPrefix).replace(/\\/g, "/");
233
+ const scanDir = (dirPath, parentId) => {
234
+ if (fileCount >= FILE_SYSTEM.MAX_FILES) return;
235
+ if (visitedDirs.has(dirPath)) return;
236
+ visitedDirs.add(dirPath);
237
+ let entries;
238
+ try {
239
+ entries = this.fs.readdir(dirPath);
240
+ } catch {
241
+ return;
242
+ }
243
+ for (const entry of entries) {
244
+ if (fileCount >= FILE_SYSTEM.MAX_FILES) break;
245
+ const fullPath = dirPath.endsWith("/") ? dirPath + entry : `${dirPath}/${entry}`;
246
+ const baseName = entry.split("/").pop() ?? entry;
247
+ if (IGNORED_DIRS.has(baseName)) {
248
+ ignoredCount++;
249
+ continue;
250
+ }
251
+ let isDir;
252
+ try {
253
+ isDir = this.fs.stat(fullPath).isDirectory;
254
+ } catch {
255
+ continue;
256
+ }
257
+ const rel = relPathOf(fullPath);
258
+ const ignored = gitignore.isIgnored(rel, isDir);
259
+ if (ignored && (!isDir || !gitignore.hasNegation)) {
260
+ ignoredCount++;
261
+ continue;
262
+ }
263
+ if (ignored && isDir) {
264
+ ignoredCount++;
265
+ scanDir(fullPath);
266
+ continue;
267
+ }
268
+ if (isDir) {
269
+ const dirNodeId = `dir:${fullPath}`;
270
+ nodes.push({
271
+ id: NodeId(dirNodeId),
272
+ label: baseName,
273
+ type: "module",
274
+ filePath: fullPath,
275
+ properties: { language: "directory" }
276
+ });
277
+ if (parentId) edges.push({
278
+ id: EdgeId(`contains:${parentId}:${dirNodeId}`),
279
+ source: NodeId(parentId),
280
+ target: NodeId(dirNodeId),
281
+ type: "contains",
282
+ properties: {}
283
+ });
284
+ scanDir(fullPath, dirNodeId);
285
+ } else {
286
+ const ext = "." + (baseName.split(".").pop() ?? "");
287
+ if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
288
+ const fileNodeId = `file:${fullPath}`;
289
+ const lang = getLanguageFromExt(ext);
290
+ const nodeType = getNodeTypeFromExt(ext);
291
+ nodes.push({
292
+ id: NodeId(fileNodeId),
293
+ label: baseName,
294
+ type: nodeType,
295
+ filePath: fullPath,
296
+ properties: {
297
+ language: lang,
298
+ extension: ext
299
+ }
300
+ });
301
+ fileCount++;
302
+ if (parentId) edges.push({
303
+ id: EdgeId(`contains:${parentId}:${fileNodeId}`),
304
+ source: NodeId(parentId),
305
+ target: NodeId(fileNodeId),
306
+ type: "contains",
307
+ properties: {}
308
+ });
309
+ }
310
+ }
311
+ };
312
+ scanDir(repoId);
313
+ log.info(`scanned ${fileCount} files, ${nodes.length} nodes, ${edges.length} edges, gitignore ${gitignorePresent ? `${gitignore.ruleCount} rules` : "absent"}, ignored ${ignoredCount}`);
314
+ return {
315
+ nodes,
316
+ edges,
317
+ source: this.source,
318
+ timestamp: Date.now(),
319
+ scanCoverage: {
320
+ gitignorePresent,
321
+ gitignoreRules: gitignore.ruleCount,
322
+ ignoredEntries: ignoredCount
323
+ }
324
+ };
325
+ }
326
+ async fetchDataWithSymbols(repoId, _invoke) {
327
+ if (!this.fs) {
328
+ log.warn("no filesystem available");
329
+ return {
330
+ nodes: [],
331
+ edges: [],
332
+ source: this.source,
333
+ timestamp: Date.now()
334
+ };
335
+ }
336
+ const structureResult = await this.fetchData(repoId, _invoke);
337
+ if (!this.treeSitterAdapter) {
338
+ log.info("no tree-sitter adapter attached, returning structure-only result");
339
+ return structureResult;
340
+ }
341
+ const filesToParse = [];
342
+ const CODE_EXTENSIONS = new Set([
343
+ ".ts",
344
+ ".tsx",
345
+ ".js",
346
+ ".jsx",
347
+ ".mjs",
348
+ ".py",
349
+ ".java",
350
+ ".rs",
351
+ ".go"
352
+ ]);
353
+ for (const node of structureResult.nodes) {
354
+ if (node.type !== "module" && node.type !== "variable") continue;
355
+ const ext = "." + (node.filePath?.split(".").pop() ?? "");
356
+ if (!CODE_EXTENSIONS.has(ext)) continue;
357
+ try {
358
+ const content = this.fs.readFile(node.filePath);
359
+ filesToParse.push({
360
+ filePath: node.filePath,
361
+ content
362
+ });
363
+ } catch {
364
+ log.debug(`could not read file: ${node.filePath}`);
365
+ }
366
+ }
367
+ if (filesToParse.length === 0) return structureResult;
368
+ log.info(`parsing ${filesToParse.length} files with tree-sitter`);
369
+ const symbolResult = await this.treeSitterAdapter.batchScan(filesToParse);
370
+ const allNodes = [...structureResult.nodes, ...symbolResult.nodes];
371
+ const allEdges = [...structureResult.edges, ...symbolResult.edges];
372
+ for (const symbolNode of symbolResult.nodes) {
373
+ const fileNodeId = `file:${symbolNode.filePath}`;
374
+ allEdges.push({
375
+ id: EdgeId(`symcontains:${fileNodeId}:${symbolNode.id}`),
376
+ source: NodeId(fileNodeId),
377
+ target: symbolNode.id,
378
+ type: "contains",
379
+ properties: {}
380
+ });
381
+ }
382
+ let aliasMapping = null;
383
+ try {
384
+ const tsconfigText = this.fs.readFile(`${repoId}/tsconfig.json`);
385
+ aliasMapping = parseTsconfigPaths(tsconfigText);
386
+ if (aliasMapping) log.info(`tsconfig paths loaded: ${aliasMapping.entries.length} alias patterns`);
387
+ } catch {}
388
+ const fileSet = new Set(structureResult.nodes.filter((n) => String(n.id).startsWith("file:")).map((n) => String(n.filePath)));
389
+ const EXT_CANDIDATES = [
390
+ ".ts",
391
+ ".tsx",
392
+ ".js",
393
+ ".jsx",
394
+ ".mjs",
395
+ ".py",
396
+ ".rs",
397
+ ".go",
398
+ ".java",
399
+ "/index.ts",
400
+ "/index.js"
401
+ ];
402
+ const repoIdPrefix = repoId.endsWith("/") ? repoId : `${repoId}/`;
403
+ /** 候选解析:先试原样(模块名已带扩展名——真实项目普遍形态,cycle-131 真机
404
+
405
+ * 发现:`./closureModel.ts` 拼接扩展名变 `.ts.ts` 全落空),再走扩展名枚举。 */
406
+ const tryCandidates = (joined) => {
407
+ if (fileSet.has(joined)) return joined;
408
+ for (const ext of EXT_CANDIDATES) {
409
+ const candidate = `${joined}${ext}`;
410
+ if (fileSet.has(candidate)) return candidate;
411
+ }
412
+ return null;
413
+ };
414
+ const resolveModuleToFile = (fromFile, moduleName) => {
415
+ if (aliasMapping) {
416
+ const aliasRel = resolveAliasModule(aliasMapping, moduleName);
417
+ if (aliasRel) {
418
+ const absCandidate = tryCandidates(`${repoIdPrefix}${aliasRel}`);
419
+ return absCandidate;
420
+ }
421
+ }
422
+ if (moduleName.startsWith("./") || moduleName.startsWith("../")) {
423
+ const base = fromFile.split("/").slice(0, -1).join("/");
424
+ let joined;
425
+ if (moduleName.startsWith("./")) joined = `${base}/${moduleName.slice(2)}`;
426
+ else {
427
+ let parts = base.split("/");
428
+ let rest = moduleName;
429
+ while (rest.startsWith("../")) {
430
+ parts = parts.slice(0, -1);
431
+ rest = rest.slice(3);
432
+ }
433
+ joined = `${parts.join("/")}/${rest}`;
434
+ }
435
+ return tryCandidates(joined);
436
+ }
437
+ return tryCandidates(moduleName);
438
+ };
439
+ let importEdgeCount = 0;
440
+ /**
441
+
442
+ * 源文件(绝对路径)→ 被 import 到的目标文件(绝对路径)集合。
443
+
444
+ * call 边的第二跳(inferred)**只允许**沿这条已解析出的 import 关系走:
445
+
446
+ * import 解析不到的文件不在图里,也就不该成为 call 边端点(宁缺勿造)。
447
+
448
+ */
449
+ const importedFilesByFile = new Map();
450
+ for (const [fromFile, modules] of symbolResult.importsByFile ?? []) {
451
+ const fromFileId = `file:${fromFile}`;
452
+ for (const moduleName of modules) {
453
+ const resolved = resolveModuleToFile(fromFile, moduleName);
454
+ if (!resolved) continue;
455
+ const targetId = `file:${resolved}`;
456
+ if (targetId === fromFileId) continue;
457
+ allEdges.push({
458
+ id: EdgeId(`tsimport:${fromFileId}:${targetId}`),
459
+ source: NodeId(fromFileId),
460
+ target: NodeId(targetId),
461
+ type: "import",
462
+ properties: {
463
+ edgeOrigin: "ast",
464
+ module: moduleName
465
+ }
466
+ });
467
+ importEdgeCount++;
468
+ const bucket = importedFilesByFile.get(fromFile);
469
+ if (bucket) bucket.add(resolved);
470
+ else importedFilesByFile.set(fromFile, new Set([resolved]));
471
+ }
472
+ }
473
+ const symbolsByFile = new Map();
474
+ for (const symbolNode of symbolResult.nodes) {
475
+ const bucket = symbolsByFile.get(symbolNode.filePath);
476
+ const entry = {
477
+ label: symbolNode.label,
478
+ id: String(symbolNode.id)
479
+ };
480
+ if (bucket) bucket.push(entry);
481
+ else symbolsByFile.set(symbolNode.filePath, [entry]);
482
+ }
483
+ let callEdgeCount = 0;
484
+ let callAmbiguous = 0;
485
+ let callUnresolved = 0;
486
+ let callExact = 0;
487
+ let callInferred = 0;
488
+ for (const [fromFile, callSites] of symbolResult.callSitesByFile ?? []) {
489
+ const importedFileSymbols = new Map();
490
+ for (const importedFile of importedFilesByFile.get(fromFile) ?? []) {
491
+ const symbols = symbolsByFile.get(importedFile);
492
+ if (symbols) importedFileSymbols.set(importedFile, symbols);
493
+ }
494
+ const resolved = resolveCallEdges({
495
+ callSites,
496
+ localSymbols: symbolsByFile.get(fromFile) ?? [],
497
+ importedFileSymbols
498
+ });
499
+ for (const edge of resolved.edges) allEdges.push({
500
+ id: EdgeId(edge.id),
501
+ source: NodeId(edge.source),
502
+ target: NodeId(edge.target),
503
+ type: "call",
504
+ properties: { ...edge.properties }
505
+ });
506
+ callEdgeCount += resolved.edges.length;
507
+ callAmbiguous += resolved.ambiguous;
508
+ callUnresolved += resolved.unresolved;
509
+ callExact += resolved.byEvidence.exact;
510
+ callInferred += resolved.byEvidence.inferred;
511
+ }
512
+ log.info(`fetchDataWithSymbols: ${allNodes.length} nodes (${structureResult.nodes.length} structure + ${symbolResult.nodes.length} symbols), ${allEdges.length} edges (imports ${importEdgeCount}, calls ${callEdgeCount} = exact ${callExact} + inferred ${callInferred}; ambiguous ${callAmbiguous} skipped, unresolved ${callUnresolved} skipped)`);
513
+ return {
514
+ nodes: allNodes,
515
+ edges: allEdges,
516
+ source: this.source,
517
+ timestamp: Date.now()
518
+ };
519
+ }
520
+ };
521
+
522
+ //#endregion
523
+ export { FileSystemAdapter };
524
+ //# sourceMappingURL=FileSystemAdapter-CgasSGbB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileSystemAdapter-CgasSGbB.js","names":["log","pattern: string","anchored: boolean","text: string","rules: GitignoreRule[]","rules: readonly GitignoreRule[]","relPath: string","_isDir: boolean","DEFAULT_IGNORED_DIRS: ReadonlySet<string>","defaultDirs: ReadonlySet<string>","name: string","isDir: boolean","readFile: (path: string) => string | undefined","repoRoot: string","ext: string","langMap: Record<string, string>","fs: FileSystemApi","adapter: TreeSitterAdapter","repoId: string","_invoke?: UpstreamInvoker","nodes: GraphNode[]","edges: GraphEdge[]","absPath: string","dirPath: string","parentId?: string","entries: string[]","isDir: boolean","filesToParse: Array<{ filePath: string; content: string }>","aliasMapping: TsconfigPathsMapping | null","joined: string","fromFile: string","moduleName: string"],"sources":["../src/adapters/gitignoreFilter.ts","../src/adapters/FileSystemAdapter.ts"],"sourcesContent":["/**\n * .gitignore 过滤器(cycle-141 第七轮)。\n *\n * ## 为什么原生代码图谱要吃 .gitignore\n *\n * 本原生通道是 codegraph 开源项目在 dsh 宿主的优化原生化实现。上游对仓库的\n * 语义是「git 追踪到的源码」,而本适配器此前只有一张**硬编码目录表**\n * (IGNORED_DIRS 13 项)——用户仓库 .gitignore 明确排除的产物目录\n * (artifacts/、.agents/、test-results/、.preview/ 等)全部进了图谱:\n * 真机取证(dsh-anic 14039 节点):.dsh/.preview/.cycles 等非源码目录的\n * 目录簇占顶层可见节点的大半,图谱显示的是「文件系统」而不是「代码仓库」。\n *\n * ## 语义范围(与 gitignore 规范对齐的子集)\n *\n * 支持:`#` 注释、空行、`!` 取反、目录锚定(尾 `/`)、路径段通配 `*`、\n * `**` 跨段、行内相对路径按 `/` 锚定(gitignore 的「含 / 的模式相对\n * .gitignore 所在目录,否则匹配任意层」规则)。不支持(显式登记缺口,\n * 宁缺勿造):字符组 `[a-z]`、中段双星(`a` 与 `b` 之间的 `**`)、逐级嵌套 .gitignore\n * (只读仓库根一份——上游 ignore 包也按目录级联,但本仓扫描从根起、\n * 子目录级联的收益远小于复杂度)。\n *\n * ## 与 IGNORED_DIRS 的关系:缺省兜底,不是替代\n *\n * 没有 .gitignore 的仓库(或文件缺失/解析失败)退回硬编码目录表——\n * 「没有配置」不等于「什么都扫」。.gitignore 在场时它的排除**叠加**在\n * 硬编码表之上(两者取并集),取反 `!` 只能救回 .gitignore 自己排除的,\n * 救不回硬编码排除的(node_modules 永不进图——与上游 codegraph 的\n * 内置忽略一致)。\n *\n * ## 零依赖\n *\n * 上游用 `ignore` npm 包;本仓沿用璇玑图「库内自包含」纪律(xuanji 零宿主\n * 依赖的同族约束),glob 匹配是十几行的纯函数,不值得为此引入依赖。\n *\n * @module adapters/gitignoreFilter\n */\n\nimport { scoped } from '../shared/Logger.ts';\n\nconst log = scoped('gitignore');\n\n/** 一条编译后的 gitignore 规则。 */\ninterface GitignoreRule {\n /** 原始行(诊断显示用)。 */\n raw: string;\n /** 取反(`!` 前缀)。 */\n negated: boolean;\n /** 只匹配目录(尾 `/`)。 */\n dirOnly: boolean;\n /** 模式含 `/` ⇒ 相对仓库根锚定;否则匹配任意层级的路径段。 */\n anchored: boolean;\n /** 编译后的正则(对「相对仓库根、/ 分隔」的路径匹配)。 */\n re: RegExp;\n}\n\n/**\n * 把一条 gitignore 模式编译成正则。\n *\n * 转译规则(gitignore → RegExp,示例用「双星」指代连续两个星号):\n * - 双星加斜杠的**前缀**形式 ⇒ 可选的任意层级前缀组;\n * - 双星加斜杠的**尾缀**形式 ⇒ 路径前缀匹配(目录及其全部内容);\n * - 单星 ⇒ 不跨段的任意字符序列;\n * - 问号 ⇒ 不跨段的单字符;\n * - 其余字符字面量转义。\n */\nfunction compilePattern(pattern: string, anchored: boolean): RegExp | null {\n let re = '';\n let i = 0;\n // 尾 / 表示目录锚定,先剥掉。\n if (pattern.endsWith('/')) pattern = pattern.slice(0, -1);\n while (i < pattern.length) {\n const two = pattern.slice(i, i + 3);\n if (two === '**/') {\n re += '(?:.*/)?';\n i += 3;\n continue;\n }\n const ch = pattern[i]!;\n if (ch === '*') {\n // `**` 中段(不在前缀位置)按 gitignore 语义近似为「任意字符含 /」。\n if (pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n } else {\n re += '[^/]*';\n i += 1;\n }\n } else if (ch === '?') {\n re += '[^/]';\n i += 1;\n } else if (ch === '/' && i === 0) {\n // 前导 /(/docs/anic 形态):与「含 / 即根锚定」同义,剥掉——\n // 根锚定由下方「anchored 不加边界前缀组」统一实现,不剥会变成 //docs 永不命中。\n i += 1;\n } else {\n re += ch.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&');\n i += 1;\n }\n }\n if (!re) return null;\n // anchored(模式含 /,gitignore 语义 = 相对仓库根):必须从字符串**开头**锚定\n //(^ 前缀)——其他位置出现的同形路径段不算(other/docs/anic 不该命中 /docs/anic)。\n // unanchored(不含 /,如 artifacts、*.log):任意段边界处命中该段模式。\n // 尾部 (?:$|/) 让「目录排除」天然覆盖目录**之下**的全部内容:artifacts/ 排除\n // artifacts 本身(dir)与其下每个文件(artifacts/bundle.js 的 ^artifacts/ 命中\n // 尾组 / 分支)——dirOnly 规则对文件不该跳过,而是该按「路径在目录下」命中\n //(首轮实测:跳过导致排除目录下的文件全部漏网)。\n const body = anchored ? `^${re}` : `(?:^|/)${re}`;\n return new RegExp(`${body}(?:$|/)`);\n}\n\n/** 解析 .gitignore 文本为规则列表(注释/空行丢弃;语法不可能错——坏行按字面量降级)。 */\nexport function parseGitignore(text: string): GitignoreRule[] {\n const rules: GitignoreRule[] = [];\n for (const rawLine of text.split(/\\r?\\n/)) {\n const line = rawLine.trim();\n if (!line || line.startsWith('#')) continue;\n let pattern = line;\n let negated = false;\n if (pattern.startsWith('!')) {\n negated = true;\n pattern = pattern.slice(1);\n }\n if (!pattern) continue;\n // 尾 / 只表示目录锚定,剥掉后再判「含 / 即根锚定」——否则 \"build/\" 的尾斜杠\n // 会被误判成锚定模式(正则 ^build 从根匹配,src/build 目录漏网,首轮实测)。\n const dirOnly = pattern.endsWith('/');\n const barePattern = dirOnly ? pattern.slice(0, -1) : pattern;\n const anchored = barePattern.includes('/');\n const re = compilePattern(pattern, anchored);\n if (!re) continue;\n rules.push({ raw: line, negated, dirOnly, anchored, re });\n }\n return rules;\n}\n\n/**\n * 对「相对仓库根、/ 分隔」的路径做匹配裁决。\n *\n * 顺序语义与 git 一致:**最后一条命中的规则赢**(后写的 `!` 取反能覆盖先前的排除)。\n * dirOnly 规则对文件不跳过:尾部 `(?:$|/)` 边界让「目录排除」覆盖目录之下的全部\n * 内容(artifacts/ 排除 artifacts 目录与其下每个文件)——编译层已实现该语义,\n * 这里不做 isDir 分流。\n */\nexport function isIgnoredByRules(rules: readonly GitignoreRule[], relPath: string, _isDir: boolean): boolean {\n let ignored = false;\n for (const rule of rules) {\n if (rule.re.test(relPath)) ignored = !rule.negated;\n }\n return ignored;\n}\n\n/**\n * 从文件系统读 workspace 根的 .gitignore 并编译。\n *\n * 读取失败(文件缺失/权限/编码)= 没有规则(调用方退回硬编码兜底),\n * 不抛出——gitignore 缺席是常态而不是错误。\n */\nexport function loadGitignoreRules(readFile: (path: string) => string, repoRoot: string): GitignoreRule[] {\n try {\n const text = readFile(`${repoRoot}/.gitignore`);\n return parseGitignore(text);\n } catch {\n return [];\n }\n}\n\n/** 硬编码兜底目录表(= FileSystemAdapter.IGNORED_DIRS 的同源常量,单一真源在此)。 */\nexport const DEFAULT_IGNORED_DIRS: ReadonlySet<string> = new Set([\n 'node_modules', '.git', 'dist', 'build', 'target',\n '__pycache__', '.venv', 'venv', '.next',\n 'coverage', '.cache', '.codegraph',\n // cycle-141:.codegraph-visualizer/ 是插件自己的索引目录,不应出现在图谱中。\n '.codegraph-visualizer',\n]);\n\n/**\n * 聚合裁决器:硬编码目录兜底 + .gitignore 规则(并集)。\n * 返回给 FileSystemAdapter.scanDir 的统一判定入口。\n */\nexport class GitignoreFilter {\n private rules: readonly GitignoreRule[];\n private hasNegationRules: boolean;\n /** 硬编码兜底是否生效(.gitignore 在场时不减——两者并集)。 */\n private defaultDirs: ReadonlySet<string>;\n\n constructor(rules: readonly GitignoreRule[], defaultDirs: ReadonlySet<string> = DEFAULT_IGNORED_DIRS) {\n this.rules = rules;\n this.hasNegationRules = rules.some((r) => r.negated);\n this.defaultDirs = defaultDirs;\n }\n\n /** 规则数(管理面板「索引覆盖范围」的白盒数据)。 */\n get ruleCount(): number {\n return this.rules.length;\n }\n\n /**\n * 是否含取反规则(scanDir 的剪枝策略开关)。\n *\n * 取反在场时目录剪枝必须降级为「不生成节点但仍递归」——否则\n * `!dir/white-listed.ext` 永远救不回被排除目录里的白名单文件\n *(目录级剪枝发生在遍历之前,白名单文件根本不会被看到)。\n */\n get hasNegation(): boolean {\n return this.hasNegationRules;\n }\n\n /** 目录名是否被硬编码兜底排除(scanDir 剪枝用,O(1))。 */\n isDefaultIgnoredDir(name: string): boolean {\n return this.defaultDirs.has(name);\n }\n\n /** 统一裁决:相对路径(/ 分隔)+ 是否目录。 */\n isIgnored(relPath: string, isDir: boolean): boolean {\n if (this.rules.length === 0) return false;\n return isIgnoredByRules(this.rules, relPath, isDir);\n }\n}\n\n/** 便捷构造:从文件系统读 .gitignore 并连同兜底表一起编译。读失败 = 纯兜底。 */\nexport function createGitignoreFilter(\n readFile: (path: string) => string | undefined,\n repoRoot: string,\n): { filter: GitignoreFilter; gitignorePresent: boolean } {\n let rules: GitignoreRule[] = [];\n let present = false;\n try {\n const text = readFile(`${repoRoot}/.gitignore`);\n if (typeof text === 'string') {\n rules = parseGitignore(text);\n present = true;\n }\n } catch (e) {\n log.debug('gitignore read failed — default dirs only', { repoRoot, error: String(e) });\n }\n return { filter: new GitignoreFilter(rules), gitignorePresent: present };\n}\n","import { scoped } from '../shared/Logger.ts';\r\nimport type { GraphNode, GraphEdge, AdapterResult } from '../types/index.ts';\r\nimport { NodeId, EdgeId } from '../types/index.ts';\r\nimport type { IDataAdapter, UpstreamInvoker } from './IAdapter.ts';\r\nimport type { TreeSitterAdapter } from './TreeSitterAdapter.ts';\r\nimport { parseTsconfigPaths, resolveAliasModule, resolveCallEdges, type TsconfigPathsMapping } from './treeSitterExtractors.ts';\r\nimport { FILE_SYSTEM } from '../shared/constants.ts';\r\nimport { createGitignoreFilter, DEFAULT_IGNORED_DIRS } from './gitignoreFilter.ts';\r\n\r\nconst log = scoped('fs-adapter');\r\n\r\nconst SUPPORTED_EXTENSIONS = new Set([\r\n '.ts', '.tsx', '.js', '.jsx', '.mjs',\r\n '.py', '.java', '.rs', '.go',\r\n '.json', '.toml', '.yaml', '.yml',\r\n]);\r\n\r\n// 硬编码兜底目录表下沉到 gitignoreFilter.ts(单一真源 DEFAULT_IGNORED_DIRS)——\r\n// cycle-141 第七轮:.gitignore 在场时与硬编码表并集裁决(见 GitignoreFilter 头注)。\r\nconst IGNORED_DIRS = DEFAULT_IGNORED_DIRS;\r\n\r\n\r\nfunction getLanguageFromExt(ext: string): string {\r\n const langMap: Record<string, string> = {\r\n '.ts': 'typescript',\r\n '.tsx': 'typescript',\r\n '.js': 'javascript',\r\n '.jsx': 'javascript',\r\n '.mjs': 'javascript',\r\n '.py': 'python',\r\n '.java': 'java',\r\n '.rs': 'rust',\r\n '.go': 'go',\r\n };\r\n return langMap[ext] ?? 'unknown';\r\n}\r\n\r\nfunction getNodeTypeFromExt(ext: string): GraphNode['type'] {\r\n if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx' || ext === '.mjs') return 'module';\r\n if (ext === '.py' || ext === '.java' || ext === '.rs' || ext === '.go') return 'module';\r\n return 'variable';\r\n}\r\n\r\nexport interface FileSystemApi {\r\n readdir(path: string): string[];\r\n readFile(path: string): string;\r\n stat(path: string): { isDirectory: boolean };\r\n}\r\n\r\nexport class FileSystemAdapter implements IDataAdapter {\r\n readonly source = 'tree-sitter' as const;\r\n private fs: FileSystemApi | null = null;\r\n private treeSitterAdapter: TreeSitterAdapter | null = null;\r\n\r\n setFileSystem(fs: FileSystemApi): void {\r\n this.fs = fs;\r\n }\r\n\r\n setTreeSitterAdapter(adapter: TreeSitterAdapter): void {\r\n this.treeSitterAdapter = adapter;\r\n }\r\n\r\n async fetchData(repoId: string, _invoke?: UpstreamInvoker): Promise<AdapterResult> {\r\n if (!this.fs) {\r\n log.warn('no filesystem available');\r\n return { nodes: [], edges: [], source: this.source, timestamp: Date.now() };\r\n }\r\n\r\n const nodes: GraphNode[] = [];\r\n const edges: GraphEdge[] = [];\r\n const visitedDirs = new Set<string>();\r\n let fileCount = 0;\r\n let ignoredCount = 0;\r\n\r\n // .gitignore 过滤(cycle-141 第七轮):仓库根的 .gitignore + 硬编码目录表\r\n // 并集裁决(GitignoreFilter 头注)。读失败 = 纯兜底(gitignore 缺席是常态)。\r\n const { filter: gitignore, gitignorePresent } = createGitignoreFilter(\r\n (p) => this.fs!.readFile(p),\r\n repoId,\r\n );\r\n // repoId 前缀长度:把绝对路径归一成「相对仓库根、/ 分隔」的裁决域。\r\n const repoPrefix = repoId.endsWith('/') ? repoId.length : repoId.length + 1;\r\n const relPathOf = (absPath: string): string => absPath.slice(repoPrefix).replace(/\\\\/g, '/');\r\n\r\n const scanDir = (dirPath: string, parentId?: string): void => {\r\n if (fileCount >= FILE_SYSTEM.MAX_FILES) return;\r\n if (visitedDirs.has(dirPath)) return;\r\n visitedDirs.add(dirPath);\r\n\r\n let entries: string[];\r\n try {\r\n entries = this.fs!.readdir(dirPath);\r\n } catch {\r\n return;\r\n }\r\n\r\n for (const entry of entries) {\r\n if (fileCount >= FILE_SYSTEM.MAX_FILES) break;\r\n const fullPath = dirPath.endsWith('/') ? dirPath + entry : `${dirPath}/${entry}`;\r\n const baseName = entry.split('/').pop() ?? entry;\r\n\r\n if (IGNORED_DIRS.has(baseName)) { ignoredCount++; continue; }\r\n\r\n let isDir: boolean;\r\n try {\r\n isDir = this.fs!.stat(fullPath).isDirectory;\r\n } catch {\r\n continue;\r\n }\r\n\r\n // .gitignore 裁决。目录在递归前剪枝(整棵子树不进图,性能主路径)——\r\n // **例外**:.gitignore 含取反规则时降级为「不生成节点但仍递归」,\r\n // 让 `!.cycles/config.yml` 这类白名单能救回目录内的文件(git 语义对齐;\r\n // 取反规则数通常 ≤ 几条,降级路径开销可控)。文件被排除只 continue,\r\n // 不存在子树问题。\r\n const rel = relPathOf(fullPath);\r\n const ignored = gitignore.isIgnored(rel, isDir);\r\n if (ignored && (!isDir || !gitignore.hasNegation)) { ignoredCount++; continue; }\r\n if (ignored && isDir) {\r\n // 目录被排除但有取反规则:跳过目录节点本体,仍递归其内容逐个裁决。\r\n ignoredCount++;\r\n scanDir(fullPath);\r\n continue;\r\n }\r\n\r\n if (isDir) {\r\n const dirNodeId = `dir:${fullPath}`;\r\n nodes.push({\r\n id: NodeId(dirNodeId),\r\n label: baseName,\r\n type: 'module',\r\n filePath: fullPath,\r\n properties: { language: 'directory' },\r\n });\r\n if (parentId) {\r\n edges.push({\r\n id: EdgeId(`contains:${parentId}:${dirNodeId}`),\r\n source: NodeId(parentId),\r\n target: NodeId(dirNodeId),\r\n type: 'contains',\r\n properties: {},\r\n });\r\n }\r\n scanDir(fullPath, dirNodeId);\r\n } else {\r\n const ext = '.' + (baseName.split('.').pop() ?? '');\r\n if (!SUPPORTED_EXTENSIONS.has(ext)) continue;\r\n\r\n const fileNodeId = `file:${fullPath}`;\r\n const lang = getLanguageFromExt(ext);\r\n const nodeType = getNodeTypeFromExt(ext);\r\n\r\n nodes.push({\r\n id: NodeId(fileNodeId),\r\n label: baseName,\r\n type: nodeType,\r\n filePath: fullPath,\r\n properties: { language: lang, extension: ext },\r\n });\r\n fileCount++;\r\n\r\n if (parentId) {\r\n edges.push({\r\n id: EdgeId(`contains:${parentId}:${fileNodeId}`),\r\n source: NodeId(parentId),\r\n target: NodeId(fileNodeId),\r\n type: 'contains',\r\n properties: {},\r\n });\r\n }\r\n }\r\n }\r\n };\r\n\r\n scanDir(repoId);\r\n\r\n // 白盒可观测:gitignore 是否在场/规则数/排除数——管理面板「索引覆盖范围」的数据面,\r\n // 也是「图谱为什么少了某目录」的唯一可诊断入口(排除是不可见操作,没有这个数字\r\n // 「被 gitignore 屏蔽」和「扫描 bug」长得一样)。scanCoverage 随 AdapterResult\r\n // 上行,tools.ts 回写索引时沉淀进 index_meta(readNativeIndexCoverage 读侧)。\r\n log.info(`scanned ${fileCount} files, ${nodes.length} nodes, ${edges.length} edges, gitignore ${gitignorePresent ? `${gitignore.ruleCount} rules` : 'absent'}, ignored ${ignoredCount}`);\r\n\r\n return {\r\n nodes,\r\n edges,\r\n source: this.source,\r\n timestamp: Date.now(),\r\n scanCoverage: {\r\n gitignorePresent,\r\n gitignoreRules: gitignore.ruleCount,\r\n ignoredEntries: ignoredCount,\r\n },\r\n } as AdapterResult & { scanCoverage: { gitignorePresent: boolean; gitignoreRules: number; ignoredEntries: number } };\r\n }\r\n\r\n async fetchDataWithSymbols(repoId: string, _invoke?: UpstreamInvoker): Promise<AdapterResult> {\r\n if (!this.fs) {\r\n log.warn('no filesystem available');\r\n return { nodes: [], edges: [], source: this.source, timestamp: Date.now() };\r\n }\r\n\r\n const structureResult = await this.fetchData(repoId, _invoke);\r\n if (!this.treeSitterAdapter) {\r\n log.info('no tree-sitter adapter attached, returning structure-only result');\r\n return structureResult;\r\n }\r\n\r\n const filesToParse: Array<{ filePath: string; content: string }> = [];\r\n const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.py', '.java', '.rs', '.go']);\r\n\r\n for (const node of structureResult.nodes) {\r\n if (node.type !== 'module' && node.type !== 'variable') continue;\r\n const ext = '.' + (node.filePath?.split('.').pop() ?? '');\r\n if (!CODE_EXTENSIONS.has(ext)) continue;\r\n try {\r\n const content = this.fs.readFile(node.filePath);\r\n filesToParse.push({ filePath: node.filePath, content });\r\n } catch {\r\n log.debug(`could not read file: ${node.filePath}`);\r\n }\r\n }\r\n\r\n if (filesToParse.length === 0) {\r\n return structureResult;\r\n }\r\n\r\n log.info(`parsing ${filesToParse.length} files with tree-sitter`);\r\n const symbolResult = await this.treeSitterAdapter.batchScan(filesToParse);\r\n\r\n const allNodes = [...structureResult.nodes, ...symbolResult.nodes];\r\n const allEdges = [...structureResult.edges, ...symbolResult.edges];\r\n\r\n for (const symbolNode of symbolResult.nodes) {\r\n const fileNodeId = `file:${symbolNode.filePath}`;\r\n allEdges.push({\r\n id: EdgeId(`symcontains:${fileNodeId}:${symbolNode.id}`),\r\n source: NodeId(fileNodeId),\r\n target: symbolNode.id,\r\n type: 'contains',\r\n properties: {},\r\n });\r\n }\r\n\r\n // ── import 边推断(cycle-129 · T2:原生依赖图缺口)────────────────────\r\n //\r\n // TreeSitter 只产出「文件 → 模块名」(语法事实,batchScan.importsByFile);\r\n // 把模块名解析到 workspace 内真实文件节点是**语义裁决**,需要目录清单——\r\n // 这正是本适配器已有的 structureResult.nodes(file: 前缀节点)。\r\n //\r\n // 路径域(调试实证):filesToParse 用**绝对路径**(fetchData 从 repoId 起\r\n // 遍历、readFile 按绝对键),因此 TreeSitter 的 importsByFile 键域与\r\n // fileSet 同为绝对路径——import 解析统一在绝对域进行,无需前缀换算\r\n // (曾误判为「结构绝对/符号相对」两域而做前缀剥离,解析永远落空)。\r\n //\r\n // 解析策略(保守,宁缺勿造):\r\n // 1. 相对路径(./ / ../)按 import 所在文件目录 + 扩展名/index 枚举解析;\r\n // 2. bare specifier:模块名 + 扩展名直接匹配 workspace 内路径;\r\n // 3. `lodash`/`react` 等包名自然落空——解析不到即跳过,绝不编造目标\r\n // (缺口:tsconfig paths / node_modules 内部图——登记规划 §2 边界)。\r\n // ── tsconfig paths 别名表(cycle-130 · T1)────────────────────────────\r\n // 读 workspace 根的 tsconfig.json(缺席/解析失败 = 无别名表,alias 解析跳过,\r\n // 相对/bare 逻辑不受影响)。一次扫描读一次即可(不缓存——batchScan 频率低)。\r\n let aliasMapping: TsconfigPathsMapping | null = null;\r\n try {\r\n const tsconfigText = this.fs!.readFile(`${repoId}/tsconfig.json`);\r\n aliasMapping = parseTsconfigPaths(tsconfigText);\r\n if (aliasMapping) {\r\n log.info(`tsconfig paths loaded: ${aliasMapping.entries.length} alias patterns`);\r\n }\r\n } catch {\r\n // 无 tsconfig / 读取失败:诚实降级(别名解析跳过)。\r\n }\r\n\r\n const fileSet = new Set(\r\n structureResult.nodes.filter((n) => String(n.id).startsWith('file:')).map((n) => String(n.filePath)),\r\n );\r\n const EXT_CANDIDATES = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.py', '.rs', '.go', '.java', '/index.ts', '/index.js'];\r\n // fileSet 是绝对域(`D:/proj/src/a.ts`);别名解析产出 repo 相对路径——\r\n // 绝对化时用 repoId 前缀(与 fileSet 同域后匹配)。\r\n const repoIdPrefix = repoId.endsWith('/') ? repoId : `${repoId}/`;\r\n /** 候选解析:先试原样(模块名已带扩展名——真实项目普遍形态,cycle-131 真机\r\n * 发现:`./closureModel.ts` 拼接扩展名变 `.ts.ts` 全落空),再走扩展名枚举。 */\r\n const tryCandidates = (joined: string): string | null => {\r\n if (fileSet.has(joined)) return joined;\r\n for (const ext of EXT_CANDIDATES) {\r\n const candidate = `${joined}${ext}`;\r\n if (fileSet.has(candidate)) return candidate;\r\n }\r\n return null;\r\n };\r\n const resolveModuleToFile = (fromFile: string, moduleName: string): string | null => {\r\n // cycle-130 · T1:别名优先(@/utils/helper → src/utils/helper → 绝对域匹配)。\r\n if (aliasMapping) {\r\n const aliasRel = resolveAliasModule(aliasMapping, moduleName);\r\n if (aliasRel) {\r\n const absCandidate = tryCandidates(`${repoIdPrefix}${aliasRel}`);\r\n return absCandidate;\r\n // 别名命中但文件不在清单内(目录上限截断)——不落 bare(宁缺勿造)。\r\n }\r\n }\r\n if (moduleName.startsWith('./') || moduleName.startsWith('../')) {\r\n const base = fromFile.split('/').slice(0, -1).join('/');\r\n let joined: string;\r\n if (moduleName.startsWith('./')) {\r\n joined = `${base}/${moduleName.slice(2)}`;\r\n } else {\r\n let parts = base.split('/');\r\n let rest = moduleName;\r\n while (rest.startsWith('../')) {\r\n parts = parts.slice(0, -1);\r\n rest = rest.slice(3);\r\n }\r\n joined = `${parts.join('/')}/${rest}`;\r\n }\r\n return tryCandidates(joined);\r\n }\r\n return tryCandidates(moduleName);\r\n };\r\n\r\n let importEdgeCount = 0;\r\n /**\r\n * 源文件(绝对路径)→ 被 import 到的目标文件(绝对路径)集合。\r\n * call 边的第二跳(inferred)**只允许**沿这条已解析出的 import 关系走:\r\n * import 解析不到的文件不在图里,也就不该成为 call 边端点(宁缺勿造)。\r\n */\r\n const importedFilesByFile = new Map<string, Set<string>>();\r\n for (const [fromFile, modules] of symbolResult.importsByFile ?? []) {\r\n const fromFileId = `file:${fromFile}`;\r\n for (const moduleName of modules) {\r\n const resolved = resolveModuleToFile(fromFile, moduleName);\r\n if (!resolved) continue;\r\n const targetId = `file:${resolved}`;\r\n if (targetId === fromFileId) continue; // 自引用跳过\r\n allEdges.push({\r\n id: EdgeId(`tsimport:${fromFileId}:${targetId}`),\r\n source: NodeId(fromFileId),\r\n target: NodeId(targetId),\r\n type: 'import',\r\n properties: { edgeOrigin: 'ast', module: moduleName },\r\n });\r\n importEdgeCount++;\r\n const bucket = importedFilesByFile.get(fromFile);\r\n if (bucket) bucket.add(resolved);\r\n else importedFilesByFile.set(fromFile, new Set([resolved]));\r\n }\r\n }\r\n\r\n // ── call 边提取与证据分级(T2:原生通道行为视图的核心缺口)─────────────\r\n //\r\n // 为什么要在这里做而不是在 TreeSitter 里:名字 → 符号的裁决需要 workspace\r\n // 上下文(本文件符号集 + 已解析出的 import 图),那正是本适配器的职能;\r\n // 语法事实(谁在第几行调用了什么名字)已由 callSitesByFile 交出。\r\n //\r\n // 两跳(详见 treeSitterExtractors.resolveCallEdges 的头注):\r\n // exact 同文件唯一命中;\r\n // inferred 跨文件(沿 import 图)唯一命中;\r\n // 同文件多义 / 两跳皆空 ⇒ 跳过(不猜)。统计进日志,便于观测「有多少调用点\r\n // 因为歧义被放弃」——没有这个数字,「0 条 call 边」和「全被跳过」长得一样。\r\n const symbolsByFile = new Map<string, Array<{ label: string; id: string }>>();\r\n for (const symbolNode of symbolResult.nodes) {\r\n const bucket = symbolsByFile.get(symbolNode.filePath);\r\n const entry = { label: symbolNode.label, id: String(symbolNode.id) };\r\n if (bucket) bucket.push(entry);\r\n else symbolsByFile.set(symbolNode.filePath, [entry]);\r\n }\r\n\r\n let callEdgeCount = 0;\r\n let callAmbiguous = 0;\r\n let callUnresolved = 0;\r\n let callExact = 0;\r\n let callInferred = 0;\r\n for (const [fromFile, callSites] of symbolResult.callSitesByFile ?? []) {\r\n const importedFileSymbols = new Map<string, Array<{ label: string; id: string }>>();\r\n for (const importedFile of importedFilesByFile.get(fromFile) ?? []) {\r\n const symbols = symbolsByFile.get(importedFile);\r\n if (symbols) importedFileSymbols.set(importedFile, symbols);\r\n }\r\n const resolved = resolveCallEdges({\r\n callSites,\r\n localSymbols: symbolsByFile.get(fromFile) ?? [],\r\n importedFileSymbols,\r\n });\r\n for (const edge of resolved.edges) {\r\n allEdges.push({\r\n id: EdgeId(edge.id),\r\n source: NodeId(edge.source),\r\n target: NodeId(edge.target),\r\n type: 'call',\r\n properties: { ...edge.properties },\r\n });\r\n }\r\n callEdgeCount += resolved.edges.length;\r\n callAmbiguous += resolved.ambiguous;\r\n callUnresolved += resolved.unresolved;\r\n callExact += resolved.byEvidence.exact;\r\n callInferred += resolved.byEvidence.inferred;\r\n }\r\n\r\n log.info(`fetchDataWithSymbols: ${allNodes.length} nodes (${structureResult.nodes.length} structure + ${symbolResult.nodes.length} symbols), ${allEdges.length} edges (imports ${importEdgeCount}, calls ${callEdgeCount} = exact ${callExact} + inferred ${callInferred}; ambiguous ${callAmbiguous} skipped, unresolved ${callUnresolved} skipped)`);\r\n\r\n return {\r\n nodes: allNodes,\r\n edges: allEdges,\r\n source: this.source,\r\n timestamp: Date.now(),\r\n };\r\n }\r\n}"],"mappings":"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"}