@mishasinitcyn/betterrank 0.1.6 → 0.1.8
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/package.json +1 -1
- package/src/cache.js +85 -20
- package/src/cli.js +221 -8
- package/src/index.js +137 -14
- package/src/server.js +11 -0
- package/src/ui.html +652 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mishasinitcyn/betterrank",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Structural code index with PageRank-ranked repo maps, symbol search, call-graph queries, and dependency analysis. Built on tree-sitter and graphology.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/cache.js
CHANGED
|
@@ -24,38 +24,90 @@ function getPlatformCacheDir() {
|
|
|
24
24
|
const CACHE_DIR = getPlatformCacheDir();
|
|
25
25
|
|
|
26
26
|
const IGNORE_PATTERNS = [
|
|
27
|
-
//
|
|
27
|
+
// ── JS / Node ──────────────────────────────────────────
|
|
28
28
|
'**/node_modules/**',
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'**/
|
|
29
|
+
'**/.npm/**',
|
|
30
|
+
'**/.yarn/**',
|
|
31
|
+
'**/.pnp.*',
|
|
32
|
+
'**/bower_components/**',
|
|
33
33
|
'**/*.min.js',
|
|
34
34
|
'**/*.bundle.js',
|
|
35
35
|
'**/*.map',
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
'**/.git/**',
|
|
39
|
-
'**/.code-index/**',
|
|
40
|
-
'**/.claude/**',
|
|
41
|
-
'**/.cursor/**',
|
|
42
|
-
|
|
43
|
-
// Language-specific caches
|
|
37
|
+
// ── Python ─────────────────────────────────────────────
|
|
44
38
|
'**/__pycache__/**',
|
|
45
39
|
'**/.venv/**',
|
|
40
|
+
'**/venv/**',
|
|
41
|
+
'**/env/**',
|
|
42
|
+
'**/.env/**',
|
|
43
|
+
'**/.virtualenvs/**',
|
|
44
|
+
'**/site-packages/**',
|
|
45
|
+
'**/*.egg-info/**',
|
|
46
|
+
'**/.eggs/**',
|
|
47
|
+
'**/.tox/**',
|
|
48
|
+
'**/.mypy_cache/**',
|
|
49
|
+
'**/.pytest_cache/**',
|
|
50
|
+
'**/.ruff_cache/**',
|
|
51
|
+
|
|
52
|
+
// ── Rust ───────────────────────────────────────────────
|
|
53
|
+
'**/target/debug/**',
|
|
54
|
+
'**/target/release/**',
|
|
55
|
+
|
|
56
|
+
// ── Java / JVM ─────────────────────────────────────────
|
|
57
|
+
'**/.gradle/**',
|
|
58
|
+
'**/.m2/**',
|
|
59
|
+
|
|
60
|
+
// ── Ruby ───────────────────────────────────────────────
|
|
61
|
+
'**/.bundle/**',
|
|
62
|
+
|
|
63
|
+
// ── C / C++ ────────────────────────────────────────────
|
|
64
|
+
'**/cmake-build-*/**',
|
|
65
|
+
'**/CMakeFiles/**',
|
|
66
|
+
|
|
67
|
+
// ── Go ─────────────────────────────────────────────────
|
|
68
|
+
'**/vendor/**',
|
|
69
|
+
|
|
70
|
+
// ── Build output & generated ───────────────────────────
|
|
71
|
+
'**/dist/**',
|
|
72
|
+
'**/build/**',
|
|
73
|
+
'**/out/**',
|
|
74
|
+
'**/coverage/**',
|
|
75
|
+
|
|
76
|
+
// ── Frameworks ─────────────────────────────────────────
|
|
46
77
|
'**/.next/**',
|
|
47
78
|
'**/.nuxt/**',
|
|
48
|
-
|
|
49
|
-
|
|
79
|
+
'**/.output/**',
|
|
80
|
+
'**/.svelte-kit/**',
|
|
81
|
+
'**/.angular/**',
|
|
82
|
+
'**/.turbo/**',
|
|
83
|
+
'**/.parcel-cache/**',
|
|
84
|
+
'**/.cache/**',
|
|
85
|
+
|
|
86
|
+
// ── iOS / mobile ───────────────────────────────────────
|
|
50
87
|
'**/Pods/**',
|
|
51
88
|
'**/*.xcframework/**',
|
|
52
89
|
|
|
53
|
-
//
|
|
90
|
+
// ── Infrastructure / deploy ────────────────────────────
|
|
91
|
+
'**/.terraform/**',
|
|
92
|
+
'**/.serverless/**',
|
|
93
|
+
'**/cdk.out/**',
|
|
94
|
+
'**/.vercel/**',
|
|
95
|
+
'**/.netlify/**',
|
|
96
|
+
|
|
97
|
+
// ── VCS & tool caches ──────────────────────────────────
|
|
98
|
+
'**/.git/**',
|
|
99
|
+
'**/.code-index/**',
|
|
100
|
+
'**/.claude/**',
|
|
101
|
+
'**/.cursor/**',
|
|
102
|
+
'**/.nx/**',
|
|
103
|
+
|
|
104
|
+
// ── UI component libraries ─────────────────────────────
|
|
105
|
+
// shadcn, etc. — high fan-in but rarely investigation targets.
|
|
54
106
|
// To re-include, add "!**/components/ui/**" in .code-index/config.json ignore list,
|
|
55
107
|
// or pass --ignore '!**/components/ui/**' on the CLI.
|
|
56
108
|
'**/components/ui/**',
|
|
57
109
|
|
|
58
|
-
// Scratch / temp
|
|
110
|
+
// ── Scratch / temp ─────────────────────────────────────
|
|
59
111
|
'tmp/**',
|
|
60
112
|
];
|
|
61
113
|
|
|
@@ -98,7 +150,7 @@ class CodeIndexCache {
|
|
|
98
150
|
this.initialized = true;
|
|
99
151
|
}
|
|
100
152
|
|
|
101
|
-
const { changed, deleted } = await this._getChangedFiles();
|
|
153
|
+
const { changed, deleted, totalScanned } = await this._getChangedFiles();
|
|
102
154
|
|
|
103
155
|
if (changed.length === 0 && deleted.length === 0) {
|
|
104
156
|
if (!this.graph) {
|
|
@@ -107,9 +159,15 @@ class CodeIndexCache {
|
|
|
107
159
|
const MDG = graphology.default?.MultiDirectedGraph || graphology.MultiDirectedGraph;
|
|
108
160
|
this.graph = new MDG({ allowSelfLoops: false });
|
|
109
161
|
}
|
|
110
|
-
return { changed: 0, deleted: 0 };
|
|
162
|
+
return { changed: 0, deleted: 0, totalScanned };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const isColdStart = !this.graph;
|
|
166
|
+
if (isColdStart) {
|
|
167
|
+
process.stderr.write(`Indexing ${this.projectRoot}... ${changed.length} files found, parsing...\n`);
|
|
111
168
|
}
|
|
112
169
|
|
|
170
|
+
const t0 = Date.now();
|
|
113
171
|
const newSymbols = await this._parseFiles(changed);
|
|
114
172
|
|
|
115
173
|
if (!this.graph) {
|
|
@@ -123,7 +181,14 @@ class CodeIndexCache {
|
|
|
123
181
|
|
|
124
182
|
await saveGraph(this.graph, this.mtimes, this.cachePath);
|
|
125
183
|
|
|
126
|
-
|
|
184
|
+
if (isColdStart) {
|
|
185
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
186
|
+
let symbols = 0;
|
|
187
|
+
this.graph.forEachNode((_n, attrs) => { if (attrs.type === 'symbol') symbols++; });
|
|
188
|
+
process.stderr.write(`Indexed ${changed.length} files (${symbols} symbols, ${this.graph.size} edges) in ${elapsed}s\n`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { changed: changed.length, deleted: deleted.length, totalScanned };
|
|
127
192
|
}
|
|
128
193
|
|
|
129
194
|
/**
|
|
@@ -204,7 +269,7 @@ class CodeIndexCache {
|
|
|
204
269
|
}
|
|
205
270
|
}
|
|
206
271
|
|
|
207
|
-
return { changed, deleted };
|
|
272
|
+
return { changed, deleted, totalScanned: files.length };
|
|
208
273
|
}
|
|
209
274
|
|
|
210
275
|
/**
|
package/src/cli.js
CHANGED
|
@@ -27,8 +27,152 @@ Global flags:
|
|
|
27
27
|
--count Return counts only (no content)
|
|
28
28
|
--offset N Skip first N results
|
|
29
29
|
--limit N Max results to return (default: ${DEFAULT_LIMIT} for list commands)
|
|
30
|
+
--help Show help for a command (e.g. betterrank search --help)
|
|
30
31
|
`.trim();
|
|
31
32
|
|
|
33
|
+
const COMMAND_HELP = {
|
|
34
|
+
map: `betterrank map [--focus file1,file2] [--root <path>]
|
|
35
|
+
|
|
36
|
+
Aider-style repo map: the most structurally important definitions ranked by PageRank.
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--focus <files> Comma-separated files to bias ranking toward
|
|
40
|
+
--count Return total symbol count only
|
|
41
|
+
--offset N Skip first N symbols
|
|
42
|
+
--limit N Max symbols to return (default: ${DEFAULT_LIMIT})
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
betterrank map --root ./backend
|
|
46
|
+
betterrank map --root ./frontend --limit 100
|
|
47
|
+
betterrank map --root ./backend --focus src/auth/handlers.ts,src/api/login.ts`,
|
|
48
|
+
|
|
49
|
+
search: `betterrank search <query> [--kind type] [--root <path>]
|
|
50
|
+
|
|
51
|
+
Substring search on symbol names + full signatures (param names, types, defaults).
|
|
52
|
+
Results ranked by PageRank (most structurally important first).
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
--kind <type> Filter: function, class, type, variable, namespace, import
|
|
56
|
+
--count Return match count only
|
|
57
|
+
--offset N Skip first N results
|
|
58
|
+
--limit N Max results (default: ${DEFAULT_LIMIT})
|
|
59
|
+
|
|
60
|
+
Tips:
|
|
61
|
+
Use short substrings (3-5 chars) — PageRank ranking handles noise.
|
|
62
|
+
"imp" finds encrypt_imp_payload, increment_impression, etc.
|
|
63
|
+
Searches match against both symbol names AND full signatures (param names, types).
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
betterrank search resolve --root ./backend
|
|
67
|
+
betterrank search auth --kind function --limit 10
|
|
68
|
+
betterrank search max_age --root . --count`,
|
|
69
|
+
|
|
70
|
+
structure: `betterrank structure [--depth N] [--root <path>]
|
|
71
|
+
|
|
72
|
+
File tree with symbol counts per file.
|
|
73
|
+
|
|
74
|
+
Options:
|
|
75
|
+
--depth N Max directory depth (default: ${DEFAULT_DEPTH})
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
betterrank structure --root ./backend --depth 2`,
|
|
79
|
+
|
|
80
|
+
symbols: `betterrank symbols [--file path] [--kind type] [--root <path>]
|
|
81
|
+
|
|
82
|
+
List symbol definitions, optionally filtered by file or kind.
|
|
83
|
+
Results ranked by PageRank (most structurally important first).
|
|
84
|
+
|
|
85
|
+
Options:
|
|
86
|
+
--file <path> Filter to a specific file (relative to --root)
|
|
87
|
+
--kind <type> Filter: function, class, type, variable, namespace, import
|
|
88
|
+
--count Return count only
|
|
89
|
+
--offset N Skip first N results
|
|
90
|
+
--limit N Max results (default: ${DEFAULT_LIMIT})
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
betterrank symbols --file src/auth/handlers.ts --root ./backend
|
|
94
|
+
betterrank symbols --kind class --root . --limit 20`,
|
|
95
|
+
|
|
96
|
+
callers: `betterrank callers <symbol> [--file path] [--root <path>]
|
|
97
|
+
|
|
98
|
+
Find all files that reference a symbol. Ranked by file-level PageRank.
|
|
99
|
+
|
|
100
|
+
Options:
|
|
101
|
+
--file <path> Disambiguate when multiple symbols share a name
|
|
102
|
+
--count Return count only
|
|
103
|
+
--offset N Skip first N results
|
|
104
|
+
--limit N Max results (default: ${DEFAULT_LIMIT})
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
betterrank callers authenticateUser --root ./backend
|
|
108
|
+
betterrank callers resolve --file src/utils.ts --root .`,
|
|
109
|
+
|
|
110
|
+
deps: `betterrank deps <file> [--root <path>]
|
|
111
|
+
|
|
112
|
+
What this file imports / depends on. Ranked by PageRank.
|
|
113
|
+
|
|
114
|
+
Options:
|
|
115
|
+
--count Return count only
|
|
116
|
+
--offset N Skip first N results
|
|
117
|
+
--limit N Max results (default: ${DEFAULT_LIMIT})
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
betterrank deps src/auth/handlers.ts --root ./backend`,
|
|
121
|
+
|
|
122
|
+
dependents: `betterrank dependents <file> [--root <path>]
|
|
123
|
+
|
|
124
|
+
What files import this file. Ranked by PageRank.
|
|
125
|
+
|
|
126
|
+
Options:
|
|
127
|
+
--count Return count only
|
|
128
|
+
--offset N Skip first N results
|
|
129
|
+
--limit N Max results (default: ${DEFAULT_LIMIT})
|
|
130
|
+
|
|
131
|
+
Examples:
|
|
132
|
+
betterrank dependents src/auth/handlers.ts --root ./backend`,
|
|
133
|
+
|
|
134
|
+
neighborhood: `betterrank neighborhood <file> [--hops N] [--max-files N] [--root <path>]
|
|
135
|
+
|
|
136
|
+
Local subgraph around a file: its imports, importers, and their symbols.
|
|
137
|
+
|
|
138
|
+
Options:
|
|
139
|
+
--hops N BFS depth for outgoing imports (default: 2)
|
|
140
|
+
--max-files N Max files to include (default: 15, direct neighbors always included)
|
|
141
|
+
--count Return counts only
|
|
142
|
+
--offset N Skip first N files
|
|
143
|
+
--limit N Max files to return
|
|
144
|
+
|
|
145
|
+
Examples:
|
|
146
|
+
betterrank neighborhood src/auth/handlers.ts --root ./backend
|
|
147
|
+
betterrank neighborhood src/api/bid.js --hops 3 --max-files 20 --root .`,
|
|
148
|
+
|
|
149
|
+
reindex: `betterrank reindex [--root <path>]
|
|
150
|
+
|
|
151
|
+
Force a full rebuild of the index. Use after branch switches, large merges,
|
|
152
|
+
or if results seem stale.
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
betterrank reindex --root ./backend`,
|
|
156
|
+
|
|
157
|
+
stats: `betterrank stats [--root <path>]
|
|
158
|
+
|
|
159
|
+
Show index statistics: file count, symbol count, edge count.
|
|
160
|
+
|
|
161
|
+
Examples:
|
|
162
|
+
betterrank stats --root .`,
|
|
163
|
+
|
|
164
|
+
ui: `betterrank ui [--port N]
|
|
165
|
+
|
|
166
|
+
Launch the interactive web UI for exploring the index.
|
|
167
|
+
|
|
168
|
+
Options:
|
|
169
|
+
--port N Port to listen on (default: 3333)
|
|
170
|
+
|
|
171
|
+
Examples:
|
|
172
|
+
betterrank ui
|
|
173
|
+
betterrank ui --port 8080`,
|
|
174
|
+
};
|
|
175
|
+
|
|
32
176
|
async function main() {
|
|
33
177
|
const args = process.argv.slice(2);
|
|
34
178
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
@@ -39,6 +183,16 @@ async function main() {
|
|
|
39
183
|
const command = args[0];
|
|
40
184
|
const flags = parseFlags(args.slice(1));
|
|
41
185
|
|
|
186
|
+
// Per-command --help
|
|
187
|
+
if (flags.help === true || flags.h === true) {
|
|
188
|
+
if (COMMAND_HELP[command]) {
|
|
189
|
+
console.log(COMMAND_HELP[command]);
|
|
190
|
+
} else {
|
|
191
|
+
console.log(USAGE);
|
|
192
|
+
}
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
|
|
42
196
|
// UI command doesn't need --root or a CodeIndex instance
|
|
43
197
|
if (command === 'ui') {
|
|
44
198
|
const { startServer } = await import('./server.js');
|
|
@@ -73,6 +227,35 @@ async function main() {
|
|
|
73
227
|
return rel;
|
|
74
228
|
}
|
|
75
229
|
|
|
230
|
+
/** Print file-not-found diagnostics and exit. Returns true if handled. */
|
|
231
|
+
function handleFileNotFound(result, filePath) {
|
|
232
|
+
if (!result || !result.fileNotFound) return false;
|
|
233
|
+
console.error(`File "${filePath}" not found in index.`);
|
|
234
|
+
if (result.suggestions && result.suggestions.length > 0) {
|
|
235
|
+
console.error(`Did you mean:`);
|
|
236
|
+
for (const s of result.suggestions) console.error(` ${s}`);
|
|
237
|
+
}
|
|
238
|
+
console.error(`Tip: File paths are relative to --root. Use \`betterrank structure\` to see indexed files.`);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Suggest shorter query alternatives by splitting on _ and camelCase boundaries. */
|
|
243
|
+
function suggestShorterQueries(query) {
|
|
244
|
+
const parts = new Set();
|
|
245
|
+
// Split on underscores
|
|
246
|
+
for (const p of query.split('_')) {
|
|
247
|
+
if (p.length >= 3) parts.add(p.toLowerCase());
|
|
248
|
+
}
|
|
249
|
+
// Split on camelCase boundaries
|
|
250
|
+
const camelParts = query.replace(/([a-z])([A-Z])/g, '$1_$2').split('_');
|
|
251
|
+
for (const p of camelParts) {
|
|
252
|
+
if (p.length >= 3) parts.add(p.toLowerCase());
|
|
253
|
+
}
|
|
254
|
+
// Remove the original query itself
|
|
255
|
+
parts.delete(query.toLowerCase());
|
|
256
|
+
return [...parts].slice(0, 4);
|
|
257
|
+
}
|
|
258
|
+
|
|
76
259
|
switch (command) {
|
|
77
260
|
case 'map': {
|
|
78
261
|
const focusFiles = flags.focus ? flags.focus.split(',') : [];
|
|
@@ -87,6 +270,17 @@ async function main() {
|
|
|
87
270
|
console.log(`Use --limit N to show more, or --count for totals`);
|
|
88
271
|
}
|
|
89
272
|
}
|
|
273
|
+
// Diagnostics on empty index
|
|
274
|
+
if (result.diagnostics) {
|
|
275
|
+
const d = result.diagnostics;
|
|
276
|
+
console.log(`\nDiagnostics:`);
|
|
277
|
+
console.log(` Root: ${d.root}`);
|
|
278
|
+
console.log(` Files found: ${d.filesScanned}`);
|
|
279
|
+
console.log(` Extensions: ${d.extensions}`);
|
|
280
|
+
if (d.filesScanned === 0) {
|
|
281
|
+
console.log(` Tip: No supported files found. Try a broader --root.`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
90
284
|
break;
|
|
91
285
|
}
|
|
92
286
|
|
|
@@ -102,7 +296,17 @@ async function main() {
|
|
|
102
296
|
console.log(`${s.file}:${s.lineStart} [${s.kind}] ${s.signature}`);
|
|
103
297
|
}
|
|
104
298
|
if (result.length === 0) {
|
|
105
|
-
|
|
299
|
+
const st = await idx.stats();
|
|
300
|
+
console.log(`(no matches for "${query}")`);
|
|
301
|
+
if (st.symbols > 0) {
|
|
302
|
+
console.log(`Index has ${st.symbols.toLocaleString()} symbols across ${st.files.toLocaleString()} files.`);
|
|
303
|
+
const suggestions = suggestShorterQueries(query);
|
|
304
|
+
if (suggestions.length > 0) {
|
|
305
|
+
console.log(`Tip: Use shorter substrings. Try: ${suggestions.map(s => `"${s}"`).join(', ')}`);
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
console.log(`Index is empty. Check --root or run: betterrank map --root <path>`);
|
|
309
|
+
}
|
|
106
310
|
} else if (result.length === effectiveLimit && userLimit === undefined) {
|
|
107
311
|
console.log(`\n(showing top ${effectiveLimit} by relevance — use --limit N or --count for total)`);
|
|
108
312
|
}
|
|
@@ -163,13 +367,15 @@ async function main() {
|
|
|
163
367
|
if (!file) { console.error('Usage: betterrank deps <file>'); process.exit(1); }
|
|
164
368
|
const effectiveLimit = countMode ? undefined : (userLimit !== undefined ? userLimit : DEFAULT_LIMIT);
|
|
165
369
|
const result = await idx.dependencies({ file, count: countMode, offset, limit: effectiveLimit });
|
|
370
|
+
if (handleFileNotFound(result, file)) break;
|
|
166
371
|
if (countMode) {
|
|
167
372
|
console.log(`total: ${result.total}`);
|
|
168
373
|
} else {
|
|
169
|
-
|
|
170
|
-
|
|
374
|
+
const items = result.items || result;
|
|
375
|
+
for (const d of items) console.log(d);
|
|
376
|
+
if (items.length === 0) {
|
|
171
377
|
console.log('(no dependencies)');
|
|
172
|
-
} else if (
|
|
378
|
+
} else if (items.length === effectiveLimit && userLimit === undefined) {
|
|
173
379
|
console.log(`\n(showing top ${effectiveLimit} by relevance — use --limit N or --count for total)`);
|
|
174
380
|
}
|
|
175
381
|
}
|
|
@@ -181,13 +387,15 @@ async function main() {
|
|
|
181
387
|
if (!file) { console.error('Usage: betterrank dependents <file>'); process.exit(1); }
|
|
182
388
|
const effectiveLimit = countMode ? undefined : (userLimit !== undefined ? userLimit : DEFAULT_LIMIT);
|
|
183
389
|
const result = await idx.dependents({ file, count: countMode, offset, limit: effectiveLimit });
|
|
390
|
+
if (handleFileNotFound(result, file)) break;
|
|
184
391
|
if (countMode) {
|
|
185
392
|
console.log(`total: ${result.total}`);
|
|
186
393
|
} else {
|
|
187
|
-
|
|
188
|
-
|
|
394
|
+
const items = result.items || result;
|
|
395
|
+
for (const d of items) console.log(d);
|
|
396
|
+
if (items.length === 0) {
|
|
189
397
|
console.log('(no dependents)');
|
|
190
|
-
} else if (
|
|
398
|
+
} else if (items.length === effectiveLimit && userLimit === undefined) {
|
|
191
399
|
console.log(`\n(showing top ${effectiveLimit} by relevance — use --limit N or --count for total)`);
|
|
192
400
|
}
|
|
193
401
|
}
|
|
@@ -208,6 +416,7 @@ async function main() {
|
|
|
208
416
|
const preview = await idx.neighborhood({
|
|
209
417
|
file, hops, maxFiles: maxFilesFlag, count: true,
|
|
210
418
|
});
|
|
419
|
+
if (handleFileNotFound(preview, file)) break;
|
|
211
420
|
console.log(`files: ${preview.totalFiles} (${preview.totalVisited} visited, ${preview.totalFiles} after ranking)`);
|
|
212
421
|
console.log(`symbols: ${preview.totalSymbols}`);
|
|
213
422
|
console.log(`edges: ${preview.totalEdges}`);
|
|
@@ -219,6 +428,7 @@ async function main() {
|
|
|
219
428
|
file, hops, maxFiles: maxFilesFlag,
|
|
220
429
|
count: countMode, offset, limit: userLimit,
|
|
221
430
|
});
|
|
431
|
+
if (handleFileNotFound(result, file)) break;
|
|
222
432
|
|
|
223
433
|
if (countMode) {
|
|
224
434
|
console.log(`files: ${result.totalFiles} (${result.totalVisited} visited, ${result.totalFiles} after ranking)`);
|
|
@@ -284,7 +494,10 @@ function parseFlags(args) {
|
|
|
284
494
|
const flags = { _positional: [] };
|
|
285
495
|
let i = 0;
|
|
286
496
|
while (i < args.length) {
|
|
287
|
-
if (args[i]
|
|
497
|
+
if (args[i] === '-h') {
|
|
498
|
+
flags.help = true;
|
|
499
|
+
i++;
|
|
500
|
+
} else if (args[i].startsWith('--')) {
|
|
288
501
|
const key = args[i].substring(2);
|
|
289
502
|
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
290
503
|
flags[key] = args[i + 1];
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
|
-
import { join, dirname, relative, sep } from 'path';
|
|
2
|
+
import { join, dirname, relative, sep, basename } from 'path';
|
|
3
3
|
import { CodeIndexCache } from './cache.js';
|
|
4
4
|
import { rankedSymbols } from './graph.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Find file nodes in the graph that look similar to the given path.
|
|
8
|
+
* Uses basename matching and substring matching on the full path.
|
|
9
|
+
*/
|
|
10
|
+
function findSimilarFiles(graph, filePath, maxSuggestions = 5) {
|
|
11
|
+
if (!graph) return [];
|
|
12
|
+
const base = basename(filePath);
|
|
13
|
+
const baseLower = base.toLowerCase();
|
|
14
|
+
const pathLower = filePath.toLowerCase();
|
|
15
|
+
const suggestions = [];
|
|
16
|
+
|
|
17
|
+
graph.forEachNode((node, attrs) => {
|
|
18
|
+
if (attrs.type !== 'file') return;
|
|
19
|
+
const nodeLower = node.toLowerCase();
|
|
20
|
+
const nodeBase = basename(node).toLowerCase();
|
|
21
|
+
// Exact basename match or basename contains query
|
|
22
|
+
if (nodeBase === baseLower || nodeBase.includes(baseLower) || nodeLower.includes(pathLower)) {
|
|
23
|
+
suggestions.push(node);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return suggestions.slice(0, maxSuggestions);
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
/**
|
|
7
31
|
* Apply offset/limit pagination to an array.
|
|
8
32
|
* Returns { items, total } where total is the unpaginated count.
|
|
@@ -78,13 +102,18 @@ class CodeIndex {
|
|
|
78
102
|
* @param {boolean} [opts.count] - If true, return only { total }
|
|
79
103
|
* @returns {{content, shownFiles, shownSymbols, totalFiles, totalSymbols}|{total: number}}
|
|
80
104
|
*/
|
|
81
|
-
async map({ focusFiles = [], offset, limit, count = false } = {}) {
|
|
82
|
-
await this._ensureReady();
|
|
105
|
+
async map({ focusFiles = [], offset, limit, count = false, structured = false } = {}) {
|
|
106
|
+
const ensureResult = await this._ensureReady();
|
|
83
107
|
const graph = this.cache.getGraph();
|
|
84
108
|
if (!graph || graph.order === 0) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
:
|
|
109
|
+
const diagnostics = {
|
|
110
|
+
root: this.projectRoot,
|
|
111
|
+
filesScanned: ensureResult.totalScanned || 0,
|
|
112
|
+
extensions: this.cache.extensions.join(', '),
|
|
113
|
+
};
|
|
114
|
+
if (count) return { total: 0, diagnostics };
|
|
115
|
+
if (structured) return { files: [], shownFiles: 0, shownSymbols: 0, totalFiles: 0, totalSymbols: 0, diagnostics };
|
|
116
|
+
return { content: '(empty index)', shownFiles: 0, shownSymbols: 0, totalFiles: 0, totalSymbols: 0, diagnostics };
|
|
88
117
|
}
|
|
89
118
|
|
|
90
119
|
// Count totals from the graph
|
|
@@ -97,7 +126,7 @@ class CodeIndex {
|
|
|
97
126
|
|
|
98
127
|
const ranked = this._getRanked(focusFiles);
|
|
99
128
|
|
|
100
|
-
// Collect all symbol entries ranked by PageRank
|
|
129
|
+
// Collect all symbol entries ranked by PageRank (rich data for both formats)
|
|
101
130
|
const allEntries = [];
|
|
102
131
|
for (const [symbolKey, _score] of ranked) {
|
|
103
132
|
let attrs;
|
|
@@ -108,18 +137,48 @@ class CodeIndex {
|
|
|
108
137
|
}
|
|
109
138
|
if (attrs.type !== 'symbol') continue;
|
|
110
139
|
|
|
111
|
-
|
|
112
|
-
|
|
140
|
+
allEntries.push({
|
|
141
|
+
file: attrs.file,
|
|
142
|
+
name: attrs.name,
|
|
143
|
+
kind: attrs.kind,
|
|
144
|
+
lineStart: attrs.lineStart,
|
|
145
|
+
lineEnd: attrs.lineEnd,
|
|
146
|
+
signature: attrs.signature,
|
|
147
|
+
});
|
|
113
148
|
}
|
|
114
149
|
|
|
115
150
|
if (count) return { total: allEntries.length };
|
|
116
151
|
|
|
117
152
|
const { items } = paginate(allEntries, { offset, limit });
|
|
118
153
|
|
|
154
|
+
// Structured format: return file objects with nested symbol arrays
|
|
155
|
+
if (structured) {
|
|
156
|
+
const fileGroups = new Map();
|
|
157
|
+
for (const entry of items) {
|
|
158
|
+
if (!fileGroups.has(entry.file)) fileGroups.set(entry.file, []);
|
|
159
|
+
fileGroups.get(entry.file).push({
|
|
160
|
+
name: entry.name,
|
|
161
|
+
kind: entry.kind,
|
|
162
|
+
lineStart: entry.lineStart,
|
|
163
|
+
lineEnd: entry.lineEnd,
|
|
164
|
+
signature: entry.signature,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
files: [...fileGroups.entries()].map(([path, symbols]) => ({ path, symbols })),
|
|
169
|
+
shownFiles: fileGroups.size,
|
|
170
|
+
shownSymbols: items.length,
|
|
171
|
+
totalFiles,
|
|
172
|
+
totalSymbols,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Text format (CLI output)
|
|
119
177
|
const fileGroups = new Map();
|
|
120
178
|
for (const entry of items) {
|
|
121
179
|
if (!fileGroups.has(entry.file)) fileGroups.set(entry.file, []);
|
|
122
|
-
|
|
180
|
+
const line = ` ${String(entry.lineStart).padStart(4)}│ ${entry.signature}`;
|
|
181
|
+
fileGroups.get(entry.file).push(line);
|
|
123
182
|
}
|
|
124
183
|
|
|
125
184
|
const lines = [];
|
|
@@ -332,7 +391,11 @@ class CodeIndex {
|
|
|
332
391
|
async dependencies({ file, offset, limit, count = false }) {
|
|
333
392
|
await this._ensureReady();
|
|
334
393
|
const graph = this.cache.getGraph();
|
|
335
|
-
if (!graph || !graph.hasNode(file))
|
|
394
|
+
if (!graph || !graph.hasNode(file)) {
|
|
395
|
+
const suggestions = findSimilarFiles(graph, file);
|
|
396
|
+
if (count) return { total: 0, fileNotFound: true, suggestions };
|
|
397
|
+
return { items: [], fileNotFound: true, suggestions };
|
|
398
|
+
}
|
|
336
399
|
|
|
337
400
|
const fileScores = this._getFileScores();
|
|
338
401
|
|
|
@@ -366,7 +429,11 @@ class CodeIndex {
|
|
|
366
429
|
async dependents({ file, offset, limit, count = false }) {
|
|
367
430
|
await this._ensureReady();
|
|
368
431
|
const graph = this.cache.getGraph();
|
|
369
|
-
if (!graph || !graph.hasNode(file))
|
|
432
|
+
if (!graph || !graph.hasNode(file)) {
|
|
433
|
+
const suggestions = findSimilarFiles(graph, file);
|
|
434
|
+
if (count) return { total: 0, fileNotFound: true, suggestions };
|
|
435
|
+
return { items: [], fileNotFound: true, suggestions };
|
|
436
|
+
}
|
|
370
437
|
|
|
371
438
|
const fileScores = this._getFileScores();
|
|
372
439
|
|
|
@@ -420,9 +487,10 @@ class CodeIndex {
|
|
|
420
487
|
await this._ensureReady();
|
|
421
488
|
const graph = this.cache.getGraph();
|
|
422
489
|
if (!graph || !graph.hasNode(file)) {
|
|
490
|
+
const suggestions = findSimilarFiles(graph, file);
|
|
423
491
|
return count
|
|
424
|
-
? { totalFiles: 0, totalSymbols: 0, totalEdges: 0 }
|
|
425
|
-
: { files: [], symbols: [], edges: [] };
|
|
492
|
+
? { totalFiles: 0, totalSymbols: 0, totalEdges: 0, fileNotFound: true, suggestions }
|
|
493
|
+
: { files: [], symbols: [], edges: [], fileNotFound: true, suggestions };
|
|
426
494
|
}
|
|
427
495
|
|
|
428
496
|
// BFS over file nodes, following outgoing IMPORTS edges (dependencies)
|
|
@@ -567,6 +635,61 @@ class CodeIndex {
|
|
|
567
635
|
};
|
|
568
636
|
}
|
|
569
637
|
|
|
638
|
+
/**
|
|
639
|
+
* File-level dependency graph for visualization.
|
|
640
|
+
* Returns nodes (files) ranked by PageRank and IMPORTS edges between them.
|
|
641
|
+
*
|
|
642
|
+
* @param {object} opts
|
|
643
|
+
* @param {number} [opts.limit] - Max nodes to return (default: 500)
|
|
644
|
+
* @returns {{ nodes: Array<{id, label, category, score}>, edges: Array<{source, target}> }}
|
|
645
|
+
*/
|
|
646
|
+
async graph({ limit = 500 } = {}) {
|
|
647
|
+
await this._ensureReady();
|
|
648
|
+
const graph = this.cache.getGraph();
|
|
649
|
+
if (!graph || graph.order === 0) {
|
|
650
|
+
return { nodes: [], edges: [] };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const fileScores = this._getFileScores();
|
|
654
|
+
|
|
655
|
+
// Collect file nodes with scores, sorted by PageRank
|
|
656
|
+
const fileEntries = [];
|
|
657
|
+
graph.forEachNode((node, attrs) => {
|
|
658
|
+
if (attrs.type !== 'file') return;
|
|
659
|
+
fileEntries.push({ id: node, score: fileScores.get(node) || 0 });
|
|
660
|
+
});
|
|
661
|
+
fileEntries.sort((a, b) => b.score - a.score);
|
|
662
|
+
|
|
663
|
+
// Cap to limit
|
|
664
|
+
const capped = fileEntries.slice(0, limit);
|
|
665
|
+
const cappedSet = new Set(capped.map(f => f.id));
|
|
666
|
+
|
|
667
|
+
// Build nodes with category (first path segment) and label (filename)
|
|
668
|
+
const nodes = capped.map(f => {
|
|
669
|
+
const parts = f.id.split('/');
|
|
670
|
+
const category = parts.length > 1 ? parts[0] : 'root';
|
|
671
|
+
const label = parts[parts.length - 1].replace(/\.[^.]+$/, '');
|
|
672
|
+
return { id: f.id, label, category, score: f.score };
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// Collect IMPORTS edges between capped files
|
|
676
|
+
const edges = [];
|
|
677
|
+
const edgeSet = new Set();
|
|
678
|
+
for (const f of capped) {
|
|
679
|
+
graph.forEachOutEdge(f.id, (_edge, attrs, source, target) => {
|
|
680
|
+
if (attrs.type !== 'IMPORTS') return;
|
|
681
|
+
if (!cappedSet.has(target)) return;
|
|
682
|
+
const key = `${source}->${target}`;
|
|
683
|
+
if (!edgeSet.has(key)) {
|
|
684
|
+
edgeSet.add(key);
|
|
685
|
+
edges.push({ source, target });
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return { nodes, edges };
|
|
691
|
+
}
|
|
692
|
+
|
|
570
693
|
/**
|
|
571
694
|
* Force a full rebuild.
|
|
572
695
|
*/
|