opencode-forge 0.1.5
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 +21 -0
- package/README.md +534 -0
- package/config.jsonc +47 -0
- package/dist/agents/architect.d.ts +3 -0
- package/dist/agents/architect.d.ts.map +1 -0
- package/dist/agents/architect.js +152 -0
- package/dist/agents/architect.js.map +1 -0
- package/dist/agents/auditor.d.ts +3 -0
- package/dist/agents/auditor.d.ts.map +1 -0
- package/dist/agents/auditor.js +168 -0
- package/dist/agents/auditor.js.map +1 -0
- package/dist/agents/code.d.ts +3 -0
- package/dist/agents/code.d.ts.map +1 -0
- package/dist/agents/code.js +67 -0
- package/dist/agents/code.js.map +1 -0
- package/dist/agents/index.d.ts +4 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +9 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/prompts.d.ts +1 -0
- package/dist/agents/prompts.d.ts.map +1 -0
- package/dist/agents/prompts.js +4 -0
- package/dist/agents/prompts.js.map +1 -0
- package/dist/agents/types.d.ts +34 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/cache/index.d.ts +4 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +5 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/memory-cache.d.ts +14 -0
- package/dist/cache/memory-cache.d.ts.map +1 -0
- package/dist/cache/memory-cache.js +51 -0
- package/dist/cache/memory-cache.js.map +1 -0
- package/dist/cache/types.d.ts +8 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +2 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/cli/commands/cancel.d.ts +15 -0
- package/dist/cli/commands/cancel.d.ts.map +1 -0
- package/dist/cli/commands/cancel.js +194 -0
- package/dist/cli/commands/cancel.js.map +1 -0
- package/dist/cli/commands/graph.d.ts +16 -0
- package/dist/cli/commands/graph.d.ts.map +1 -0
- package/dist/cli/commands/graph.js +208 -0
- package/dist/cli/commands/graph.js.map +1 -0
- package/dist/cli/commands/restart.d.ts +15 -0
- package/dist/cli/commands/restart.d.ts.map +1 -0
- package/dist/cli/commands/restart.js +268 -0
- package/dist/cli/commands/restart.js.map +1 -0
- package/dist/cli/commands/status.d.ts +17 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +356 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/upgrade.d.ts +3 -0
- package/dist/cli/commands/upgrade.d.ts.map +1 -0
- package/dist/cli/commands/upgrade.js +40 -0
- package/dist/cli/commands/upgrade.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +224 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils.d.ts +36 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +163 -0
- package/dist/cli/utils.js.map +1 -0
- package/dist/command/template/review.txt +101 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +186 -0
- package/dist/config.js.map +1 -0
- package/dist/constants/loop.d.ts +10 -0
- package/dist/constants/loop.d.ts.map +1 -0
- package/dist/constants/loop.js +6 -0
- package/dist/constants/loop.js.map +1 -0
- package/dist/graph/cache.d.ts +17 -0
- package/dist/graph/cache.d.ts.map +1 -0
- package/dist/graph/cache.js +50 -0
- package/dist/graph/cache.js.map +1 -0
- package/dist/graph/client.d.ts +51 -0
- package/dist/graph/client.d.ts.map +1 -0
- package/dist/graph/client.js +152 -0
- package/dist/graph/client.js.map +1 -0
- package/dist/graph/clone-detection.d.ts +9 -0
- package/dist/graph/clone-detection.d.ts.map +1 -0
- package/dist/graph/clone-detection.js +148 -0
- package/dist/graph/clone-detection.js.map +1 -0
- package/dist/graph/constants.d.ts +18 -0
- package/dist/graph/constants.d.ts.map +1 -0
- package/dist/graph/constants.js +532 -0
- package/dist/graph/constants.js.map +1 -0
- package/dist/graph/database.d.ts +11 -0
- package/dist/graph/database.d.ts.map +1 -0
- package/dist/graph/database.js +250 -0
- package/dist/graph/database.js.map +1 -0
- package/dist/graph/index.d.ts +14 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +13 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/repo-map.d.ts +59 -0
- package/dist/graph/repo-map.d.ts.map +1 -0
- package/dist/graph/repo-map.js +948 -0
- package/dist/graph/repo-map.js.map +1 -0
- package/dist/graph/rpc.d.ts +34 -0
- package/dist/graph/rpc.d.ts.map +1 -0
- package/dist/graph/rpc.js +139 -0
- package/dist/graph/rpc.js.map +1 -0
- package/dist/graph/service.d.ts +46 -0
- package/dist/graph/service.d.ts.map +1 -0
- package/dist/graph/service.js +329 -0
- package/dist/graph/service.js.map +1 -0
- package/dist/graph/tree-sitter.d.ts +40 -0
- package/dist/graph/tree-sitter.d.ts.map +1 -0
- package/dist/graph/tree-sitter.js +799 -0
- package/dist/graph/tree-sitter.js.map +1 -0
- package/dist/graph/types.d.ts +175 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +105 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/graph/utils.d.ts +64 -0
- package/dist/graph/utils.d.ts.map +1 -0
- package/dist/graph/utils.js +406 -0
- package/dist/graph/utils.js.map +1 -0
- package/dist/graph/worker.d.ts +2 -0
- package/dist/graph/worker.d.ts.map +1 -0
- package/dist/graph/worker.js +6043 -0
- package/dist/graph/worker.js.map +1 -0
- package/dist/hooks/compaction-utils.d.ts +21 -0
- package/dist/hooks/compaction-utils.d.ts.map +1 -0
- package/dist/hooks/compaction-utils.js +82 -0
- package/dist/hooks/compaction-utils.js.map +1 -0
- package/dist/hooks/graph-command.d.ts +27 -0
- package/dist/hooks/graph-command.d.ts.map +1 -0
- package/dist/hooks/graph-command.js +57 -0
- package/dist/hooks/graph-command.js.map +1 -0
- package/dist/hooks/graph-tools.d.ts +11 -0
- package/dist/hooks/graph-tools.d.ts.map +1 -0
- package/dist/hooks/graph-tools.js +125 -0
- package/dist/hooks/graph-tools.js.map +1 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/loop.d.ts +23 -0
- package/dist/hooks/loop.d.ts.map +1 -0
- package/dist/hooks/loop.js +667 -0
- package/dist/hooks/loop.js.map +1 -0
- package/dist/hooks/sandbox-tools.d.ts +13 -0
- package/dist/hooks/sandbox-tools.d.ts.map +1 -0
- package/dist/hooks/sandbox-tools.js +105 -0
- package/dist/hooks/sandbox-tools.js.map +1 -0
- package/dist/hooks/session.d.ts +19 -0
- package/dist/hooks/session.d.ts.map +1 -0
- package/dist/hooks/session.js +56 -0
- package/dist/hooks/session.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +298 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox/context.d.ts +27 -0
- package/dist/sandbox/context.d.ts.map +1 -0
- package/dist/sandbox/context.js +18 -0
- package/dist/sandbox/context.js.map +1 -0
- package/dist/sandbox/docker.d.ts +29 -0
- package/dist/sandbox/docker.d.ts.map +1 -0
- package/dist/sandbox/docker.js +213 -0
- package/dist/sandbox/docker.js.map +1 -0
- package/dist/sandbox/manager.d.ts +23 -0
- package/dist/sandbox/manager.d.ts.map +1 -0
- package/dist/sandbox/manager.js +131 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/sandbox/path.d.ts +4 -0
- package/dist/sandbox/path.d.ts.map +1 -0
- package/dist/sandbox/path.js +27 -0
- package/dist/sandbox/path.js.map +1 -0
- package/dist/services/kv.d.ts +17 -0
- package/dist/services/kv.d.ts.map +1 -0
- package/dist/services/kv.js +62 -0
- package/dist/services/kv.js.map +1 -0
- package/dist/services/loop.d.ts +96 -0
- package/dist/services/loop.d.ts.map +1 -0
- package/dist/services/loop.js +315 -0
- package/dist/services/loop.js.map +1 -0
- package/dist/setup.d.ts +4 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +118 -0
- package/dist/setup.js.map +1 -0
- package/dist/storage/database.d.ts +6 -0
- package/dist/storage/database.d.ts.map +1 -0
- package/dist/storage/database.js +90 -0
- package/dist/storage/database.js.map +1 -0
- package/dist/storage/graph-projects.d.ts +80 -0
- package/dist/storage/graph-projects.d.ts.map +1 -0
- package/dist/storage/graph-projects.js +154 -0
- package/dist/storage/graph-projects.js.map +1 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +3 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/kv-queries.d.ts +18 -0
- package/dist/storage/kv-queries.d.ts.map +1 -0
- package/dist/storage/kv-queries.js +70 -0
- package/dist/storage/kv-queries.js.map +1 -0
- package/dist/tools/graph.d.ts +9 -0
- package/dist/tools/graph.d.ts.map +1 -0
- package/dist/tools/graph.js +272 -0
- package/dist/tools/graph.js.map +1 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +16 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/loop.d.ts +21 -0
- package/dist/tools/loop.d.ts.map +1 -0
- package/dist/tools/loop.js +570 -0
- package/dist/tools/loop.js.map +1 -0
- package/dist/tools/plan-approval.d.ts +15 -0
- package/dist/tools/plan-approval.d.ts.map +1 -0
- package/dist/tools/plan-approval.js +203 -0
- package/dist/tools/plan-approval.js.map +1 -0
- package/dist/tools/plan-execute.d.ts +4 -0
- package/dist/tools/plan-execute.d.ts.map +1 -0
- package/dist/tools/plan-execute.js +85 -0
- package/dist/tools/plan-execute.js.map +1 -0
- package/dist/tools/plan-kv.d.ts +4 -0
- package/dist/tools/plan-kv.d.ts.map +1 -0
- package/dist/tools/plan-kv.js +107 -0
- package/dist/tools/plan-kv.js.map +1 -0
- package/dist/tools/review.d.ts +4 -0
- package/dist/tools/review.d.ts.map +1 -0
- package/dist/tools/review.js +90 -0
- package/dist/tools/review.js.map +1 -0
- package/dist/tools/sandbox-fs.d.ts +22 -0
- package/dist/tools/sandbox-fs.d.ts.map +1 -0
- package/dist/tools/sandbox-fs.js +83 -0
- package/dist/tools/sandbox-fs.js.map +1 -0
- package/dist/tools/types.d.ts +26 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.js +2061 -0
- package/dist/types.d.ts +124 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/git-branch.d.ts +11 -0
- package/dist/utils/git-branch.d.ts.map +1 -0
- package/dist/utils/git-branch.js +35 -0
- package/dist/utils/git-branch.js.map +1 -0
- package/dist/utils/graph-status-store.d.ts +72 -0
- package/dist/utils/graph-status-store.d.ts.map +1 -0
- package/dist/utils/graph-status-store.js +62 -0
- package/dist/utils/graph-status-store.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/loop-format.d.ts +5 -0
- package/dist/utils/loop-format.d.ts.map +1 -0
- package/dist/utils/loop-format.js +29 -0
- package/dist/utils/loop-format.js.map +1 -0
- package/dist/utils/loop-helpers.d.ts +9 -0
- package/dist/utils/loop-helpers.d.ts.map +1 -0
- package/dist/utils/loop-helpers.js +20 -0
- package/dist/utils/loop-helpers.js.map +1 -0
- package/dist/utils/loop-launch.d.ts +32 -0
- package/dist/utils/loop-launch.d.ts.map +1 -0
- package/dist/utils/loop-launch.js +162 -0
- package/dist/utils/loop-launch.js.map +1 -0
- package/dist/utils/model-fallback.d.ts +27 -0
- package/dist/utils/model-fallback.d.ts.map +1 -0
- package/dist/utils/model-fallback.js +33 -0
- package/dist/utils/model-fallback.js.map +1 -0
- package/dist/utils/partial-match.d.ts +7 -0
- package/dist/utils/partial-match.d.ts.map +1 -0
- package/dist/utils/partial-match.js +56 -0
- package/dist/utils/partial-match.js.map +1 -0
- package/dist/utils/plan-execution.d.ts +65 -0
- package/dist/utils/plan-execution.d.ts.map +1 -0
- package/dist/utils/plan-execution.js +107 -0
- package/dist/utils/plan-execution.js.map +1 -0
- package/dist/utils/session-stats.d.ts +36 -0
- package/dist/utils/session-stats.d.ts.map +1 -0
- package/dist/utils/session-stats.js +145 -0
- package/dist/utils/session-stats.js.map +1 -0
- package/dist/utils/tui-graph-status.d.ts +38 -0
- package/dist/utils/tui-graph-status.d.ts.map +1 -0
- package/dist/utils/tui-graph-status.js +95 -0
- package/dist/utils/tui-graph-status.js.map +1 -0
- package/dist/utils/tui-plan-store.d.ts +54 -0
- package/dist/utils/tui-plan-store.d.ts.map +1 -0
- package/dist/utils/tui-plan-store.js +168 -0
- package/dist/utils/tui-plan-store.js.map +1 -0
- package/dist/utils/tui-refresh-helpers.d.ts +44 -0
- package/dist/utils/tui-refresh-helpers.d.ts.map +1 -0
- package/dist/utils/tui-refresh-helpers.js +120 -0
- package/dist/utils/tui-refresh-helpers.js.map +1 -0
- package/dist/utils/upgrade.d.ts +23 -0
- package/dist/utils/upgrade.d.ts.map +1 -0
- package/dist/utils/upgrade.js +111 -0
- package/dist/utils/upgrade.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +92 -0
- package/scripts/build.ts +67 -0
- package/src/command/template/review.txt +101 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
// Core RepoMap implementation - ported from soulforge
|
|
2
|
+
import { resolve, join, dirname, extname, relative } from 'path';
|
|
3
|
+
import { existsSync, statSync } from 'fs';
|
|
4
|
+
import { TreeSitterBackend } from './tree-sitter';
|
|
5
|
+
import { FileCache } from './cache';
|
|
6
|
+
import { tokenize, computeMinHash, computeFragmentHashes, jaccardSimilarity } from './clone-detection';
|
|
7
|
+
import { INDEXABLE_EXTENSIONS, PAGERANK_ITERATIONS, PAGERANK_DAMPING, MAX_INDEXED_FILES, } from './constants';
|
|
8
|
+
import { isBarrelFile, kindTag, collectFilesAsync, extractSignature } from './utils';
|
|
9
|
+
export class RepoMap {
|
|
10
|
+
db;
|
|
11
|
+
cwd;
|
|
12
|
+
treeSitter;
|
|
13
|
+
cache;
|
|
14
|
+
stmts = {};
|
|
15
|
+
maxFiles;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.cwd = resolve(config.cwd);
|
|
18
|
+
this.db = config.db;
|
|
19
|
+
this.maxFiles = config.maxFiles ?? MAX_INDEXED_FILES;
|
|
20
|
+
this.treeSitter = new TreeSitterBackend();
|
|
21
|
+
this.cache = new FileCache(200);
|
|
22
|
+
this.treeSitter.setCache(this.cache);
|
|
23
|
+
this.prepareStatements();
|
|
24
|
+
}
|
|
25
|
+
prepareStatements() {
|
|
26
|
+
this.stmts = {
|
|
27
|
+
getFileById: this.db.prepare('SELECT * FROM files WHERE id = ?'),
|
|
28
|
+
getFileByPath: this.db.prepare('SELECT * FROM files WHERE path = ?'),
|
|
29
|
+
getSymbolsByFileId: this.db.prepare('SELECT * FROM symbols WHERE file_id = ?'),
|
|
30
|
+
getRefsByFileId: this.db.prepare('SELECT * FROM refs WHERE file_id = ?'),
|
|
31
|
+
getEdgesBySource: this.db.prepare('SELECT * FROM edges WHERE source_file_id = ?'),
|
|
32
|
+
getEdgesByTarget: this.db.prepare('SELECT * FROM edges WHERE target_file_id = ?'),
|
|
33
|
+
getAllFiles: this.db.prepare('SELECT * FROM files ORDER BY pagerank DESC'),
|
|
34
|
+
getAllSymbols: this.db.prepare('SELECT * FROM symbols'),
|
|
35
|
+
getAllEdges: this.db.prepare('SELECT * FROM edges'),
|
|
36
|
+
getAllRefs: this.db.prepare('SELECT * FROM refs'),
|
|
37
|
+
insertFile: this.db.prepare(`
|
|
38
|
+
INSERT OR REPLACE INTO files (path, mtime_ms, language, line_count, symbol_count, pagerank, is_barrel, indexed_at)
|
|
39
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
40
|
+
`),
|
|
41
|
+
insertSymbol: this.db.prepare(`
|
|
42
|
+
INSERT INTO symbols (file_id, name, kind, line, end_line, is_exported, signature, qualified_name)
|
|
43
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
44
|
+
`),
|
|
45
|
+
insertRef: this.db.prepare(`
|
|
46
|
+
INSERT INTO refs (file_id, name, source_file_id, import_source)
|
|
47
|
+
VALUES (?, ?, ?, ?)
|
|
48
|
+
`),
|
|
49
|
+
insertEdge: this.db.prepare(`
|
|
50
|
+
INSERT OR REPLACE INTO edges (source_file_id, target_file_id, weight, confidence)
|
|
51
|
+
VALUES (?, ?, ?, ?)
|
|
52
|
+
`),
|
|
53
|
+
insertCoChange: this.db.prepare(`
|
|
54
|
+
INSERT OR REPLACE INTO cochanges (file_id_a, file_id_b, count)
|
|
55
|
+
VALUES (?, ?, ?)
|
|
56
|
+
`),
|
|
57
|
+
deleteFile: this.db.prepare('DELETE FROM files WHERE id = ?'),
|
|
58
|
+
deleteRefsByFileId: this.db.prepare('DELETE FROM refs WHERE file_id = ?'),
|
|
59
|
+
deleteEdgesBySource: this.db.prepare('DELETE FROM edges WHERE source_file_id = ?'),
|
|
60
|
+
deleteEdgesByTarget: this.db.prepare('DELETE FROM edges WHERE target_file_id = ?'),
|
|
61
|
+
deleteSymbolsByFileId: this.db.prepare('DELETE FROM symbols WHERE file_id = ?'),
|
|
62
|
+
deleteShapeHashesByFileId: this.db.prepare('DELETE FROM shape_hashes WHERE file_id = ?'),
|
|
63
|
+
deleteTokenSignaturesByFileId: this.db.prepare('DELETE FROM token_signatures WHERE file_id = ?'),
|
|
64
|
+
deleteTokenFragmentsByFileId: this.db.prepare('DELETE FROM token_fragments WHERE file_id = ?'),
|
|
65
|
+
deleteExternalImportsByFileId: this.db.prepare('DELETE FROM external_imports WHERE file_id = ?'),
|
|
66
|
+
getCounts: this.db.prepare(`
|
|
67
|
+
SELECT
|
|
68
|
+
(SELECT COUNT(*) FROM files) as files,
|
|
69
|
+
(SELECT COUNT(*) FROM symbols) as symbols,
|
|
70
|
+
(SELECT COUNT(*) FROM edges) as edges
|
|
71
|
+
`),
|
|
72
|
+
// Queries for dependents/dependencies
|
|
73
|
+
getEdgesByTargetFile: this.db.prepare('SELECT * FROM edges WHERE target_file_id = ?'),
|
|
74
|
+
getEdgesBySourceFile: this.db.prepare('SELECT * FROM edges WHERE source_file_id = ?'),
|
|
75
|
+
// Query for blast radius
|
|
76
|
+
getEdgesTargetIds: this.db.prepare('SELECT target_file_id FROM edges WHERE source_file_id = ?'),
|
|
77
|
+
// FTS search
|
|
78
|
+
searchSymbolsFtsQuery: this.db.prepare(`
|
|
79
|
+
SELECT s.name, f.path, s.kind, s.line, s.is_exported AS isExported, f.pagerank, s.id
|
|
80
|
+
FROM symbols_fts ft
|
|
81
|
+
JOIN symbols s ON ft.rowid = s.id
|
|
82
|
+
JOIN files f ON s.file_id = f.id
|
|
83
|
+
WHERE symbols_fts MATCH ?
|
|
84
|
+
ORDER BY rank
|
|
85
|
+
LIMIT ?
|
|
86
|
+
`),
|
|
87
|
+
// Call graph queries
|
|
88
|
+
getSymbolByFileAndLine: this.db.prepare('SELECT id, name, kind, line, signature FROM symbols WHERE file_id = ? AND line = ? LIMIT 1'),
|
|
89
|
+
getCallersQuery: this.db.prepare(`
|
|
90
|
+
SELECT s.name as caller_name, f.path as caller_path, s.line as caller_line, c.line as call_line
|
|
91
|
+
FROM calls c
|
|
92
|
+
JOIN symbols s ON c.caller_symbol_id = s.id
|
|
93
|
+
JOIN files f ON s.file_id = f.id
|
|
94
|
+
WHERE c.callee_name = ? AND (c.callee_file_id IS NULL OR c.callee_file_id = ?)
|
|
95
|
+
`),
|
|
96
|
+
getCalleesQuery: this.db.prepare(`
|
|
97
|
+
SELECT c.callee_name, f.path as callee_file, c.line as call_line, s.line as callee_def_line
|
|
98
|
+
FROM calls c
|
|
99
|
+
JOIN files f ON c.callee_file_id = f.id
|
|
100
|
+
JOIN symbols s ON c.callee_symbol_id = s.id
|
|
101
|
+
WHERE c.caller_symbol_id = ?
|
|
102
|
+
`),
|
|
103
|
+
// Co-changes
|
|
104
|
+
getCoChanges: this.db.prepare(`
|
|
105
|
+
SELECT
|
|
106
|
+
CASE WHEN file_id_a = ? THEN file_id_b ELSE file_id_a END as other_id,
|
|
107
|
+
count
|
|
108
|
+
FROM cochanges
|
|
109
|
+
WHERE file_id_a = ? OR file_id_b = ?
|
|
110
|
+
ORDER BY count DESC
|
|
111
|
+
LIMIT 20
|
|
112
|
+
`),
|
|
113
|
+
// File symbols query
|
|
114
|
+
getFileSymbolsQuery: this.db.prepare('SELECT * FROM symbols WHERE file_id = ?'),
|
|
115
|
+
// Resolve unresolved refs
|
|
116
|
+
getUnresolvedRefs: this.db.prepare('SELECT * FROM refs WHERE source_file_id IS NULL'),
|
|
117
|
+
resolveRefMatch: this.db.prepare(`
|
|
118
|
+
SELECT s.id, s.file_id, f.path
|
|
119
|
+
FROM symbols s
|
|
120
|
+
JOIN files f ON s.file_id = f.id
|
|
121
|
+
WHERE s.name = ? AND s.is_exported = 1
|
|
122
|
+
`),
|
|
123
|
+
// Test files
|
|
124
|
+
getTestFiles: this.db.prepare(`
|
|
125
|
+
SELECT id, path FROM files
|
|
126
|
+
WHERE path LIKE '%.test.%' OR path LIKE '%_test.%' OR path LIKE '%.spec.%'
|
|
127
|
+
`),
|
|
128
|
+
// Build call graph helpers - include files with any refs (resolved or unresolved)
|
|
129
|
+
getFilesWithImports: this.db.prepare(`
|
|
130
|
+
SELECT DISTINCT f.id, f.path FROM files f
|
|
131
|
+
WHERE EXISTS (SELECT 1 FROM symbols s WHERE s.file_id = f.id AND s.kind IN ('function', 'method'))
|
|
132
|
+
AND EXISTS (SELECT 1 FROM refs r WHERE r.file_id = f.id AND r.name != '*')
|
|
133
|
+
`),
|
|
134
|
+
getImportsForFile: this.db.prepare(`
|
|
135
|
+
SELECT DISTINCT r.name, r.source_file_id FROM refs r
|
|
136
|
+
WHERE r.file_id = ? AND r.source_file_id IS NOT NULL AND r.name != '*'
|
|
137
|
+
`),
|
|
138
|
+
getFunctionsForFile: this.db.prepare(`
|
|
139
|
+
SELECT id, name, line, end_line FROM symbols
|
|
140
|
+
WHERE file_id = ? AND kind IN ('function', 'method') AND end_line > line
|
|
141
|
+
`),
|
|
142
|
+
resolveCallee: this.db.prepare(`
|
|
143
|
+
SELECT id FROM symbols WHERE file_id = ? AND name = ? AND is_exported = 1 LIMIT 1
|
|
144
|
+
`),
|
|
145
|
+
insertCall: this.db.prepare(`
|
|
146
|
+
INSERT INTO calls (caller_symbol_id, callee_name, callee_symbol_id, callee_file_id, line)
|
|
147
|
+
VALUES (?, ?, ?, ?, ?)
|
|
148
|
+
`),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async initialize() {
|
|
152
|
+
try {
|
|
153
|
+
await this.treeSitter.initialize(this.cwd);
|
|
154
|
+
this.initSchema();
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
console.error('Failed to initialize RepoMap:', err);
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
initSchema() {
|
|
162
|
+
this.db.run(`
|
|
163
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
164
|
+
id INTEGER PRIMARY KEY,
|
|
165
|
+
version INTEGER NOT NULL
|
|
166
|
+
)
|
|
167
|
+
`);
|
|
168
|
+
const version = this.db.prepare('SELECT version FROM schema_version ORDER BY id DESC LIMIT 1').get();
|
|
169
|
+
if (!version || version.version < 1) {
|
|
170
|
+
this.db.run('INSERT INTO schema_version (version) VALUES (1)');
|
|
171
|
+
}
|
|
172
|
+
// Only populate FTS if symbols table exists
|
|
173
|
+
try {
|
|
174
|
+
const ftsCount = this.db.prepare('SELECT COUNT(*) as c FROM symbols_fts').get();
|
|
175
|
+
if (!ftsCount || ftsCount.c === 0) {
|
|
176
|
+
const symbols = this.stmts.getAllSymbols.all();
|
|
177
|
+
for (const sym of symbols) {
|
|
178
|
+
const file = this.stmts.getFileById.get(sym.file_id);
|
|
179
|
+
if (file) {
|
|
180
|
+
try {
|
|
181
|
+
this.db.run('INSERT INTO symbols_fts (rowid, name, path) VALUES (?, ?, ?)', [sym.id, sym.name, file.path]);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// FTS insert may fail
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// FTS table may not exist yet - that's ok, it will be created by database.ts
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async scan() {
|
|
195
|
+
const result = await collectFilesAsync(this.cwd);
|
|
196
|
+
const files = result.files.slice(0, this.maxFiles);
|
|
197
|
+
console.log(`Scanning ${files.length} files...`);
|
|
198
|
+
for (const file of files) {
|
|
199
|
+
try {
|
|
200
|
+
const relPath = relative(this.cwd, file.path);
|
|
201
|
+
await this.indexFile(relPath);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
console.error(`Error indexing ${file.path}:`, err);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
console.log('Resolving unresolved refs...');
|
|
208
|
+
await this.resolveUnresolvedRefs();
|
|
209
|
+
console.log('Building edges...');
|
|
210
|
+
await this.buildEdges();
|
|
211
|
+
console.log('Computing PageRank...');
|
|
212
|
+
await this.computePageRank();
|
|
213
|
+
console.log('Linking test files...');
|
|
214
|
+
this.linkTestFiles();
|
|
215
|
+
console.log('Building call graph...');
|
|
216
|
+
await this.buildCallGraph();
|
|
217
|
+
console.log('Building co-changes...');
|
|
218
|
+
await this.buildCoChanges();
|
|
219
|
+
console.log('Scan complete.');
|
|
220
|
+
}
|
|
221
|
+
async indexFile(filePath) {
|
|
222
|
+
// Normalize path against cwd - handle both absolute and relative paths
|
|
223
|
+
const absPath = filePath.startsWith('/') ? filePath : resolve(this.cwd, filePath);
|
|
224
|
+
const relPath = relative(this.cwd, absPath);
|
|
225
|
+
const ext = extname(absPath).toLowerCase();
|
|
226
|
+
if (!(ext in INDEXABLE_EXTENSIONS))
|
|
227
|
+
return;
|
|
228
|
+
let stats;
|
|
229
|
+
try {
|
|
230
|
+
stats = statSync(absPath);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (stats.size > 500_000)
|
|
236
|
+
return;
|
|
237
|
+
// Use absolute path for tree-sitter to ensure consistent caching
|
|
238
|
+
const outline = await this.treeSitter.getFileOutline(absPath);
|
|
239
|
+
if (!outline)
|
|
240
|
+
return;
|
|
241
|
+
const existing = this.stmts.getFileByPath.get(relPath);
|
|
242
|
+
if (existing && existing.mtime_ms === stats.mtimeMs) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (existing) {
|
|
246
|
+
this.stmts.deleteRefsByFileId.run([existing.id]);
|
|
247
|
+
this.stmts.deleteEdgesBySource.run([existing.id]);
|
|
248
|
+
this.stmts.deleteEdgesByTarget.run([existing.id]);
|
|
249
|
+
this.stmts.deleteSymbolsByFileId.run([existing.id]);
|
|
250
|
+
this.stmts.deleteShapeHashesByFileId.run([existing.id]);
|
|
251
|
+
this.stmts.deleteTokenSignaturesByFileId.run([existing.id]);
|
|
252
|
+
this.stmts.deleteTokenFragmentsByFileId.run([existing.id]);
|
|
253
|
+
this.stmts.deleteFile.run([existing.id]);
|
|
254
|
+
}
|
|
255
|
+
const isBarrel = isBarrelFile(relPath);
|
|
256
|
+
const lineCount = outline.symbols.length > 0
|
|
257
|
+
? Math.max(...outline.symbols.map(s => s.location.endLine || s.location.line))
|
|
258
|
+
: 1;
|
|
259
|
+
const fileId = this.db.run('INSERT INTO files (path, mtime_ms, language, line_count, symbol_count, pagerank, is_barrel, indexed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [relPath, stats.mtimeMs, outline.language, lineCount, outline.symbols.length, 0, isBarrel ? 1 : 0, Date.now()]).lastInsertRowid;
|
|
260
|
+
// Extract signatures using the utility function
|
|
261
|
+
const { readFile } = await import('fs/promises');
|
|
262
|
+
const content = await readFile(absPath, 'utf-8');
|
|
263
|
+
const lines = content.split('\n');
|
|
264
|
+
// Deduplicate symbols by line and name to prevent duplicate DB rows
|
|
265
|
+
// This is a second line of defense after tree-sitter deduplication
|
|
266
|
+
const seenSymbols = new Set();
|
|
267
|
+
for (const sym of outline.symbols) {
|
|
268
|
+
const key = `${sym.location.line}-${sym.name}-${sym.kind}`;
|
|
269
|
+
if (seenSymbols.has(key))
|
|
270
|
+
continue;
|
|
271
|
+
seenSymbols.add(key);
|
|
272
|
+
const signature = extractSignature(lines, sym.location.line - 1, sym.kind);
|
|
273
|
+
this.stmts.insertSymbol.run([
|
|
274
|
+
fileId,
|
|
275
|
+
sym.name,
|
|
276
|
+
sym.kind,
|
|
277
|
+
sym.location.line,
|
|
278
|
+
sym.location.endLine || sym.location.line,
|
|
279
|
+
outline.exports.some(e => e.name === sym.name) ? 1 : 0,
|
|
280
|
+
signature || null,
|
|
281
|
+
sym.name
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
284
|
+
// Process imports - distinguish internal vs external
|
|
285
|
+
const externalImports = [];
|
|
286
|
+
for (const imp of outline.imports) {
|
|
287
|
+
const isRelative = imp.source.startsWith('.') || imp.source.startsWith('/');
|
|
288
|
+
if (isRelative) {
|
|
289
|
+
// Internal relative import - resolve and track in refs
|
|
290
|
+
const resolvedSource = await this.resolveImportSource(imp.source, absPath);
|
|
291
|
+
let sourceFileId = null;
|
|
292
|
+
if (resolvedSource) {
|
|
293
|
+
const resolvedFile = this.stmts.getFileByPath.get(resolvedSource);
|
|
294
|
+
if (resolvedFile) {
|
|
295
|
+
sourceFileId = resolvedFile.id;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (const specifier of imp.specifiers) {
|
|
299
|
+
this.stmts.insertRef.run([fileId, specifier, sourceFileId, imp.source]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// External package import - track in external_imports
|
|
304
|
+
// Preserve scoped package names (e.g. @types/node, @opencode-ai/sdk)
|
|
305
|
+
let packageName;
|
|
306
|
+
if (imp.source.startsWith('@')) {
|
|
307
|
+
// Scoped package: take first two segments (@scope/name)
|
|
308
|
+
const parts = imp.source.split('/');
|
|
309
|
+
packageName = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
// Unscoped package: take first segment
|
|
313
|
+
packageName = imp.source.split('/')[0];
|
|
314
|
+
}
|
|
315
|
+
externalImports.push({ package: packageName, specifiers: imp.specifiers });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Insert external imports
|
|
319
|
+
for (const extImp of externalImports) {
|
|
320
|
+
this.db.run('INSERT INTO external_imports (file_id, package, specifiers) VALUES (?, ?, ?)', [fileId, extImp.package, extImp.specifiers.join(',')]);
|
|
321
|
+
}
|
|
322
|
+
const shapeHashes = await this.treeSitter.getShapeHashes(filePath);
|
|
323
|
+
if (shapeHashes) {
|
|
324
|
+
for (const hash of shapeHashes) {
|
|
325
|
+
this.db.run('INSERT INTO shape_hashes (file_id, name, kind, line, end_line, shape_hash, node_count) VALUES (?, ?, ?, ?, ?, ?, ?)', [fileId, hash.name, hash.kind, hash.line, hash.endLine, hash.shapeHash, hash.nodeCount]);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
// Use absolute path for cache lookup to match tree-sitter caching convention
|
|
330
|
+
const content = await this.cache.get(absPath) || '';
|
|
331
|
+
const tokens = tokenize(content);
|
|
332
|
+
const minhash = computeMinHash(tokens);
|
|
333
|
+
if (minhash) {
|
|
334
|
+
for (const sym of outline.symbols) {
|
|
335
|
+
const symMinhash = computeMinHash(tokens.slice(Math.floor((sym.location.line - 1) * tokens.length / lineCount), Math.floor((sym.location.endLine || sym.location.line) * tokens.length / lineCount)));
|
|
336
|
+
if (symMinhash) {
|
|
337
|
+
this.db.run('INSERT INTO token_signatures (file_id, name, line, end_line, minhash) VALUES (?, ?, ?, ?, ?)', [fileId, sym.name, sym.location.line, sym.location.endLine || sym.location.line, symMinhash]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const fragmentHashes = computeFragmentHashes(tokens);
|
|
341
|
+
for (const frag of fragmentHashes) {
|
|
342
|
+
this.db.run('INSERT INTO token_fragments (hash, file_id, name, line, token_offset) VALUES (?, ?, ?, ?, ?)', [frag.hash, fileId, '', 1, frag.tokenOffset]);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
console.debug('Token extraction failed for file:', filePath, err);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async resolveImportSource(importSource, fromFile) {
|
|
351
|
+
const fromDir = dirname(fromFile);
|
|
352
|
+
if (importSource.startsWith('.')) {
|
|
353
|
+
const resolved = resolve(fromDir, importSource);
|
|
354
|
+
if (existsSync(resolved))
|
|
355
|
+
return relative(this.cwd, resolved);
|
|
356
|
+
for (const ext of ['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '.py', '.go', '.rs']) {
|
|
357
|
+
if (existsSync(resolved + ext)) {
|
|
358
|
+
return relative(this.cwd, resolved + ext);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (const index of ['/index.ts', '/index.tsx', '/index.js', '/__init__.py']) {
|
|
362
|
+
if (existsSync(resolved + index)) {
|
|
363
|
+
return relative(this.cwd, resolved + index);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
async resolveUnresolvedRefs() {
|
|
371
|
+
const unresolved = this.stmts.getUnresolvedRefs.all();
|
|
372
|
+
for (const ref of unresolved) {
|
|
373
|
+
// Find matching exported symbols by name
|
|
374
|
+
// Prefer matches from the import source path if available
|
|
375
|
+
const matches = this.db.prepare(`
|
|
376
|
+
SELECT s.id, s.file_id, f.path
|
|
377
|
+
FROM symbols s
|
|
378
|
+
JOIN files f ON s.file_id = f.id
|
|
379
|
+
WHERE s.name = ? AND s.is_exported = 1
|
|
380
|
+
`).all(ref.name);
|
|
381
|
+
if (matches.length >= 1) {
|
|
382
|
+
// If we have an import source, try to match by path first
|
|
383
|
+
if (ref.import_source) {
|
|
384
|
+
const pathMatch = matches.find(m => {
|
|
385
|
+
const importPath = ref.import_source.startsWith('.')
|
|
386
|
+
? ref.import_source
|
|
387
|
+
: ref.import_source;
|
|
388
|
+
return m.path === importPath || m.path.endsWith(importPath);
|
|
389
|
+
});
|
|
390
|
+
if (pathMatch) {
|
|
391
|
+
this.db.run('UPDATE refs SET source_file_id = ? WHERE id = ?', [pathMatch.file_id, ref.id]);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Fall back to first match by name (best effort for ambiguous cases)
|
|
396
|
+
this.db.run('UPDATE refs SET source_file_id = ? WHERE id = ?', [matches[0].file_id, ref.id]);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async buildEdges() {
|
|
401
|
+
const refs = this.stmts.getAllRefs.all();
|
|
402
|
+
const edgeMap = new Map();
|
|
403
|
+
for (const ref of refs) {
|
|
404
|
+
if (ref.source_file_id) {
|
|
405
|
+
const key = `${ref.file_id}-${ref.source_file_id}`;
|
|
406
|
+
const existing = edgeMap.get(key);
|
|
407
|
+
if (existing) {
|
|
408
|
+
edgeMap.set(key, { weight: existing.weight + 1, confidence: existing.confidence });
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
edgeMap.set(key, { weight: 1, confidence: 1 });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (const [key, data] of edgeMap) {
|
|
416
|
+
const [source, target] = key.split('-').map(Number);
|
|
417
|
+
const idf = Math.log(2);
|
|
418
|
+
const dampenedWeight = data.weight * idf;
|
|
419
|
+
this.stmts.insertEdge.run([source, target, dampenedWeight, data.confidence]);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async computePageRank() {
|
|
423
|
+
const files = this.stmts.getAllFiles.all();
|
|
424
|
+
const n = files.length;
|
|
425
|
+
if (n === 0)
|
|
426
|
+
return;
|
|
427
|
+
const damping = PAGERANK_DAMPING;
|
|
428
|
+
const iterations = PAGERANK_ITERATIONS;
|
|
429
|
+
const ranks = new Map();
|
|
430
|
+
for (const file of files) {
|
|
431
|
+
ranks.set(file.id, 1 / n);
|
|
432
|
+
}
|
|
433
|
+
const edges = this.stmts.getAllEdges.all();
|
|
434
|
+
const outgoing = new Map();
|
|
435
|
+
const incoming = new Map();
|
|
436
|
+
for (const edge of edges) {
|
|
437
|
+
outgoing.set(edge.source_file_id, (outgoing.get(edge.source_file_id) || 0) + edge.weight);
|
|
438
|
+
if (!incoming.has(edge.target_file_id)) {
|
|
439
|
+
incoming.set(edge.target_file_id, []);
|
|
440
|
+
}
|
|
441
|
+
incoming.get(edge.target_file_id).push(edge);
|
|
442
|
+
}
|
|
443
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
444
|
+
const newRanks = new Map();
|
|
445
|
+
for (const file of files) {
|
|
446
|
+
let rank = (1 - damping) / n;
|
|
447
|
+
const incomingEdges = incoming.get(file.id) || [];
|
|
448
|
+
for (const edge of incomingEdges) {
|
|
449
|
+
const outWeight = outgoing.get(edge.source_file_id) || 1;
|
|
450
|
+
const sourceRank = ranks.get(edge.source_file_id) || 0;
|
|
451
|
+
rank += damping * (sourceRank * edge.weight / outWeight);
|
|
452
|
+
}
|
|
453
|
+
newRanks.set(file.id, rank);
|
|
454
|
+
}
|
|
455
|
+
ranks.clear();
|
|
456
|
+
for (const [k, v] of newRanks) {
|
|
457
|
+
ranks.set(k, v);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (const file of files) {
|
|
461
|
+
const rank = ranks.get(file.id) || 0;
|
|
462
|
+
this.db.run('UPDATE files SET pagerank = ? WHERE id = ?', [rank, file.id]);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async computePageRankSync() {
|
|
466
|
+
await this.computePageRank();
|
|
467
|
+
}
|
|
468
|
+
async render(opts) {
|
|
469
|
+
const maxFiles = opts?.maxFiles ?? 20;
|
|
470
|
+
const maxSymbolsPerFile = opts?.maxSymbols ?? 5;
|
|
471
|
+
const files = this.stmts.getAllFiles.all();
|
|
472
|
+
if (!files || files.length === 0) {
|
|
473
|
+
return { content: '', paths: [] };
|
|
474
|
+
}
|
|
475
|
+
const topFiles = files.slice(0, maxFiles);
|
|
476
|
+
let content = '';
|
|
477
|
+
const paths = [];
|
|
478
|
+
for (const file of topFiles) {
|
|
479
|
+
const symbols = this.stmts.getSymbolsByFileId.all(file.id);
|
|
480
|
+
if (!symbols || symbols.length === 0)
|
|
481
|
+
continue;
|
|
482
|
+
content += `// ${file.path}\n`;
|
|
483
|
+
for (const sym of symbols.slice(0, maxSymbolsPerFile)) {
|
|
484
|
+
content += `// ${kindTag(sym.kind)}${sym.name}\n`;
|
|
485
|
+
}
|
|
486
|
+
content += '\n';
|
|
487
|
+
paths.push(file.path);
|
|
488
|
+
}
|
|
489
|
+
return { content, paths };
|
|
490
|
+
}
|
|
491
|
+
getStats() {
|
|
492
|
+
const counts = this.stmts.getCounts.get();
|
|
493
|
+
const summaries = this.db.prepare('SELECT COUNT(*) as count FROM semantic_summaries').get();
|
|
494
|
+
const calls = this.db.prepare('SELECT COUNT(*) as count FROM calls').get();
|
|
495
|
+
return {
|
|
496
|
+
files: counts.files,
|
|
497
|
+
symbols: counts.symbols,
|
|
498
|
+
edges: counts.edges,
|
|
499
|
+
summaries: summaries.count,
|
|
500
|
+
calls: calls.count,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
getTopFiles(limit = 20) {
|
|
504
|
+
const files = this.db.prepare('SELECT * FROM files ORDER BY pagerank DESC LIMIT ?').all(limit);
|
|
505
|
+
return files.map(f => ({
|
|
506
|
+
path: f.path,
|
|
507
|
+
pagerank: f.pagerank,
|
|
508
|
+
lines: f.line_count,
|
|
509
|
+
symbols: f.symbol_count,
|
|
510
|
+
language: f.language,
|
|
511
|
+
}));
|
|
512
|
+
}
|
|
513
|
+
getFileDependents(path) {
|
|
514
|
+
const file = this.stmts.getFileByPath.get(path);
|
|
515
|
+
if (!file)
|
|
516
|
+
return [];
|
|
517
|
+
const edges = this.stmts.getEdgesByTargetFile.all(file.id);
|
|
518
|
+
const results = [];
|
|
519
|
+
for (const edge of edges) {
|
|
520
|
+
const source = this.stmts.getFileById.get(edge.source_file_id);
|
|
521
|
+
if (source) {
|
|
522
|
+
results.push({ path: source.path, weight: edge.weight });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return results;
|
|
526
|
+
}
|
|
527
|
+
getFileDependencies(path) {
|
|
528
|
+
const file = this.stmts.getFileByPath.get(path);
|
|
529
|
+
if (!file)
|
|
530
|
+
return [];
|
|
531
|
+
const edges = this.stmts.getEdgesBySourceFile.all(file.id);
|
|
532
|
+
const results = [];
|
|
533
|
+
for (const edge of edges) {
|
|
534
|
+
const target = this.stmts.getFileById.get(edge.target_file_id);
|
|
535
|
+
if (target) {
|
|
536
|
+
results.push({ path: target.path, weight: edge.weight });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return results;
|
|
540
|
+
}
|
|
541
|
+
getFileCoChanges(path) {
|
|
542
|
+
const file = this.stmts.getFileByPath.get(path);
|
|
543
|
+
if (!file)
|
|
544
|
+
return [];
|
|
545
|
+
const cochanges = this.stmts.getCoChanges.all(file.id, file.id, file.id);
|
|
546
|
+
return cochanges.map(c => {
|
|
547
|
+
const other = this.stmts.getFileById.get(c.other_id);
|
|
548
|
+
return {
|
|
549
|
+
path: other?.path || '',
|
|
550
|
+
count: c.count,
|
|
551
|
+
};
|
|
552
|
+
}).filter(r => r.path);
|
|
553
|
+
}
|
|
554
|
+
getFileBlastRadius(path) {
|
|
555
|
+
const file = this.stmts.getFileByPath.get(path);
|
|
556
|
+
if (!file)
|
|
557
|
+
return 0;
|
|
558
|
+
const visited = new Set();
|
|
559
|
+
const queue = [file.id];
|
|
560
|
+
while (queue.length > 0) {
|
|
561
|
+
const id = queue.shift();
|
|
562
|
+
if (visited.has(id))
|
|
563
|
+
continue;
|
|
564
|
+
visited.add(id);
|
|
565
|
+
const edges = this.stmts.getEdgesTargetIds.all(id);
|
|
566
|
+
for (const edge of edges) {
|
|
567
|
+
if (!visited.has(edge.target_file_id)) {
|
|
568
|
+
queue.push(edge.target_file_id);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return visited.size - 1;
|
|
573
|
+
}
|
|
574
|
+
getFileSymbols(path) {
|
|
575
|
+
const file = this.stmts.getFileByPath.get(path);
|
|
576
|
+
if (!file)
|
|
577
|
+
return [];
|
|
578
|
+
const symbols = this.stmts.getFileSymbolsQuery.all(file.id);
|
|
579
|
+
return symbols.map(s => ({
|
|
580
|
+
name: s.name,
|
|
581
|
+
kind: s.kind,
|
|
582
|
+
isExported: !!s.is_exported,
|
|
583
|
+
line: s.line,
|
|
584
|
+
endLine: s.end_line,
|
|
585
|
+
}));
|
|
586
|
+
}
|
|
587
|
+
findSymbols(query, limit = 50) {
|
|
588
|
+
const results = this.db.prepare(`
|
|
589
|
+
SELECT s.name, f.path, s.kind, s.line, s.is_exported AS isExported, f.pagerank, s.id
|
|
590
|
+
FROM symbols s
|
|
591
|
+
JOIN files f ON s.file_id = f.id
|
|
592
|
+
WHERE s.name LIKE ?
|
|
593
|
+
ORDER BY f.pagerank DESC
|
|
594
|
+
LIMIT ?
|
|
595
|
+
`).all(`%${query}%`, limit);
|
|
596
|
+
return results;
|
|
597
|
+
}
|
|
598
|
+
searchSymbolsFts(query, limit = 50) {
|
|
599
|
+
try {
|
|
600
|
+
const results = this.stmts.searchSymbolsFtsQuery.all(query, limit);
|
|
601
|
+
return results;
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
getSymbolSignature(path, line) {
|
|
608
|
+
const file = this.stmts.getFileByPath.get(path);
|
|
609
|
+
if (!file)
|
|
610
|
+
return null;
|
|
611
|
+
const symbol = this.stmts.getSymbolByFileAndLine.get(file.id, line);
|
|
612
|
+
if (!symbol)
|
|
613
|
+
return null;
|
|
614
|
+
return {
|
|
615
|
+
path,
|
|
616
|
+
kind: symbol.kind,
|
|
617
|
+
signature: symbol.signature || '',
|
|
618
|
+
line: symbol.line,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
getCallers(path, line) {
|
|
622
|
+
// Find the symbol at the given location
|
|
623
|
+
const fileId = this.stmts.getFileByPath.get(path);
|
|
624
|
+
if (!fileId)
|
|
625
|
+
return [];
|
|
626
|
+
const symbol = this.stmts.getSymbolByFileAndLine.get(fileId.id, line);
|
|
627
|
+
if (!symbol)
|
|
628
|
+
return [];
|
|
629
|
+
// Find all calls where this symbol is the callee - use both name and file for disambiguation
|
|
630
|
+
const callers = this.db.prepare(`
|
|
631
|
+
SELECT s.name as caller_name, f.path as caller_path, s.line as caller_line, c.line as call_line
|
|
632
|
+
FROM calls c
|
|
633
|
+
JOIN symbols s ON c.caller_symbol_id = s.id
|
|
634
|
+
JOIN files f ON s.file_id = f.id
|
|
635
|
+
WHERE c.callee_name = ? AND (c.callee_file_id IS NULL OR c.callee_file_id = ?)
|
|
636
|
+
`).all(symbol.name, fileId.id);
|
|
637
|
+
return callers.map(c => ({
|
|
638
|
+
callerName: c.caller_name,
|
|
639
|
+
callerPath: c.caller_path,
|
|
640
|
+
callerLine: c.caller_line,
|
|
641
|
+
callLine: c.call_line,
|
|
642
|
+
}));
|
|
643
|
+
}
|
|
644
|
+
getCallees(path, line) {
|
|
645
|
+
// Find the symbol at the given location
|
|
646
|
+
const fileId = this.stmts.getFileByPath.get(path);
|
|
647
|
+
if (!fileId)
|
|
648
|
+
return [];
|
|
649
|
+
const symbol = this.stmts.getSymbolByFileAndLine.get(fileId.id, line);
|
|
650
|
+
if (!symbol)
|
|
651
|
+
return [];
|
|
652
|
+
// Find all calls made by this symbol - use symbol id for precise matching
|
|
653
|
+
const callees = this.db.prepare(`
|
|
654
|
+
SELECT c.callee_name, f.path as callee_file, c.line as call_line,
|
|
655
|
+
(SELECT line FROM symbols WHERE id = c.callee_symbol_id) as callee_def_line
|
|
656
|
+
FROM calls c
|
|
657
|
+
JOIN files f ON c.callee_file_id = f.id
|
|
658
|
+
WHERE c.caller_symbol_id = ?
|
|
659
|
+
`).all(symbol.id);
|
|
660
|
+
return callees.map(c => ({
|
|
661
|
+
calleeName: c.callee_name,
|
|
662
|
+
calleeFile: c.callee_file,
|
|
663
|
+
calleeLine: c.callee_def_line || c.call_line,
|
|
664
|
+
callLine: c.call_line,
|
|
665
|
+
}));
|
|
666
|
+
}
|
|
667
|
+
getUnusedExports(limit = 50) {
|
|
668
|
+
const exports = this.db.prepare(`
|
|
669
|
+
SELECT s.*, f.path, f.line_count
|
|
670
|
+
FROM symbols s
|
|
671
|
+
JOIN files f ON s.file_id = f.id
|
|
672
|
+
WHERE s.is_exported = 1
|
|
673
|
+
LIMIT ?
|
|
674
|
+
`).all(limit);
|
|
675
|
+
const unused = [];
|
|
676
|
+
for (const exp of exports) {
|
|
677
|
+
const used = this.db.prepare('SELECT 1 FROM refs WHERE name = ? AND source_file_id IS NOT NULL LIMIT 1').get(exp.name);
|
|
678
|
+
if (!used) {
|
|
679
|
+
unused.push({
|
|
680
|
+
name: exp.name,
|
|
681
|
+
path: exp.path,
|
|
682
|
+
kind: exp.kind,
|
|
683
|
+
line: exp.line,
|
|
684
|
+
endLine: exp.end_line,
|
|
685
|
+
lineCount: exp.line_count,
|
|
686
|
+
usedInternally: false,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return unused;
|
|
691
|
+
}
|
|
692
|
+
getDuplicateStructures(limit = 20) {
|
|
693
|
+
const hashes = this.db.prepare(`
|
|
694
|
+
SELECT shape_hash, kind, node_count,
|
|
695
|
+
GROUP_CONCAT(file_id || ':' || line) as members
|
|
696
|
+
FROM shape_hashes
|
|
697
|
+
GROUP BY shape_hash
|
|
698
|
+
HAVING COUNT(*) > 1
|
|
699
|
+
LIMIT ?
|
|
700
|
+
`).all(limit);
|
|
701
|
+
return hashes.map(h => ({
|
|
702
|
+
shapeHash: h.shape_hash,
|
|
703
|
+
kind: h.kind,
|
|
704
|
+
nodeCount: h.node_count,
|
|
705
|
+
members: h.members.split(',').map(m => {
|
|
706
|
+
const [fileId, line] = m.split(':');
|
|
707
|
+
const file = this.stmts.getFileById.get(Number(fileId));
|
|
708
|
+
return { path: file?.path || '', line: Number(line) };
|
|
709
|
+
}),
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
getNearDuplicates(threshold = 0.8, limit = 50) {
|
|
713
|
+
const signatures = this.db.prepare('SELECT * FROM token_signatures').all();
|
|
714
|
+
const results = [];
|
|
715
|
+
for (let i = 0; i < signatures.length; i++) {
|
|
716
|
+
for (let j = i + 1; j < signatures.length; j++) {
|
|
717
|
+
const a = signatures[i];
|
|
718
|
+
const b = signatures[j];
|
|
719
|
+
if (a.file_id === b.file_id)
|
|
720
|
+
continue;
|
|
721
|
+
const similarity = jaccardSimilarity(a.minhash, b.minhash);
|
|
722
|
+
if (similarity >= threshold) {
|
|
723
|
+
const fileA = this.stmts.getFileById.get(a.file_id);
|
|
724
|
+
const fileB = this.stmts.getFileById.get(b.file_id);
|
|
725
|
+
if (fileA && fileB) {
|
|
726
|
+
results.push({
|
|
727
|
+
similarity,
|
|
728
|
+
a: { path: fileA.path, line: a.line, name: a.name },
|
|
729
|
+
b: { path: fileB.path, line: b.line, name: b.name },
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return results.slice(0, limit);
|
|
736
|
+
}
|
|
737
|
+
getExternalPackages(limit = 50) {
|
|
738
|
+
const packages = this.db.prepare(`
|
|
739
|
+
SELECT package, COUNT(DISTINCT file_id) as file_count,
|
|
740
|
+
GROUP_CONCAT(DISTINCT specifiers) as specifiers
|
|
741
|
+
FROM external_imports
|
|
742
|
+
GROUP BY package
|
|
743
|
+
ORDER BY file_count DESC
|
|
744
|
+
LIMIT ?
|
|
745
|
+
`).all(limit);
|
|
746
|
+
return packages.map(p => ({
|
|
747
|
+
package: p.package,
|
|
748
|
+
fileCount: p.file_count,
|
|
749
|
+
specifiers: p.specifiers ? p.specifiers.split(',').map(s => s.trim()) : [],
|
|
750
|
+
}));
|
|
751
|
+
}
|
|
752
|
+
async onFileChanged(path) {
|
|
753
|
+
const absPath = resolve(path);
|
|
754
|
+
const relPath = relative(this.cwd, absPath);
|
|
755
|
+
try {
|
|
756
|
+
// Check if file still exists
|
|
757
|
+
try {
|
|
758
|
+
statSync(absPath);
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
// File was deleted - remove from graph
|
|
762
|
+
await this.removeFile(relPath);
|
|
763
|
+
// Rebuild all derived state after deletion
|
|
764
|
+
await this.buildEdges();
|
|
765
|
+
await this.resolveUnresolvedRefs();
|
|
766
|
+
await this.computePageRank();
|
|
767
|
+
await this.buildCallGraph();
|
|
768
|
+
return { status: 'ok' };
|
|
769
|
+
}
|
|
770
|
+
// Re-index the file
|
|
771
|
+
await this.indexFile(relPath);
|
|
772
|
+
// Rebuild all derived state for correctness
|
|
773
|
+
const file = this.stmts.getFileByPath.get(relPath);
|
|
774
|
+
if (file) {
|
|
775
|
+
// Remove stale edges
|
|
776
|
+
this.stmts.deleteEdgesBySource.run([file.id]);
|
|
777
|
+
this.stmts.deleteEdgesByTarget.run([file.id]);
|
|
778
|
+
// Resolve any unresolved refs after reindexing
|
|
779
|
+
await this.resolveUnresolvedRefs();
|
|
780
|
+
// Rebuild edges from all refs (not just this file's outgoing)
|
|
781
|
+
await this.buildEdges();
|
|
782
|
+
// Recompute PageRank
|
|
783
|
+
await this.computePageRank();
|
|
784
|
+
// Rebuild call graph
|
|
785
|
+
await this.buildCallGraph();
|
|
786
|
+
}
|
|
787
|
+
return { status: 'ok' };
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
console.error('Error updating file:', err);
|
|
791
|
+
return { status: 'error' };
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
async removeFile(relPath) {
|
|
795
|
+
const existing = this.stmts.getFileByPath.get(relPath);
|
|
796
|
+
if (!existing)
|
|
797
|
+
return;
|
|
798
|
+
// Delete all related data
|
|
799
|
+
this.stmts.deleteRefsByFileId.run([existing.id]);
|
|
800
|
+
this.stmts.deleteEdgesBySource.run([existing.id]);
|
|
801
|
+
this.stmts.deleteEdgesByTarget.run([existing.id]);
|
|
802
|
+
this.stmts.deleteSymbolsByFileId.run([existing.id]);
|
|
803
|
+
this.stmts.deleteShapeHashesByFileId.run([existing.id]);
|
|
804
|
+
this.stmts.deleteTokenSignaturesByFileId.run([existing.id]);
|
|
805
|
+
this.stmts.deleteTokenFragmentsByFileId.run([existing.id]);
|
|
806
|
+
this.stmts.deleteExternalImportsByFileId.run([existing.id]);
|
|
807
|
+
this.stmts.deleteFile.run([existing.id]);
|
|
808
|
+
}
|
|
809
|
+
async buildCoChanges() {
|
|
810
|
+
// Check if git is available
|
|
811
|
+
try {
|
|
812
|
+
const { execSync } = await import('child_process');
|
|
813
|
+
execSync('git rev-parse --git-dir', { cwd: this.cwd, stdio: 'pipe' });
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
return; // Not a git repo
|
|
817
|
+
}
|
|
818
|
+
this.db.run('DELETE FROM cochanges');
|
|
819
|
+
let logOutput;
|
|
820
|
+
try {
|
|
821
|
+
const { execFile } = await import('child_process');
|
|
822
|
+
logOutput = await new Promise((resolve, reject) => {
|
|
823
|
+
execFile('git', ['log', '--pretty=format:---COMMIT---', '--name-only', '-n', '300'], { cwd: this.cwd, timeout: 10_000, maxBuffer: 5_000_000 }, (err, stdout) => (err ? reject(err) : resolve(stdout)));
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const pathToId = new Map();
|
|
830
|
+
for (const row of this.db.prepare('SELECT id, path FROM files').all()) {
|
|
831
|
+
pathToId.set(row.path, row.id);
|
|
832
|
+
}
|
|
833
|
+
const pairCounts = new Map();
|
|
834
|
+
const commits = logOutput.split('---COMMIT---').filter((s) => s.trim());
|
|
835
|
+
for (const commit of commits) {
|
|
836
|
+
const files = commit
|
|
837
|
+
.split('\n')
|
|
838
|
+
.map((l) => l.trim())
|
|
839
|
+
.filter((l) => l && pathToId.has(l));
|
|
840
|
+
if (files.length < 2 || files.length > 20)
|
|
841
|
+
continue;
|
|
842
|
+
for (let i = 0; i < files.length; i++) {
|
|
843
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
844
|
+
const a = files[i];
|
|
845
|
+
const b = files[j];
|
|
846
|
+
const key = a < b ? `${a}\0${b}` : `${b}\0${a}`;
|
|
847
|
+
pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (pairCounts.size === 0)
|
|
852
|
+
return;
|
|
853
|
+
const insert = this.db.prepare(`
|
|
854
|
+
INSERT OR REPLACE INTO cochanges (file_id_a, file_id_b, count)
|
|
855
|
+
VALUES (?, ?, ?)
|
|
856
|
+
`);
|
|
857
|
+
const entries = [...pairCounts.entries()].filter(([, count]) => count >= 2);
|
|
858
|
+
const tx = this.db.transaction(() => {
|
|
859
|
+
for (const [key, count] of entries) {
|
|
860
|
+
const [a, b] = key.split('\0');
|
|
861
|
+
const idA = pathToId.get(a);
|
|
862
|
+
const idB = pathToId.get(b);
|
|
863
|
+
if (idA !== undefined && idB !== undefined) {
|
|
864
|
+
insert.run(idA, idB, count);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
tx();
|
|
869
|
+
}
|
|
870
|
+
async buildCallGraph() {
|
|
871
|
+
const { readFileSync } = await import('fs');
|
|
872
|
+
const regexCache = new Map();
|
|
873
|
+
this.db.run('DELETE FROM calls');
|
|
874
|
+
const filesWithImports = this.stmts.getFilesWithImports.all();
|
|
875
|
+
if (filesWithImports.length === 0)
|
|
876
|
+
return;
|
|
877
|
+
// Pre-read all files
|
|
878
|
+
const fileContents = new Map();
|
|
879
|
+
for (const file of filesWithImports) {
|
|
880
|
+
try {
|
|
881
|
+
const content = readFileSync(join(this.cwd, file.path), 'utf-8');
|
|
882
|
+
fileContents.set(file.id, content.split('\n'));
|
|
883
|
+
}
|
|
884
|
+
catch { }
|
|
885
|
+
}
|
|
886
|
+
const tx = this.db.transaction(() => {
|
|
887
|
+
for (const file of filesWithImports) {
|
|
888
|
+
const lines = fileContents.get(file.id);
|
|
889
|
+
if (!lines)
|
|
890
|
+
continue;
|
|
891
|
+
const imports = this.stmts.getImportsForFile.all(file.id);
|
|
892
|
+
if (imports.length === 0)
|
|
893
|
+
continue;
|
|
894
|
+
const functions = this.stmts.getFunctionsForFile.all(file.id);
|
|
895
|
+
if (functions.length === 0)
|
|
896
|
+
continue;
|
|
897
|
+
const importPatterns = imports.map((imp) => {
|
|
898
|
+
const escaped = imp.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
899
|
+
let re = regexCache.get(imp.name);
|
|
900
|
+
if (!re) {
|
|
901
|
+
re = new RegExp(`\\b${escaped}\\b`);
|
|
902
|
+
regexCache.set(imp.name, re);
|
|
903
|
+
}
|
|
904
|
+
return { name: imp.name, sourceFileId: imp.source_file_id, re };
|
|
905
|
+
});
|
|
906
|
+
for (const func of functions) {
|
|
907
|
+
const bodyStart = func.line;
|
|
908
|
+
const bodyEnd = Math.min(func.end_line, lines.length);
|
|
909
|
+
const bodyText = lines.slice(bodyStart - 1, bodyEnd).join('\n');
|
|
910
|
+
for (const imp of importPatterns) {
|
|
911
|
+
if (imp.name === func.name)
|
|
912
|
+
continue;
|
|
913
|
+
if (imp.re.test(bodyText)) {
|
|
914
|
+
let callLine = func.line;
|
|
915
|
+
for (let i = bodyStart - 1; i < bodyEnd; i++) {
|
|
916
|
+
const ln = lines[i];
|
|
917
|
+
if (ln !== undefined && imp.re.test(ln)) {
|
|
918
|
+
callLine = i + 1;
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const calleeRow = this.stmts.resolveCallee.get(imp.sourceFileId, imp.name);
|
|
923
|
+
this.stmts.insertCall.run(func.id, imp.name, calleeRow?.id ?? null, imp.sourceFileId, callLine);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
tx();
|
|
930
|
+
}
|
|
931
|
+
linkTestFiles() {
|
|
932
|
+
const testFiles = this.stmts.getTestFiles.all();
|
|
933
|
+
for (const testFile of testFiles) {
|
|
934
|
+
const sourcePath = testFile.path
|
|
935
|
+
.replace(/\.test\./, '.')
|
|
936
|
+
.replace(/_test\./, '.')
|
|
937
|
+
.replace(/\.spec\./, '.');
|
|
938
|
+
const source = this.stmts.getFileByPath.get(sourcePath);
|
|
939
|
+
if (source) {
|
|
940
|
+
this.stmts.insertEdge.run([testFile.id, source.id, 1, 1]);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
rescueOrphans() {
|
|
945
|
+
// Ensure all files have at least some graph connections
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
//# sourceMappingURL=repo-map.js.map
|