@mgamil/mapx 0.2.4
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/LICENSE +194 -0
- package/README.md +488 -0
- package/VERSION +1 -0
- package/dist/agents/generator.d.ts +74 -0
- package/dist/agents/generator.js +375 -0
- package/dist/agents/templates.d.ts +29 -0
- package/dist/agents/templates.js +459 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +1835 -0
- package/dist/core/cluster-engine.d.ts +32 -0
- package/dist/core/cluster-engine.js +314 -0
- package/dist/core/config.d.ts +29 -0
- package/dist/core/config.js +178 -0
- package/dist/core/context-builder.d.ts +61 -0
- package/dist/core/context-builder.js +252 -0
- package/dist/core/flow-tracer.d.ts +63 -0
- package/dist/core/flow-tracer.js +366 -0
- package/dist/core/git-tracker.d.ts +20 -0
- package/dist/core/git-tracker.js +159 -0
- package/dist/core/graph.d.ts +42 -0
- package/dist/core/graph.js +186 -0
- package/dist/core/metrics.d.ts +24 -0
- package/dist/core/metrics.js +87 -0
- package/dist/core/scanner.d.ts +53 -0
- package/dist/core/scanner.js +949 -0
- package/dist/core/store-bun.d.ts +13 -0
- package/dist/core/store-bun.js +34 -0
- package/dist/core/store-interface.d.ts +15 -0
- package/dist/core/store-interface.js +7 -0
- package/dist/core/store-node.d.ts +13 -0
- package/dist/core/store-node.js +35 -0
- package/dist/core/store.d.ts +132 -0
- package/dist/core/store.js +614 -0
- package/dist/core/workspace-manager.d.ts +9 -0
- package/dist/core/workspace-manager.js +64 -0
- package/dist/exporters/dot-exporter.d.ts +16 -0
- package/dist/exporters/dot-exporter.js +179 -0
- package/dist/exporters/graph-exporter.d.ts +14 -0
- package/dist/exporters/graph-exporter.js +85 -0
- package/dist/exporters/index.d.ts +9 -0
- package/dist/exporters/index.js +12 -0
- package/dist/exporters/llm-exporter.d.ts +18 -0
- package/dist/exporters/llm-exporter.js +224 -0
- package/dist/exporters/svg-exporter.d.ts +19 -0
- package/dist/exporters/svg-exporter.js +319 -0
- package/dist/exporters/toon-exporter.d.ts +16 -0
- package/dist/exporters/toon-exporter.js +246 -0
- package/dist/frameworks/detectors/aspnet.d.ts +11 -0
- package/dist/frameworks/detectors/aspnet.js +52 -0
- package/dist/frameworks/detectors/django.d.ts +14 -0
- package/dist/frameworks/detectors/django.js +135 -0
- package/dist/frameworks/detectors/drupal.d.ts +13 -0
- package/dist/frameworks/detectors/drupal.js +94 -0
- package/dist/frameworks/detectors/express.d.ts +12 -0
- package/dist/frameworks/detectors/express.js +234 -0
- package/dist/frameworks/detectors/fastapi.d.ts +12 -0
- package/dist/frameworks/detectors/fastapi.js +203 -0
- package/dist/frameworks/detectors/flask.d.ts +12 -0
- package/dist/frameworks/detectors/flask.js +244 -0
- package/dist/frameworks/detectors/go.d.ts +11 -0
- package/dist/frameworks/detectors/go.js +75 -0
- package/dist/frameworks/detectors/laravel.d.ts +11 -0
- package/dist/frameworks/detectors/laravel.js +462 -0
- package/dist/frameworks/detectors/nestjs.d.ts +12 -0
- package/dist/frameworks/detectors/nestjs.js +155 -0
- package/dist/frameworks/detectors/nextjs.d.ts +11 -0
- package/dist/frameworks/detectors/nextjs.js +118 -0
- package/dist/frameworks/detectors/rails.d.ts +12 -0
- package/dist/frameworks/detectors/rails.js +76 -0
- package/dist/frameworks/detectors/react-router.d.ts +11 -0
- package/dist/frameworks/detectors/react-router.js +115 -0
- package/dist/frameworks/detectors/rust.d.ts +11 -0
- package/dist/frameworks/detectors/rust.js +59 -0
- package/dist/frameworks/detectors/spring.d.ts +11 -0
- package/dist/frameworks/detectors/spring.js +56 -0
- package/dist/frameworks/detectors/sveltekit.d.ts +11 -0
- package/dist/frameworks/detectors/sveltekit.js +154 -0
- package/dist/frameworks/detectors/symfony.d.ts +13 -0
- package/dist/frameworks/detectors/symfony.js +175 -0
- package/dist/frameworks/detectors/tanstack-router.d.ts +12 -0
- package/dist/frameworks/detectors/tanstack-router.js +80 -0
- package/dist/frameworks/detectors/vapor.d.ts +11 -0
- package/dist/frameworks/detectors/vapor.js +52 -0
- package/dist/frameworks/detectors/vue-router.d.ts +12 -0
- package/dist/frameworks/detectors/vue-router.js +237 -0
- package/dist/frameworks/detectors/wordpress.d.ts +13 -0
- package/dist/frameworks/detectors/wordpress.js +141 -0
- package/dist/frameworks/detectors/yii.d.ts +11 -0
- package/dist/frameworks/detectors/yii.js +131 -0
- package/dist/frameworks/framework-registry.d.ts +13 -0
- package/dist/frameworks/framework-registry.js +77 -0
- package/dist/frameworks/route-registry.d.ts +26 -0
- package/dist/frameworks/route-registry.js +102 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +30 -0
- package/dist/languages/index.d.ts +2 -0
- package/dist/languages/index.js +7 -0
- package/dist/languages/installer.d.ts +13 -0
- package/dist/languages/installer.js +103 -0
- package/dist/languages/registry.d.ts +19 -0
- package/dist/languages/registry.js +427 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +20 -0
- package/dist/mcp.d.ts +11 -0
- package/dist/mcp.js +1699 -0
- package/dist/parsers/common-methods.d.ts +3 -0
- package/dist/parsers/common-methods.js +33 -0
- package/dist/parsers/fallback-parser.d.ts +10 -0
- package/dist/parsers/fallback-parser.js +18 -0
- package/dist/parsers/generic-wasm-parser.d.ts +23 -0
- package/dist/parsers/generic-wasm-parser.js +168 -0
- package/dist/parsers/ignored-symbols.d.ts +26 -0
- package/dist/parsers/ignored-symbols.js +77 -0
- package/dist/parsers/index.d.ts +9 -0
- package/dist/parsers/index.js +13 -0
- package/dist/parsers/languages/javascript.d.ts +11 -0
- package/dist/parsers/languages/javascript.js +28 -0
- package/dist/parsers/languages/php.d.ts +15 -0
- package/dist/parsers/languages/php.js +648 -0
- package/dist/parsers/languages/typescript.d.ts +10 -0
- package/dist/parsers/languages/typescript.js +9 -0
- package/dist/parsers/languages/vue.d.ts +13 -0
- package/dist/parsers/languages/vue.js +63 -0
- package/dist/parsers/parse-worker.d.ts +2 -0
- package/dist/parsers/parse-worker.js +185 -0
- package/dist/parsers/parser-interface.d.ts +9 -0
- package/dist/parsers/parser-interface.js +0 -0
- package/dist/parsers/parser-registry.d.ts +8 -0
- package/dist/parsers/parser-registry.js +52 -0
- package/dist/parsers/wasm-parser.d.ts +16 -0
- package/dist/parsers/wasm-parser.js +110 -0
- package/dist/types.d.ts +172 -0
- package/dist/types.js +0 -0
- package/dist/ui/index.html +270 -0
- package/dist/ui/main.js +581 -0
- package/dist/ui/main.js.map +7 -0
- package/dist/ui/styles.css +573 -0
- package/dist/ui-events.d.ts +36 -0
- package/dist/ui-events.js +61 -0
- package/dist/ui-server.d.ts +12 -0
- package/dist/ui-server.js +504 -0
- package/package.json +179 -0
- package/queries/bash/references.scm +22 -0
- package/queries/bash/symbols.scm +15 -0
- package/queries/c/references.scm +14 -0
- package/queries/c/symbols.scm +30 -0
- package/queries/c-sharp/references.scm +26 -0
- package/queries/c-sharp/symbols.scm +57 -0
- package/queries/cpp/references.scm +21 -0
- package/queries/cpp/symbols.scm +44 -0
- package/queries/dart/references.scm +33 -0
- package/queries/dart/symbols.scm +38 -0
- package/queries/elixir/references.scm +45 -0
- package/queries/elixir/symbols.scm +41 -0
- package/queries/go/references.scm +22 -0
- package/queries/go/symbols.scm +53 -0
- package/queries/java/references.scm +32 -0
- package/queries/java/symbols.scm +41 -0
- package/queries/javascript/references.scm +14 -0
- package/queries/javascript/symbols.scm +23 -0
- package/queries/kotlin/references.scm +31 -0
- package/queries/kotlin/symbols.scm +24 -0
- package/queries/lua/references.scm +19 -0
- package/queries/lua/symbols.scm +29 -0
- package/queries/pascal/references.scm +29 -0
- package/queries/pascal/symbols.scm +45 -0
- package/queries/php/references.scm +109 -0
- package/queries/php/symbols.scm +33 -0
- package/queries/python/references.scm +50 -0
- package/queries/python/symbols.scm +21 -0
- package/queries/ruby/references.scm +48 -0
- package/queries/ruby/symbols.scm +24 -0
- package/queries/rust/references.scm +31 -0
- package/queries/rust/symbols.scm +35 -0
- package/queries/scala/references.scm +30 -0
- package/queries/scala/symbols.scm +35 -0
- package/queries/svelte/references.scm +20 -0
- package/queries/svelte/symbols.scm +30 -0
- package/queries/swift/references.scm +22 -0
- package/queries/swift/symbols.scm +37 -0
- package/queries/typescript/references.scm +25 -0
- package/queries/typescript/symbols.scm +35 -0
- package/queries/vue/references.scm +20 -0
- package/queries/vue/symbols.scm +28 -0
- package/queries/zig/references.scm +20 -0
- package/queries/zig/symbols.scm +22 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
import { readFile, writeFile, unlink, stat, readdir, open } from "node:fs/promises";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { resolve, relative, join } from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { cpus } from "node:os";
|
|
6
|
+
import { ClusterEngine } from "./cluster-engine.js";
|
|
7
|
+
import { getParserForFile } from "../parsers/parser-registry.js";
|
|
8
|
+
import { getLanguageForFile } from "../languages/registry.js";
|
|
9
|
+
import { getGitBlobHashes, getChangedFiles, getCurrentCommitSha, isGitRepo } from "./git-tracker.js";
|
|
10
|
+
import { minimatch } from "minimatch";
|
|
11
|
+
import { FrameworkRegistry } from "../frameworks/framework-registry.js";
|
|
12
|
+
import { RouteRegistry } from "../frameworks/route-registry.js";
|
|
13
|
+
import { buildIgnoredSymbols } from "../parsers/ignored-symbols.js";
|
|
14
|
+
const DEFAULT_CONCURRENCY = Math.min(cpus().length || 4, 8);
|
|
15
|
+
const DEFAULT_IGNORE = /* @__PURE__ */ new Set([
|
|
16
|
+
"node_modules",
|
|
17
|
+
"vendor",
|
|
18
|
+
".git",
|
|
19
|
+
"dist",
|
|
20
|
+
".mapx",
|
|
21
|
+
"__pycache__",
|
|
22
|
+
".next",
|
|
23
|
+
".nuxt",
|
|
24
|
+
"coverage",
|
|
25
|
+
".cache",
|
|
26
|
+
".turbo",
|
|
27
|
+
"target",
|
|
28
|
+
"build",
|
|
29
|
+
".gradle",
|
|
30
|
+
".idea",
|
|
31
|
+
".vscode",
|
|
32
|
+
".vs"
|
|
33
|
+
]);
|
|
34
|
+
function buildMatcher(excludes, includes) {
|
|
35
|
+
return (rel) => {
|
|
36
|
+
if (excludes.some((p) => {
|
|
37
|
+
if (minimatch(rel, p, { dot: true })) return true;
|
|
38
|
+
const segments = rel.split("/");
|
|
39
|
+
if (segments.some((seg) => seg === p)) return true;
|
|
40
|
+
return false;
|
|
41
|
+
})) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (includes.length > 0) {
|
|
45
|
+
const matched = includes.some((p) => {
|
|
46
|
+
if (minimatch(rel, p, { dot: true })) return true;
|
|
47
|
+
const segments = rel.split("/");
|
|
48
|
+
if (segments.some((seg) => seg === p)) return true;
|
|
49
|
+
return false;
|
|
50
|
+
});
|
|
51
|
+
if (!matched) return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
class Scanner {
|
|
57
|
+
store;
|
|
58
|
+
config;
|
|
59
|
+
graph;
|
|
60
|
+
onProgress;
|
|
61
|
+
concurrency;
|
|
62
|
+
aborted = false;
|
|
63
|
+
cliExcludes = [];
|
|
64
|
+
cliIncludes = [];
|
|
65
|
+
workspaceFileMap = /* @__PURE__ */ new Map();
|
|
66
|
+
constructor(store, config, graph, onProgress, options) {
|
|
67
|
+
this.store = store;
|
|
68
|
+
this.config = config;
|
|
69
|
+
this.graph = graph;
|
|
70
|
+
this.onProgress = onProgress;
|
|
71
|
+
this.concurrency = DEFAULT_CONCURRENCY;
|
|
72
|
+
this.cliExcludes = options?.excludes ?? [];
|
|
73
|
+
this.cliIncludes = options?.includes ?? [];
|
|
74
|
+
}
|
|
75
|
+
abort() {
|
|
76
|
+
this.aborted = true;
|
|
77
|
+
}
|
|
78
|
+
loadResumeState(repoName) {
|
|
79
|
+
const data = this.store.getMeta(`scan_resume_state:${repoName}`);
|
|
80
|
+
if (!data) return null;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(data);
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
saveResumeState(repoName, state) {
|
|
88
|
+
this.store.setMeta(`scan_resume_state:${repoName}`, JSON.stringify(state));
|
|
89
|
+
}
|
|
90
|
+
clearResumeState(repoName) {
|
|
91
|
+
this.store.setMeta(`scan_resume_state:${repoName}`, "");
|
|
92
|
+
}
|
|
93
|
+
getLockPath() {
|
|
94
|
+
return join(this.config.getWorkspaceRoot(), ".mapx", "scan.lock");
|
|
95
|
+
}
|
|
96
|
+
async acquireScanLock() {
|
|
97
|
+
const lockPath = this.getLockPath();
|
|
98
|
+
try {
|
|
99
|
+
const fd = await open(lockPath, "wx");
|
|
100
|
+
await fd.write(String(process.pid), 0, "utf-8");
|
|
101
|
+
await fd.close();
|
|
102
|
+
return true;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err.code !== "EEXIST") throw err;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const pid = parseInt(readFileSync(lockPath, "utf-8").trim(), 10);
|
|
108
|
+
if (pid && pid !== process.pid) {
|
|
109
|
+
try {
|
|
110
|
+
process.kill(pid, 0);
|
|
111
|
+
return false;
|
|
112
|
+
} catch {
|
|
113
|
+
try {
|
|
114
|
+
await unlink(lockPath);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
return this.acquireScanLock();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await unlink(lockPath);
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
return this.acquireScanLock();
|
|
127
|
+
}
|
|
128
|
+
async releaseScanLock() {
|
|
129
|
+
try {
|
|
130
|
+
await unlink(this.getLockPath());
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async scanFull(repoNames, options) {
|
|
135
|
+
const acquired = await this.acquireScanLock();
|
|
136
|
+
if (!acquired) {
|
|
137
|
+
const lockPath = this.getLockPath();
|
|
138
|
+
const pid = existsSync(lockPath) ? readFileSync(lockPath, "utf-8").trim() : "?";
|
|
139
|
+
throw new Error(`Another scan is already running on this project (PID ${pid}). Wait for it to finish or delete ${lockPath} if it is stale.`);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const reposToScan = repoNames && repoNames.length > 0 ? repoNames.includes("all") ? this.config.repos.map((r) => r.name) : repoNames : [this.config.repos[0].name];
|
|
143
|
+
this.workspaceFileMap.clear();
|
|
144
|
+
for (const f of this.store.getAllFiles()) {
|
|
145
|
+
this.workspaceFileMap.set(f.path, f.repo);
|
|
146
|
+
}
|
|
147
|
+
for (const repoName of reposToScan) {
|
|
148
|
+
const repo = this.config.repos.find((r) => r.name === repoName);
|
|
149
|
+
if (!repo) continue;
|
|
150
|
+
const workspaceRoot = this.config.getWorkspaceRoot();
|
|
151
|
+
const repoRoot = resolve(workspaceRoot, repo.path);
|
|
152
|
+
const discovered = await this.discoverFiles(repoRoot, repo.name, true);
|
|
153
|
+
for (const f of discovered) {
|
|
154
|
+
this.workspaceFileMap.set(f.relativePath, repo.name);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
let filesScanned = 0;
|
|
158
|
+
let symbolsFound = 0;
|
|
159
|
+
let edgesFound = 0;
|
|
160
|
+
const combinedLangBreakdown = {};
|
|
161
|
+
const startTime = Date.now();
|
|
162
|
+
for (const repoName of reposToScan) {
|
|
163
|
+
const repo = this.config.repos.find((r) => r.name === repoName);
|
|
164
|
+
if (!repo) continue;
|
|
165
|
+
const res = await this._scanFullForRepo(repo, options?.force);
|
|
166
|
+
filesScanned += res.filesScanned;
|
|
167
|
+
symbolsFound += res.symbolsFound;
|
|
168
|
+
edgesFound += res.edgesFound;
|
|
169
|
+
for (const [lang, count] of Object.entries(res.languageBreakdown)) {
|
|
170
|
+
combinedLangBreakdown[lang] = (combinedLangBreakdown[lang] || 0) + count;
|
|
171
|
+
}
|
|
172
|
+
if (this.aborted) break;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
filesScanned,
|
|
176
|
+
symbolsFound,
|
|
177
|
+
edgesFound,
|
|
178
|
+
durationMs: Date.now() - startTime,
|
|
179
|
+
languageBreakdown: combinedLangBreakdown,
|
|
180
|
+
interrupted: this.aborted,
|
|
181
|
+
totalFiles: filesScanned
|
|
182
|
+
};
|
|
183
|
+
} finally {
|
|
184
|
+
await this.releaseScanLock();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async _scanFullForRepo(repo, force = false) {
|
|
188
|
+
const startTime = Date.now();
|
|
189
|
+
this.aborted = false;
|
|
190
|
+
const workspaceRoot = this.config.getWorkspaceRoot();
|
|
191
|
+
const repoRoot = resolve(workspaceRoot, repo.path);
|
|
192
|
+
const discovered = await this.discoverFiles(repoRoot, repo.name);
|
|
193
|
+
this.onProgress?.({ phase: "discover", current: discovered.length, total: discovered.length });
|
|
194
|
+
const filesWithSymbols = /* @__PURE__ */ new Set();
|
|
195
|
+
for (const sym of this.store.getAllSymbols(repo.name)) {
|
|
196
|
+
filesWithSymbols.add(sym.file_path);
|
|
197
|
+
}
|
|
198
|
+
const newFiles = discovered.filter((f) => f.isNew);
|
|
199
|
+
const changedFiles = discovered.filter((f) => !f.isNew && f.contentChanged);
|
|
200
|
+
const incompleteFiles = discovered.filter((f) => !f.isNew && !f.contentChanged && !filesWithSymbols.has(f.relativePath));
|
|
201
|
+
const unchangedFiles = force ? [] : discovered.filter((f) => !f.isNew && !f.contentChanged && filesWithSymbols.has(f.relativePath));
|
|
202
|
+
const filesToParse = force ? discovered : [...newFiles, ...changedFiles, ...incompleteFiles];
|
|
203
|
+
const resumeState = this.loadResumeState(repo.name);
|
|
204
|
+
const resumedCompleted = new Set(resumeState?.completedFiles || []);
|
|
205
|
+
const toParse = resumedCompleted.size > 0 ? filesToParse.filter((f) => !resumedCompleted.has(f.relativePath)) : filesToParse;
|
|
206
|
+
let totalSymbols = resumeState?.totalSymbols || 0;
|
|
207
|
+
let totalEdges = resumeState?.totalEdges || 0;
|
|
208
|
+
for (const f of unchangedFiles) {
|
|
209
|
+
this.graph.addFileNode(f.relativePath, f.language, f.sizeBytes, f.lines);
|
|
210
|
+
}
|
|
211
|
+
if (toParse.length > 0) {
|
|
212
|
+
this.onProgress?.({ phase: "index", current: 0, total: toParse.length });
|
|
213
|
+
const gitHashes = isGitRepo(repoRoot) ? getGitBlobHashes(repoRoot) : /* @__PURE__ */ new Map();
|
|
214
|
+
this.store.inTransaction(() => {
|
|
215
|
+
for (let i = 0; i < toParse.length; i++) {
|
|
216
|
+
const f = toParse[i];
|
|
217
|
+
const blobHash = gitHashes.get(f.relativePath) || null;
|
|
218
|
+
this.store.upsertFile({
|
|
219
|
+
path: f.relativePath,
|
|
220
|
+
repo: repo.name,
|
|
221
|
+
language: f.language,
|
|
222
|
+
gitBlobHash: blobHash,
|
|
223
|
+
contentHash: f.contentHash,
|
|
224
|
+
lastScanned: (/* @__PURE__ */ new Date()).toISOString(),
|
|
225
|
+
sizeBytes: f.sizeBytes,
|
|
226
|
+
lines: f.lines
|
|
227
|
+
});
|
|
228
|
+
this.graph.addFileNode(f.relativePath, f.language, f.sizeBytes, f.lines);
|
|
229
|
+
this.onProgress?.({ phase: "index", current: i + 1, total: toParse.length, file: f.relativePath });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
this.onProgress?.({ phase: "parse", current: unchangedFiles.length + resumedCompleted.size, total: discovered.length });
|
|
233
|
+
const completed = /* @__PURE__ */ new Set([
|
|
234
|
+
...unchangedFiles.map((f) => f.relativePath),
|
|
235
|
+
...resumedCompleted
|
|
236
|
+
]);
|
|
237
|
+
this.saveResumeState(repo.name, {
|
|
238
|
+
totalFiles: discovered.length,
|
|
239
|
+
completedFiles: [...completed],
|
|
240
|
+
totalSymbols,
|
|
241
|
+
totalEdges
|
|
242
|
+
});
|
|
243
|
+
const registry = FrameworkRegistry.getInstance();
|
|
244
|
+
const filePaths = discovered.map((f) => f.relativePath);
|
|
245
|
+
const activeDetectors = await registry.detectActiveFrameworks(repoRoot, filePaths);
|
|
246
|
+
const activeFrameworks = activeDetectors.map((d) => d.name);
|
|
247
|
+
const ignoredSymbols = buildIgnoredSymbols(activeFrameworks);
|
|
248
|
+
const parseResults = await this.parseFilesParallel(toParse, workspaceRoot, ignoredSymbols);
|
|
249
|
+
const fileMap = this.workspaceFileMap.size > 0 ? this.workspaceFileMap : (() => {
|
|
250
|
+
const allTrackedFiles = this.store.getAllFiles();
|
|
251
|
+
const map = /* @__PURE__ */ new Map();
|
|
252
|
+
for (const f of allTrackedFiles) map.set(f.path, f.repo);
|
|
253
|
+
for (const f of toParse) map.set(f.relativePath, repo.name);
|
|
254
|
+
return map;
|
|
255
|
+
})();
|
|
256
|
+
const BATCH_SIZE = 100;
|
|
257
|
+
for (let batchStart = 0; batchStart < toParse.length && !this.aborted; batchStart += BATCH_SIZE) {
|
|
258
|
+
const batchEnd = Math.min(batchStart + BATCH_SIZE, toParse.length);
|
|
259
|
+
this.store.inTransaction(() => {
|
|
260
|
+
for (let i = batchStart; i < batchEnd; i++) {
|
|
261
|
+
this.writeParseResultWithMap(toParse[i].relativePath, parseResults[i], repo.name, fileMap);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
for (let i = batchStart; i < batchEnd; i++) {
|
|
265
|
+
totalSymbols += parseResults[i].symbols.length;
|
|
266
|
+
totalEdges += parseResults[i].references.length;
|
|
267
|
+
completed.add(toParse[i].relativePath);
|
|
268
|
+
}
|
|
269
|
+
this.onProgress?.({
|
|
270
|
+
phase: "parse",
|
|
271
|
+
current: completed.size,
|
|
272
|
+
total: discovered.length,
|
|
273
|
+
file: toParse[batchEnd - 1].relativePath
|
|
274
|
+
});
|
|
275
|
+
this.saveResumeState(repo.name, {
|
|
276
|
+
totalFiles: discovered.length,
|
|
277
|
+
completedFiles: [...completed],
|
|
278
|
+
totalSymbols,
|
|
279
|
+
totalEdges
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
} else if (unchangedFiles.length > 0) {
|
|
283
|
+
this.onProgress?.({ phase: "parse", current: unchangedFiles.length, total: discovered.length });
|
|
284
|
+
}
|
|
285
|
+
const deletedPaths = this.detectDeletedFiles(discovered, repo.name);
|
|
286
|
+
if (deletedPaths.length > 0) {
|
|
287
|
+
this.store.inTransaction(() => {
|
|
288
|
+
for (const p of deletedPaths) {
|
|
289
|
+
this.store.deleteFile(p);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const langBreakdown = {};
|
|
294
|
+
for (const f of discovered) {
|
|
295
|
+
langBreakdown[f.language] = (langBreakdown[f.language] || 0) + 1;
|
|
296
|
+
}
|
|
297
|
+
if (!this.aborted) {
|
|
298
|
+
const commitSha = isGitRepo(repoRoot) ? getCurrentCommitSha(repoRoot) : null;
|
|
299
|
+
if (commitSha) this.store.setMeta("last_scan_commit:" + repo.name, commitSha);
|
|
300
|
+
this.store.setMeta("last_scan_time:" + repo.name, (/* @__PURE__ */ new Date()).toISOString());
|
|
301
|
+
this.clearResumeState(repo.name);
|
|
302
|
+
this.onProgress?.({ phase: "cluster", current: 0, total: 0 });
|
|
303
|
+
const clusterEngine = new ClusterEngine(this.store);
|
|
304
|
+
clusterEngine.detect(repo.name);
|
|
305
|
+
await this.scanFrameworkRoutesAndHooks(repo, repoRoot);
|
|
306
|
+
}
|
|
307
|
+
const totalParsed = unchangedFiles.length + (toParse.length > 0 ? toParse.length : 0);
|
|
308
|
+
return {
|
|
309
|
+
filesScanned: totalParsed,
|
|
310
|
+
symbolsFound: totalSymbols,
|
|
311
|
+
edgesFound: totalEdges,
|
|
312
|
+
durationMs: Date.now() - startTime,
|
|
313
|
+
languageBreakdown: langBreakdown,
|
|
314
|
+
interrupted: this.aborted,
|
|
315
|
+
totalFiles: discovered.length
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async scanIncremental(repoNames) {
|
|
319
|
+
const acquired = await this.acquireScanLock();
|
|
320
|
+
if (!acquired) {
|
|
321
|
+
const lockPath = this.getLockPath();
|
|
322
|
+
const pid = existsSync(lockPath) ? readFileSync(lockPath, "utf-8").trim() : "?";
|
|
323
|
+
throw new Error(`Another scan is already running on this project (PID ${pid}). Wait for it to finish or delete ${lockPath} if it is stale.`);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const reposToScan = repoNames && repoNames.length > 0 ? repoNames.includes("all") ? this.config.repos.map((r) => r.name) : repoNames : [this.config.repos[0].name];
|
|
327
|
+
this.workspaceFileMap.clear();
|
|
328
|
+
for (const f of this.store.getAllFiles()) {
|
|
329
|
+
this.workspaceFileMap.set(f.path, f.repo);
|
|
330
|
+
}
|
|
331
|
+
for (const repoName of reposToScan) {
|
|
332
|
+
const repo = this.config.repos.find((r) => r.name === repoName);
|
|
333
|
+
if (!repo) continue;
|
|
334
|
+
const workspaceRoot = this.config.getWorkspaceRoot();
|
|
335
|
+
const repoRoot = resolve(workspaceRoot, repo.path);
|
|
336
|
+
const discovered = await this.discoverFiles(repoRoot, repo.name, true);
|
|
337
|
+
for (const f of discovered) {
|
|
338
|
+
this.workspaceFileMap.set(f.relativePath, repo.name);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
let filesScanned = 0;
|
|
342
|
+
let symbolsFound = 0;
|
|
343
|
+
let edgesFound = 0;
|
|
344
|
+
const combinedLangBreakdown = {};
|
|
345
|
+
const startTime = Date.now();
|
|
346
|
+
for (const repoName of reposToScan) {
|
|
347
|
+
const repo = this.config.repos.find((r) => r.name === repoName);
|
|
348
|
+
if (!repo) continue;
|
|
349
|
+
const res = await this._scanIncrementalForRepo(repo);
|
|
350
|
+
filesScanned += res.filesScanned;
|
|
351
|
+
symbolsFound += res.symbolsFound;
|
|
352
|
+
edgesFound += res.edgesFound;
|
|
353
|
+
for (const [lang, count] of Object.entries(res.languageBreakdown)) {
|
|
354
|
+
combinedLangBreakdown[lang] = (combinedLangBreakdown[lang] || 0) + count;
|
|
355
|
+
}
|
|
356
|
+
if (this.aborted) break;
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
filesScanned,
|
|
360
|
+
symbolsFound,
|
|
361
|
+
edgesFound,
|
|
362
|
+
durationMs: Date.now() - startTime,
|
|
363
|
+
languageBreakdown: combinedLangBreakdown,
|
|
364
|
+
interrupted: this.aborted,
|
|
365
|
+
totalFiles: filesScanned
|
|
366
|
+
};
|
|
367
|
+
} finally {
|
|
368
|
+
await this.releaseScanLock();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async _scanIncrementalForRepo(repo) {
|
|
372
|
+
const startTime = Date.now();
|
|
373
|
+
this.aborted = false;
|
|
374
|
+
const workspaceRoot = this.config.getWorkspaceRoot();
|
|
375
|
+
const repoRoot = resolve(workspaceRoot, repo.path);
|
|
376
|
+
if (!isGitRepo(repoRoot)) {
|
|
377
|
+
return this._scanFullForRepo(repo);
|
|
378
|
+
}
|
|
379
|
+
const lastCommit = this.store.getMeta("last_scan_commit:" + repo.name);
|
|
380
|
+
this.onProgress?.({ phase: "detect", current: 0, total: 0 });
|
|
381
|
+
const changes = getChangedFiles(repoRoot, lastCommit || void 0);
|
|
382
|
+
if (changes.length === 0) {
|
|
383
|
+
return {
|
|
384
|
+
filesScanned: 0,
|
|
385
|
+
symbolsFound: 0,
|
|
386
|
+
edgesFound: 0,
|
|
387
|
+
durationMs: Date.now() - startTime,
|
|
388
|
+
languageBreakdown: {}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const excludes = [
|
|
392
|
+
...this.config.settings.excludePatterns,
|
|
393
|
+
...this.cliExcludes
|
|
394
|
+
];
|
|
395
|
+
const includes = [
|
|
396
|
+
...this.config.settings.includePatterns,
|
|
397
|
+
...this.cliIncludes
|
|
398
|
+
];
|
|
399
|
+
const matcher = buildMatcher(excludes, includes);
|
|
400
|
+
const toRemove = [];
|
|
401
|
+
const toReindex = [];
|
|
402
|
+
for (const change of changes) {
|
|
403
|
+
const absolutePath = resolve(repoRoot, change.path);
|
|
404
|
+
const relativePath = relative(workspaceRoot, absolutePath).replace(/\\/g, "/");
|
|
405
|
+
if (!matcher(relativePath)) {
|
|
406
|
+
toRemove.push(relativePath);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (change.status === "removed") {
|
|
410
|
+
toRemove.push(relativePath);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const fileInfo = await this.getFileInfo(absolutePath, relativePath);
|
|
414
|
+
if (fileInfo) {
|
|
415
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
416
|
+
const contentHash = createHash("md5").update(content).digest("hex");
|
|
417
|
+
toReindex.push({ path: relativePath, fileInfo, contentHash });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
this.store.inTransaction(() => {
|
|
421
|
+
for (const p of toRemove) {
|
|
422
|
+
this.store.deleteFile(p);
|
|
423
|
+
}
|
|
424
|
+
for (const { path: p, fileInfo, contentHash } of toReindex) {
|
|
425
|
+
this.store.deleteSymbolsForFile(p);
|
|
426
|
+
this.store.deleteEdgesForFile(p);
|
|
427
|
+
this.store.upsertFile({
|
|
428
|
+
path: p,
|
|
429
|
+
repo: repo.name,
|
|
430
|
+
language: fileInfo.language,
|
|
431
|
+
gitBlobHash: null,
|
|
432
|
+
contentHash,
|
|
433
|
+
lastScanned: (/* @__PURE__ */ new Date()).toISOString(),
|
|
434
|
+
sizeBytes: fileInfo.sizeBytes,
|
|
435
|
+
lines: fileInfo.lines
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
const registry = FrameworkRegistry.getInstance();
|
|
440
|
+
const allFilePaths = this.store.getAllFiles(repo.name).map((f) => f.path);
|
|
441
|
+
const activeDetectors = await registry.detectActiveFrameworks(repoRoot, allFilePaths);
|
|
442
|
+
const activeFrameworks = activeDetectors.map((d) => d.name);
|
|
443
|
+
const ignoredSymbols = buildIgnoredSymbols(activeFrameworks);
|
|
444
|
+
const parseResults = await this.parseFilesParallel(
|
|
445
|
+
toReindex.map((r) => r.fileInfo),
|
|
446
|
+
workspaceRoot,
|
|
447
|
+
ignoredSymbols
|
|
448
|
+
);
|
|
449
|
+
let totalSymbols = 0;
|
|
450
|
+
let totalEdges = 0;
|
|
451
|
+
const langBreakdown = {};
|
|
452
|
+
for (let i = 0; i < toReindex.length && !this.aborted; i++) {
|
|
453
|
+
const { path: relPath, fileInfo } = toReindex[i];
|
|
454
|
+
const result = parseResults[i];
|
|
455
|
+
this.writeParseResult(relPath, result, repo.name);
|
|
456
|
+
totalSymbols += result.symbols.length;
|
|
457
|
+
totalEdges += result.references.length;
|
|
458
|
+
langBreakdown[fileInfo.language] = (langBreakdown[fileInfo.language] || 0) + 1;
|
|
459
|
+
this.onProgress?.({
|
|
460
|
+
phase: "parse",
|
|
461
|
+
current: i + 1,
|
|
462
|
+
total: changes.length,
|
|
463
|
+
file: relPath
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
if (!this.aborted) {
|
|
467
|
+
const commitSha = getCurrentCommitSha(repoRoot);
|
|
468
|
+
if (commitSha) this.store.setMeta("last_scan_commit:" + repo.name, commitSha);
|
|
469
|
+
this.store.setMeta("last_scan_time:" + repo.name, (/* @__PURE__ */ new Date()).toISOString());
|
|
470
|
+
this.onProgress?.({ phase: "cluster", current: 0, total: 0 });
|
|
471
|
+
const clusterEngine = new ClusterEngine(this.store);
|
|
472
|
+
clusterEngine.detect(repo.name);
|
|
473
|
+
await this.scanFrameworkRoutesAndHooks(repo, repoRoot);
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
filesScanned: changes.length,
|
|
477
|
+
symbolsFound: totalSymbols,
|
|
478
|
+
edgesFound: totalEdges,
|
|
479
|
+
durationMs: Date.now() - startTime,
|
|
480
|
+
languageBreakdown: langBreakdown
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
async discoverFiles(repoRoot, repoName, silent = false) {
|
|
484
|
+
const files = [];
|
|
485
|
+
const workspaceRoot = this.config.getWorkspaceRoot();
|
|
486
|
+
const excludes = [
|
|
487
|
+
...this.config.settings.excludePatterns,
|
|
488
|
+
...this.cliExcludes
|
|
489
|
+
];
|
|
490
|
+
const includes = [
|
|
491
|
+
...this.config.settings.includePatterns,
|
|
492
|
+
...this.cliIncludes
|
|
493
|
+
];
|
|
494
|
+
const matcher = buildMatcher(excludes, includes);
|
|
495
|
+
const trackedHashes = /* @__PURE__ */ new Map();
|
|
496
|
+
const allTracked = this.store.getAllFiles(repoName);
|
|
497
|
+
for (const f of allTracked) {
|
|
498
|
+
if (f.content_hash) trackedHashes.set(f.path, f.content_hash);
|
|
499
|
+
}
|
|
500
|
+
if (!silent) {
|
|
501
|
+
this.onProgress?.({ phase: "discover", current: 0, total: 0 });
|
|
502
|
+
}
|
|
503
|
+
const walk = async (currentDir) => {
|
|
504
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
505
|
+
for (const entry of entries) {
|
|
506
|
+
if (this.aborted) return;
|
|
507
|
+
if (DEFAULT_IGNORE.has(entry.name)) continue;
|
|
508
|
+
if (entry.name.startsWith(".") && entry.name !== ".mapx") continue;
|
|
509
|
+
const fullPath = join(currentDir, entry.name);
|
|
510
|
+
if (entry.isDirectory()) {
|
|
511
|
+
const relDir = relative(workspaceRoot, fullPath).replace(/\\/g, "/");
|
|
512
|
+
if (this.shouldExcludeDir(relDir, excludes)) continue;
|
|
513
|
+
const gitPath = join(fullPath, ".git");
|
|
514
|
+
if (existsSync(gitPath)) {
|
|
515
|
+
const registeredAbsPaths = this.config.repos.map((r) => resolve(workspaceRoot, r.path));
|
|
516
|
+
if (!registeredAbsPaths.map((p) => resolve(p)).includes(resolve(fullPath))) {
|
|
517
|
+
console.warn(`
|
|
518
|
+
\u26A0\uFE0F Warning: Found unregistered nested git repository at ${fullPath}. Skipping walk inside this directory.`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
await walk(fullPath);
|
|
523
|
+
} else if (entry.isFile()) {
|
|
524
|
+
const relPath = relative(workspaceRoot, fullPath).replace(/\\/g, "/");
|
|
525
|
+
if (!matcher(relPath)) continue;
|
|
526
|
+
const langDef = getLanguageForFile(fullPath, this.config.getResolvedUserLanguages());
|
|
527
|
+
if (!langDef) continue;
|
|
528
|
+
try {
|
|
529
|
+
const content = await readFile(fullPath, "utf-8");
|
|
530
|
+
const stats = await stat(fullPath);
|
|
531
|
+
const lines = content.split("\n").length;
|
|
532
|
+
const contentHash = createHash("md5").update(content).digest("hex");
|
|
533
|
+
const storedHash = trackedHashes.get(relPath);
|
|
534
|
+
const isNew = !storedHash;
|
|
535
|
+
const contentChanged = !!storedHash && storedHash !== contentHash;
|
|
536
|
+
files.push({
|
|
537
|
+
absolutePath: fullPath,
|
|
538
|
+
relativePath: relPath,
|
|
539
|
+
language: langDef.name,
|
|
540
|
+
sizeBytes: stats.size,
|
|
541
|
+
lines,
|
|
542
|
+
contentHash,
|
|
543
|
+
isNew,
|
|
544
|
+
contentChanged
|
|
545
|
+
});
|
|
546
|
+
if (!silent) {
|
|
547
|
+
this.onProgress?.({ phase: "discover", current: files.length, total: 0, file: relPath });
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
await walk(repoRoot);
|
|
555
|
+
return files;
|
|
556
|
+
}
|
|
557
|
+
detectDeletedFiles(discovered, repoName) {
|
|
558
|
+
const currentPaths = new Set(discovered.map((f) => f.relativePath));
|
|
559
|
+
const tracked = this.store.getAllFiles(repoName);
|
|
560
|
+
const deleted = [];
|
|
561
|
+
for (const f of tracked) {
|
|
562
|
+
const p = f.path;
|
|
563
|
+
if (!currentPaths.has(p)) deleted.push(p);
|
|
564
|
+
}
|
|
565
|
+
return deleted;
|
|
566
|
+
}
|
|
567
|
+
async parseFilesParallel(files, workspaceRoot, ignoredSymbols) {
|
|
568
|
+
if (files.length === 0) return [];
|
|
569
|
+
return this.parseOnMainThread(files, workspaceRoot, ignoredSymbols);
|
|
570
|
+
}
|
|
571
|
+
async parseWithWorkers(files, workspaceRoot, ignoredSymbols) {
|
|
572
|
+
return this.parseOnMainThread(files, workspaceRoot, ignoredSymbols);
|
|
573
|
+
}
|
|
574
|
+
async parseOnMainThread(files, workspaceRoot, ignoredSymbols) {
|
|
575
|
+
const results = new Array(files.length);
|
|
576
|
+
const sources = await Promise.all(
|
|
577
|
+
files.map(async (f) => {
|
|
578
|
+
try {
|
|
579
|
+
return await readFile(f.absolutePath, "utf-8");
|
|
580
|
+
} catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
);
|
|
585
|
+
const CONCURRENCY = this.concurrency;
|
|
586
|
+
let nextIdx = 0;
|
|
587
|
+
const runWorker = async () => {
|
|
588
|
+
while (!this.aborted) {
|
|
589
|
+
const i = nextIdx++;
|
|
590
|
+
if (i >= files.length) break;
|
|
591
|
+
const fileInfo = files[i];
|
|
592
|
+
const relPath = relative(workspaceRoot, fileInfo.absolutePath).replace(/\\/g, "/");
|
|
593
|
+
if (sources[i] === null) {
|
|
594
|
+
results[i] = { symbols: [], references: [], errors: [{ message: `Failed to read ${relPath}` }] };
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
const parser = getParserForFile(relPath, this.config.getResolvedUserLanguages());
|
|
599
|
+
results[i] = await parser.parse(relPath, sources[i], {
|
|
600
|
+
facadeMap: this.config.settings.php?.facadeMap,
|
|
601
|
+
ignoredSymbols
|
|
602
|
+
});
|
|
603
|
+
} catch {
|
|
604
|
+
results[i] = { symbols: [], references: [], errors: [{ message: `Failed to parse ${relPath}` }] };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, files.length) }, runWorker));
|
|
609
|
+
return results;
|
|
610
|
+
}
|
|
611
|
+
writeParseResult(relativePath, result, repoName) {
|
|
612
|
+
if (this.workspaceFileMap.size === 0) {
|
|
613
|
+
for (const f of this.store.getAllFiles()) {
|
|
614
|
+
this.workspaceFileMap.set(f.path, f.repo);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
this.store.inTransaction(() => {
|
|
618
|
+
this.writeParseResultWithMap(relativePath, result, repoName, this.workspaceFileMap);
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
writeParseResultWithMap(relativePath, result, repoName, fileMap) {
|
|
622
|
+
if (result.fileMetadata) {
|
|
623
|
+
this.store.updateFileMetadata(relativePath, result.fileMetadata);
|
|
624
|
+
}
|
|
625
|
+
this.store.deleteSymbolsForFile(relativePath);
|
|
626
|
+
for (const sym of result.symbols) {
|
|
627
|
+
this.graph.addSymbolNode(
|
|
628
|
+
sym.name,
|
|
629
|
+
relativePath,
|
|
630
|
+
sym.name,
|
|
631
|
+
sym.kind,
|
|
632
|
+
sym.startLine,
|
|
633
|
+
sym.endLine,
|
|
634
|
+
sym.scope
|
|
635
|
+
);
|
|
636
|
+
this.store.insertSymbol({
|
|
637
|
+
filePath: relativePath,
|
|
638
|
+
repo: repoName,
|
|
639
|
+
name: sym.name,
|
|
640
|
+
kind: sym.kind,
|
|
641
|
+
scope: sym.scope,
|
|
642
|
+
signature: sym.signature,
|
|
643
|
+
startLine: sym.startLine,
|
|
644
|
+
endLine: sym.endLine,
|
|
645
|
+
metadata: JSON.stringify(sym.metadata)
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
this.store.deleteEdgesForFile(relativePath);
|
|
649
|
+
const resolvedRefs = this.resolveReferencesWithMap(result.references, relativePath, repoName, fileMap, result.symbols);
|
|
650
|
+
for (const edge of resolvedRefs) {
|
|
651
|
+
this.graph.addDependencyEdge(edge);
|
|
652
|
+
this.store.insertEdge(edge);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
resolveReferences(refs, sourcePath, repoName) {
|
|
656
|
+
if (this.workspaceFileMap.size === 0) {
|
|
657
|
+
for (const f of this.store.getAllFiles()) {
|
|
658
|
+
this.workspaceFileMap.set(f.path, f.repo);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const dbSymbols = this.store.getSymbolsForFile(sourcePath);
|
|
662
|
+
const symbols = dbSymbols.map((s) => ({
|
|
663
|
+
name: s.name,
|
|
664
|
+
kind: s.kind,
|
|
665
|
+
scope: s.scope,
|
|
666
|
+
startLine: s.start_line,
|
|
667
|
+
endLine: s.end_line,
|
|
668
|
+
metadata: s.metadata ? JSON.parse(s.metadata) : {}
|
|
669
|
+
}));
|
|
670
|
+
return this.resolveReferencesWithMap(refs, sourcePath, repoName, this.workspaceFileMap, symbols);
|
|
671
|
+
}
|
|
672
|
+
resolveReferencesWithMap(refs, sourcePath, repoName, fileMap, symbols) {
|
|
673
|
+
const edges = [];
|
|
674
|
+
for (const ref of refs) {
|
|
675
|
+
if (!ref.targetName || typeof ref.targetName !== "string") continue;
|
|
676
|
+
let targetFile = null;
|
|
677
|
+
if (ref.referenceType === "require") {
|
|
678
|
+
targetFile = this.resolveRequirePath(ref.targetName, sourcePath, fileMap);
|
|
679
|
+
} else if (ref.referenceType === "import") {
|
|
680
|
+
targetFile = this.resolveImportPath(ref.targetName, sourcePath, fileMap);
|
|
681
|
+
} else {
|
|
682
|
+
targetFile = this.resolveSymbolToFile(ref.targetName, fileMap);
|
|
683
|
+
}
|
|
684
|
+
if (targetFile) {
|
|
685
|
+
const targetRepoName = fileMap.get(targetFile);
|
|
686
|
+
let sourceSymbol = ref.sourceSymbol;
|
|
687
|
+
if (!sourceSymbol && symbols) {
|
|
688
|
+
let bestSym = null;
|
|
689
|
+
let bestSpan = Infinity;
|
|
690
|
+
for (const sym of symbols) {
|
|
691
|
+
if (sym.startLine <= ref.startLine && ref.startLine <= sym.endLine) {
|
|
692
|
+
const span = sym.endLine - sym.startLine;
|
|
693
|
+
if (span < bestSpan) {
|
|
694
|
+
bestSpan = span;
|
|
695
|
+
bestSym = sym;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (bestSym) {
|
|
700
|
+
sourceSymbol = bestSym.scope ? `${bestSym.scope}::${bestSym.name}` : bestSym.name;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
edges.push({
|
|
704
|
+
sourceFile: sourcePath,
|
|
705
|
+
targetFile,
|
|
706
|
+
sourceSymbol,
|
|
707
|
+
targetSymbol: ref.targetName,
|
|
708
|
+
edgeType: ref.referenceType,
|
|
709
|
+
repo: repoName,
|
|
710
|
+
weight: 1,
|
|
711
|
+
verifiability: ref.verifiability ?? "verified",
|
|
712
|
+
targetRepo: targetRepoName && targetRepoName !== repoName ? targetRepoName : void 0,
|
|
713
|
+
metadata: { startLine: ref.startLine }
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return edges;
|
|
718
|
+
}
|
|
719
|
+
resolveRequirePath(target, sourcePath, fileMap) {
|
|
720
|
+
if (!target || typeof target !== "string") return null;
|
|
721
|
+
const dir = sourcePath.includes("/") ? sourcePath.substring(0, sourcePath.lastIndexOf("/")) : "";
|
|
722
|
+
const candidates = [
|
|
723
|
+
target.startsWith("./") ? join(dir, target) : target,
|
|
724
|
+
target.startsWith("./") ? join(dir, target + ".php") : target,
|
|
725
|
+
target + ".php"
|
|
726
|
+
];
|
|
727
|
+
for (const candidate of candidates) {
|
|
728
|
+
const normalized = candidate.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
729
|
+
if (fileMap.has(normalized)) return normalized;
|
|
730
|
+
}
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
resolveImportPath(target, sourcePath, fileMap) {
|
|
734
|
+
if (!target || typeof target !== "string") return null;
|
|
735
|
+
let resolvedTarget = target;
|
|
736
|
+
if (sourcePath.endsWith(".vue") && target.startsWith("@/")) {
|
|
737
|
+
resolvedTarget = "src/" + target.substring(2);
|
|
738
|
+
} else {
|
|
739
|
+
const dir = sourcePath.includes("/") ? sourcePath.substring(0, sourcePath.lastIndexOf("/")) : "";
|
|
740
|
+
resolvedTarget = target.startsWith(".") ? join(dir, target) : target;
|
|
741
|
+
}
|
|
742
|
+
const normalizedTarget = resolvedTarget.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\//, "");
|
|
743
|
+
const candidates = [
|
|
744
|
+
normalizedTarget,
|
|
745
|
+
normalizedTarget + "/index.js",
|
|
746
|
+
normalizedTarget + "/index.ts",
|
|
747
|
+
normalizedTarget + "/index.vue",
|
|
748
|
+
normalizedTarget + ".js",
|
|
749
|
+
normalizedTarget + ".ts",
|
|
750
|
+
normalizedTarget + ".vue"
|
|
751
|
+
];
|
|
752
|
+
for (const candidate of candidates) {
|
|
753
|
+
if (fileMap.has(candidate)) {
|
|
754
|
+
return candidate;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
resolveSymbolToFile(symbolName, fileMap) {
|
|
760
|
+
if (!symbolName || typeof symbolName !== "string") return null;
|
|
761
|
+
let matches = this.store.searchSymbols(symbolName);
|
|
762
|
+
if (matches.length > 0) {
|
|
763
|
+
const exactMatch = matches.find((m) => m.name === symbolName);
|
|
764
|
+
if (exactMatch) return exactMatch.file_path;
|
|
765
|
+
}
|
|
766
|
+
if (symbolName.includes("\\")) {
|
|
767
|
+
const parts = symbolName.split("\\");
|
|
768
|
+
const shortName = parts[parts.length - 1];
|
|
769
|
+
matches = this.store.searchSymbols(shortName);
|
|
770
|
+
if (matches.length > 0) {
|
|
771
|
+
const exactMatch = matches.find((m) => m.name === shortName);
|
|
772
|
+
if (exactMatch) return exactMatch.file_path;
|
|
773
|
+
return matches[0].file_path;
|
|
774
|
+
}
|
|
775
|
+
} else if (matches.length > 0) {
|
|
776
|
+
return matches[0].file_path;
|
|
777
|
+
}
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
shouldExcludeDir(relDir, excludes) {
|
|
781
|
+
return excludes.some((p) => {
|
|
782
|
+
if (minimatch(relDir, p, { dot: true })) return true;
|
|
783
|
+
if (minimatch(relDir + "/**", p, { dot: true })) return true;
|
|
784
|
+
const segments = relDir.split("/");
|
|
785
|
+
if (segments.some((seg) => seg === p)) return true;
|
|
786
|
+
return false;
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
async getFileInfo(absolutePath, relativePath) {
|
|
790
|
+
const langDef = getLanguageForFile(absolutePath, this.config.getResolvedUserLanguages());
|
|
791
|
+
if (!langDef) return null;
|
|
792
|
+
try {
|
|
793
|
+
const stats = await stat(absolutePath);
|
|
794
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
795
|
+
return {
|
|
796
|
+
absolutePath,
|
|
797
|
+
language: langDef.name,
|
|
798
|
+
sizeBytes: stats.size,
|
|
799
|
+
lines: content.split("\n").length
|
|
800
|
+
};
|
|
801
|
+
} catch {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
async scanFrameworkRoutesAndHooks(repo, repoRoot) {
|
|
806
|
+
const workspaceRoot = this.config.getWorkspaceRoot();
|
|
807
|
+
const files = await this.store.getAllFiles(repo.name);
|
|
808
|
+
const filePaths = files.map((f) => f.path);
|
|
809
|
+
const registry = FrameworkRegistry.getInstance();
|
|
810
|
+
const activeDetectors = await registry.detectActiveFrameworks(repoRoot, filePaths);
|
|
811
|
+
if (activeDetectors.length === 0) return;
|
|
812
|
+
const frameworksPath = join(workspaceRoot, ".mapx", "frameworks.json");
|
|
813
|
+
const activeNames = activeDetectors.map((d) => d.name);
|
|
814
|
+
await writeFile(frameworksPath, JSON.stringify(activeNames, null, 2), "utf-8");
|
|
815
|
+
this.store.deleteFrameworkEdgesForRepo(repo.name);
|
|
816
|
+
this.graph.dropFrameworkEdgesForRepo(repo.name);
|
|
817
|
+
const routeRegistry = new RouteRegistry();
|
|
818
|
+
await routeRegistry.load(workspaceRoot);
|
|
819
|
+
routeRegistry.clearRepo(repo.name, new Set(filePaths));
|
|
820
|
+
const ctx = {
|
|
821
|
+
workspaceRoot,
|
|
822
|
+
repoName: repo.name,
|
|
823
|
+
resolveSymbolToFile: (symName) => {
|
|
824
|
+
return this.resolveSymbolToFile(symName, this.workspaceFileMap);
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
for (const detector of activeDetectors) {
|
|
828
|
+
const matchingPaths = filePaths.filter((p) => detector.filePattern.test(p));
|
|
829
|
+
for (const relPath of matchingPaths) {
|
|
830
|
+
try {
|
|
831
|
+
const absPath = resolve(workspaceRoot, relPath);
|
|
832
|
+
const content = await readFile(absPath, "utf-8");
|
|
833
|
+
const routes = await detector.extractRoutes(relPath, content, ctx);
|
|
834
|
+
for (const route of routes) {
|
|
835
|
+
let conf = 1;
|
|
836
|
+
const routeConf = route.metadata?.confidence ?? route.confidence;
|
|
837
|
+
if (typeof routeConf === "number") {
|
|
838
|
+
conf = routeConf;
|
|
839
|
+
} else if (typeof routeConf === "string") {
|
|
840
|
+
if (routeConf === "declared") conf = 1;
|
|
841
|
+
else if (routeConf === "inferred") conf = 0.8;
|
|
842
|
+
else if (routeConf === "low") conf = 0.3;
|
|
843
|
+
}
|
|
844
|
+
if (conf < 0.5) {
|
|
845
|
+
console.warn(`[mapx] Suppressing route edge due to low confidence (${conf}): ${route.method} ${route.path} -> ${route.handlerFile}`);
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if (!route.metadata) route.metadata = {};
|
|
849
|
+
route.metadata.repo = repo.name;
|
|
850
|
+
route.metadata.sourceFile = relPath;
|
|
851
|
+
routeRegistry.addRoute(route);
|
|
852
|
+
this.store.insertEdge({
|
|
853
|
+
sourceFile: relPath,
|
|
854
|
+
targetFile: route.handlerFile,
|
|
855
|
+
sourceSymbol: null,
|
|
856
|
+
targetSymbol: route.handlerSymbol || null,
|
|
857
|
+
edgeType: "route",
|
|
858
|
+
repo: repo.name,
|
|
859
|
+
weight: 1,
|
|
860
|
+
verifiability: "inferred",
|
|
861
|
+
metadata: {
|
|
862
|
+
httpVerb: route.method,
|
|
863
|
+
uri: route.path,
|
|
864
|
+
middlewares: route.middlewares,
|
|
865
|
+
confidence: route.metadata?.confidence || "inferred"
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
this.graph.addDependencyEdge({
|
|
869
|
+
sourceFile: relPath,
|
|
870
|
+
targetFile: route.handlerFile,
|
|
871
|
+
sourceSymbol: null,
|
|
872
|
+
targetSymbol: route.handlerSymbol || null,
|
|
873
|
+
edgeType: "route",
|
|
874
|
+
repo: repo.name,
|
|
875
|
+
weight: 1,
|
|
876
|
+
verifiability: "inferred",
|
|
877
|
+
metadata: {
|
|
878
|
+
httpVerb: route.method,
|
|
879
|
+
uri: route.path,
|
|
880
|
+
middlewares: route.middlewares,
|
|
881
|
+
confidence: route.metadata?.confidence || "inferred"
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
if (detector.extractHooks) {
|
|
886
|
+
const hooks = await detector.extractHooks(relPath, content, ctx);
|
|
887
|
+
for (const hook of hooks) {
|
|
888
|
+
let conf = 1;
|
|
889
|
+
const hookConf = hook.metadata?.confidence ?? hook.confidence;
|
|
890
|
+
if (typeof hookConf === "number") {
|
|
891
|
+
conf = hookConf;
|
|
892
|
+
} else if (typeof hookConf === "string") {
|
|
893
|
+
if (hookConf === "declared") conf = 1;
|
|
894
|
+
else if (hookConf === "inferred") conf = 0.8;
|
|
895
|
+
else if (hookConf === "low") conf = 0.3;
|
|
896
|
+
}
|
|
897
|
+
if (conf < 0.5) {
|
|
898
|
+
console.warn(`[mapx] Suppressing hook edge due to low confidence (${conf}): ${hook.hookName} -> ${hook.handlerFile}`);
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (!hook.metadata) hook.metadata = {};
|
|
902
|
+
hook.metadata.repo = repo.name;
|
|
903
|
+
hook.metadata.sourceFile = relPath;
|
|
904
|
+
routeRegistry.addHook(hook);
|
|
905
|
+
const edgeType = ["graphql_resolver", "message_handler", "websocket_handler", "middleware"].includes(hook.hookType) ? hook.hookType : "hook";
|
|
906
|
+
this.store.insertEdge({
|
|
907
|
+
sourceFile: relPath,
|
|
908
|
+
targetFile: hook.handlerFile,
|
|
909
|
+
sourceSymbol: null,
|
|
910
|
+
targetSymbol: hook.handlerSymbol || null,
|
|
911
|
+
edgeType,
|
|
912
|
+
repo: repo.name,
|
|
913
|
+
weight: 1,
|
|
914
|
+
verifiability: "inferred",
|
|
915
|
+
metadata: {
|
|
916
|
+
hookName: hook.hookName,
|
|
917
|
+
hookType: hook.hookType,
|
|
918
|
+
confidence: hook.metadata?.confidence || "inferred"
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
this.graph.addDependencyEdge({
|
|
922
|
+
sourceFile: relPath,
|
|
923
|
+
targetFile: hook.handlerFile,
|
|
924
|
+
sourceSymbol: null,
|
|
925
|
+
targetSymbol: hook.handlerSymbol || null,
|
|
926
|
+
edgeType,
|
|
927
|
+
repo: repo.name,
|
|
928
|
+
weight: 1,
|
|
929
|
+
verifiability: "inferred",
|
|
930
|
+
metadata: {
|
|
931
|
+
hookName: hook.hookName,
|
|
932
|
+
hookType: hook.hookType,
|
|
933
|
+
confidence: hook.metadata?.confidence || "inferred"
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} catch (err) {
|
|
939
|
+
console.error(`Failed to extract routes/hooks for ${relPath}:`, err);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
await routeRegistry.save(workspaceRoot);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
export {
|
|
947
|
+
Scanner,
|
|
948
|
+
buildMatcher
|
|
949
|
+
};
|