grepmind-core 0.1.0-alpha
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 +190 -0
- package/dist/config/types.d.ts +174 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +137 -0
- package/dist/config/types.js.map +1 -0
- package/dist/git.d.ts +98 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +298 -0
- package/dist/git.js.map +1 -0
- package/dist/git.test.d.ts +7 -0
- package/dist/git.test.d.ts.map +1 -0
- package/dist/git.test.js +242 -0
- package/dist/git.test.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/branch.d.ts +121 -0
- package/dist/indexer/branch.d.ts.map +1 -0
- package/dist/indexer/branch.js +451 -0
- package/dist/indexer/branch.js.map +1 -0
- package/dist/indexer/chunker.d.ts +9 -0
- package/dist/indexer/chunker.d.ts.map +1 -0
- package/dist/indexer/chunker.js +70 -0
- package/dist/indexer/chunker.js.map +1 -0
- package/dist/indexer/chunker.test.d.ts +2 -0
- package/dist/indexer/chunker.test.d.ts.map +1 -0
- package/dist/indexer/chunker.test.js +180 -0
- package/dist/indexer/chunker.test.js.map +1 -0
- package/dist/indexer/code/branch.d.ts +155 -0
- package/dist/indexer/code/branch.d.ts.map +1 -0
- package/dist/indexer/code/branch.js +550 -0
- package/dist/indexer/code/branch.js.map +1 -0
- package/dist/indexer/code/branch.test.d.ts +7 -0
- package/dist/indexer/code/branch.test.d.ts.map +1 -0
- package/dist/indexer/code/branch.test.js +241 -0
- package/dist/indexer/code/branch.test.js.map +1 -0
- package/dist/indexer/code/chunker.d.ts +61 -0
- package/dist/indexer/code/chunker.d.ts.map +1 -0
- package/dist/indexer/code/chunker.js +311 -0
- package/dist/indexer/code/chunker.js.map +1 -0
- package/dist/indexer/code/chunker.test.d.ts +2 -0
- package/dist/indexer/code/chunker.test.d.ts.map +1 -0
- package/dist/indexer/code/chunker.test.js +552 -0
- package/dist/indexer/code/chunker.test.js.map +1 -0
- package/dist/indexer/code/fts.test.d.ts +2 -0
- package/dist/indexer/code/fts.test.d.ts.map +1 -0
- package/dist/indexer/code/fts.test.js +14 -0
- package/dist/indexer/code/fts.test.js.map +1 -0
- package/dist/indexer/code/graph/embedded.d.ts +11 -0
- package/dist/indexer/code/graph/embedded.d.ts.map +1 -0
- package/dist/indexer/code/graph/embedded.js +152 -0
- package/dist/indexer/code/graph/embedded.js.map +1 -0
- package/dist/indexer/code/graph/embedded.test.d.ts +2 -0
- package/dist/indexer/code/graph/embedded.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/embedded.test.js +105 -0
- package/dist/indexer/code/graph/embedded.test.js.map +1 -0
- package/dist/indexer/code/graph/facts.d.ts +11 -0
- package/dist/indexer/code/graph/facts.d.ts.map +1 -0
- package/dist/indexer/code/graph/facts.js +456 -0
- package/dist/indexer/code/graph/facts.js.map +1 -0
- package/dist/indexer/code/graph/facts.test.d.ts +2 -0
- package/dist/indexer/code/graph/facts.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/facts.test.js +181 -0
- package/dist/indexer/code/graph/facts.test.js.map +1 -0
- package/dist/indexer/code/graph/id.d.ts +14 -0
- package/dist/indexer/code/graph/id.d.ts.map +1 -0
- package/dist/indexer/code/graph/id.js +40 -0
- package/dist/indexer/code/graph/id.js.map +1 -0
- package/dist/indexer/code/graph/id.test.d.ts +2 -0
- package/dist/indexer/code/graph/id.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/id.test.js +86 -0
- package/dist/indexer/code/graph/id.test.js.map +1 -0
- package/dist/indexer/code/graph/index.d.ts +133 -0
- package/dist/indexer/code/graph/index.d.ts.map +1 -0
- package/dist/indexer/code/graph/index.js +1876 -0
- package/dist/indexer/code/graph/index.js.map +1 -0
- package/dist/indexer/code/graph/index.test.d.ts +2 -0
- package/dist/indexer/code/graph/index.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/index.test.js +210 -0
- package/dist/indexer/code/graph/index.test.js.map +1 -0
- package/dist/indexer/code/graph/queries.d.ts +22 -0
- package/dist/indexer/code/graph/queries.d.ts.map +1 -0
- package/dist/indexer/code/graph/queries.js +79 -0
- package/dist/indexer/code/graph/queries.js.map +1 -0
- package/dist/indexer/code/graph/queries.test.d.ts +2 -0
- package/dist/indexer/code/graph/queries.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/queries.test.js +108 -0
- package/dist/indexer/code/graph/queries.test.js.map +1 -0
- package/dist/indexer/code/graph/resolver.d.ts +136 -0
- package/dist/indexer/code/graph/resolver.d.ts.map +1 -0
- package/dist/indexer/code/graph/resolver.js +839 -0
- package/dist/indexer/code/graph/resolver.js.map +1 -0
- package/dist/indexer/code/graph/resolver.test.d.ts +2 -0
- package/dist/indexer/code/graph/resolver.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/resolver.test.js +482 -0
- package/dist/indexer/code/graph/resolver.test.js.map +1 -0
- package/dist/indexer/code/graph/semantic.d.ts +33 -0
- package/dist/indexer/code/graph/semantic.d.ts.map +1 -0
- package/dist/indexer/code/graph/semantic.js +279 -0
- package/dist/indexer/code/graph/semantic.js.map +1 -0
- package/dist/indexer/code/graph/semantic.test.d.ts +2 -0
- package/dist/indexer/code/graph/semantic.test.d.ts.map +1 -0
- package/dist/indexer/code/graph/semantic.test.js +127 -0
- package/dist/indexer/code/graph/semantic.test.js.map +1 -0
- package/dist/indexer/code/index.d.ts +404 -0
- package/dist/indexer/code/index.d.ts.map +1 -0
- package/dist/indexer/code/index.js +2070 -0
- package/dist/indexer/code/index.js.map +1 -0
- package/dist/indexer/code/languages/bash.d.ts +14 -0
- package/dist/indexer/code/languages/bash.d.ts.map +1 -0
- package/dist/indexer/code/languages/bash.js +125 -0
- package/dist/indexer/code/languages/bash.js.map +1 -0
- package/dist/indexer/code/languages/css.d.ts +16 -0
- package/dist/indexer/code/languages/css.d.ts.map +1 -0
- package/dist/indexer/code/languages/css.js +204 -0
- package/dist/indexer/code/languages/css.js.map +1 -0
- package/dist/indexer/code/languages/generic.d.ts +61 -0
- package/dist/indexer/code/languages/generic.d.ts.map +1 -0
- package/dist/indexer/code/languages/generic.js +150 -0
- package/dist/indexer/code/languages/generic.js.map +1 -0
- package/dist/indexer/code/languages/graphql.d.ts +13 -0
- package/dist/indexer/code/languages/graphql.d.ts.map +1 -0
- package/dist/indexer/code/languages/graphql.js +180 -0
- package/dist/indexer/code/languages/graphql.js.map +1 -0
- package/dist/indexer/code/languages/html.d.ts +16 -0
- package/dist/indexer/code/languages/html.d.ts.map +1 -0
- package/dist/indexer/code/languages/html.js +138 -0
- package/dist/indexer/code/languages/html.js.map +1 -0
- package/dist/indexer/code/languages/index.d.ts +9 -0
- package/dist/indexer/code/languages/index.d.ts.map +1 -0
- package/dist/indexer/code/languages/index.js +12 -0
- package/dist/indexer/code/languages/index.js.map +1 -0
- package/dist/indexer/code/languages/json.d.ts +12 -0
- package/dist/indexer/code/languages/json.d.ts.map +1 -0
- package/dist/indexer/code/languages/json.js +66 -0
- package/dist/indexer/code/languages/json.js.map +1 -0
- package/dist/indexer/code/languages/registry.d.ts +78 -0
- package/dist/indexer/code/languages/registry.d.ts.map +1 -0
- package/dist/indexer/code/languages/registry.js +72 -0
- package/dist/indexer/code/languages/registry.js.map +1 -0
- package/dist/indexer/code/languages/typescript.d.ts +39 -0
- package/dist/indexer/code/languages/typescript.d.ts.map +1 -0
- package/dist/indexer/code/languages/typescript.js +300 -0
- package/dist/indexer/code/languages/typescript.js.map +1 -0
- package/dist/indexer/code/languages/yaml.d.ts +13 -0
- package/dist/indexer/code/languages/yaml.d.ts.map +1 -0
- package/dist/indexer/code/languages/yaml.js +90 -0
- package/dist/indexer/code/languages/yaml.js.map +1 -0
- package/dist/indexer/code/parser.d.ts +26 -0
- package/dist/indexer/code/parser.d.ts.map +1 -0
- package/dist/indexer/code/parser.js +332 -0
- package/dist/indexer/code/parser.js.map +1 -0
- package/dist/indexer/code/retry.d.ts +58 -0
- package/dist/indexer/code/retry.d.ts.map +1 -0
- package/dist/indexer/code/retry.js +192 -0
- package/dist/indexer/code/retry.js.map +1 -0
- package/dist/indexer/code/tree/builder.d.ts +30 -0
- package/dist/indexer/code/tree/builder.d.ts.map +1 -0
- package/dist/indexer/code/tree/builder.js +132 -0
- package/dist/indexer/code/tree/builder.js.map +1 -0
- package/dist/indexer/code/tree/builder.test.d.ts +2 -0
- package/dist/indexer/code/tree/builder.test.d.ts.map +1 -0
- package/dist/indexer/code/tree/builder.test.js +31 -0
- package/dist/indexer/code/tree/builder.test.js.map +1 -0
- package/dist/indexer/code/tree/cache.d.ts +22 -0
- package/dist/indexer/code/tree/cache.d.ts.map +1 -0
- package/dist/indexer/code/tree/cache.js +85 -0
- package/dist/indexer/code/tree/cache.js.map +1 -0
- package/dist/indexer/code/tree/context.d.ts +32 -0
- package/dist/indexer/code/tree/context.d.ts.map +1 -0
- package/dist/indexer/code/tree/context.js +78 -0
- package/dist/indexer/code/tree/context.js.map +1 -0
- package/dist/indexer/code/tree/embedding.d.ts +9 -0
- package/dist/indexer/code/tree/embedding.d.ts.map +1 -0
- package/dist/indexer/code/tree/embedding.js +53 -0
- package/dist/indexer/code/tree/embedding.js.map +1 -0
- package/dist/indexer/code/tree/embedding.test.d.ts +2 -0
- package/dist/indexer/code/tree/embedding.test.d.ts.map +1 -0
- package/dist/indexer/code/tree/embedding.test.js +57 -0
- package/dist/indexer/code/tree/embedding.test.js.map +1 -0
- package/dist/indexer/code/tree/id.d.ts +3 -0
- package/dist/indexer/code/tree/id.d.ts.map +1 -0
- package/dist/indexer/code/tree/id.js +8 -0
- package/dist/indexer/code/tree/id.js.map +1 -0
- package/dist/indexer/code/tree/index.d.ts +113 -0
- package/dist/indexer/code/tree/index.d.ts.map +1 -0
- package/dist/indexer/code/tree/index.js +1146 -0
- package/dist/indexer/code/tree/index.js.map +1 -0
- package/dist/indexer/code/tree/rename.d.ts +13 -0
- package/dist/indexer/code/tree/rename.d.ts.map +1 -0
- package/dist/indexer/code/tree/rename.js +46 -0
- package/dist/indexer/code/tree/rename.js.map +1 -0
- package/dist/indexer/code/tree/repomap.d.ts +29 -0
- package/dist/indexer/code/tree/repomap.d.ts.map +1 -0
- package/dist/indexer/code/tree/repomap.js +95 -0
- package/dist/indexer/code/tree/repomap.js.map +1 -0
- package/dist/indexer/code/tree/repomap.test.d.ts +2 -0
- package/dist/indexer/code/tree/repomap.test.d.ts.map +1 -0
- package/dist/indexer/code/tree/repomap.test.js +93 -0
- package/dist/indexer/code/tree/repomap.test.js.map +1 -0
- package/dist/indexer/code/tree/stats.d.ts +26 -0
- package/dist/indexer/code/tree/stats.d.ts.map +1 -0
- package/dist/indexer/code/tree/stats.js +49 -0
- package/dist/indexer/code/tree/stats.js.map +1 -0
- package/dist/indexer/code/tree/types.d.ts +186 -0
- package/dist/indexer/code/tree/types.d.ts.map +1 -0
- package/dist/indexer/code/tree/types.js +10 -0
- package/dist/indexer/code/tree/types.js.map +1 -0
- package/dist/indexer/code/wal.d.ts +144 -0
- package/dist/indexer/code/wal.d.ts.map +1 -0
- package/dist/indexer/code/wal.js +283 -0
- package/dist/indexer/code/wal.js.map +1 -0
- package/dist/indexer/embeddings.d.ts +113 -0
- package/dist/indexer/embeddings.d.ts.map +1 -0
- package/dist/indexer/embeddings.js +477 -0
- package/dist/indexer/embeddings.js.map +1 -0
- package/dist/indexer/git-sync.d.ts +117 -0
- package/dist/indexer/git-sync.d.ts.map +1 -0
- package/dist/indexer/git-sync.js +398 -0
- package/dist/indexer/git-sync.js.map +1 -0
- package/dist/indexer/index.d.ts +175 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +1096 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/mocks/mock-reranker.d.ts +12 -0
- package/dist/indexer/mocks/mock-reranker.d.ts.map +1 -0
- package/dist/indexer/mocks/mock-reranker.js +26 -0
- package/dist/indexer/mocks/mock-reranker.js.map +1 -0
- package/dist/indexer/parser.d.ts +8 -0
- package/dist/indexer/parser.d.ts.map +1 -0
- package/dist/indexer/parser.js +44 -0
- package/dist/indexer/parser.js.map +1 -0
- package/dist/indexer/parser.test.d.ts +2 -0
- package/dist/indexer/parser.test.d.ts.map +1 -0
- package/dist/indexer/parser.test.js +197 -0
- package/dist/indexer/parser.test.js.map +1 -0
- package/dist/indexer/reranking.d.ts +71 -0
- package/dist/indexer/reranking.d.ts.map +1 -0
- package/dist/indexer/reranking.integration.test.d.ts +2 -0
- package/dist/indexer/reranking.integration.test.d.ts.map +1 -0
- package/dist/indexer/reranking.integration.test.js +104 -0
- package/dist/indexer/reranking.integration.test.js.map +1 -0
- package/dist/indexer/reranking.js +256 -0
- package/dist/indexer/reranking.js.map +1 -0
- package/dist/indexer/reranking.test.d.ts +2 -0
- package/dist/indexer/reranking.test.d.ts.map +1 -0
- package/dist/indexer/reranking.test.js +130 -0
- package/dist/indexer/reranking.test.js.map +1 -0
- package/dist/indexer/wal/file-storage.d.ts +60 -0
- package/dist/indexer/wal/file-storage.d.ts.map +1 -0
- package/dist/indexer/wal/file-storage.js +277 -0
- package/dist/indexer/wal/file-storage.js.map +1 -0
- package/dist/indexer/wal/file-storage.test.d.ts +8 -0
- package/dist/indexer/wal/file-storage.test.d.ts.map +1 -0
- package/dist/indexer/wal/file-storage.test.js +444 -0
- package/dist/indexer/wal/file-storage.test.js.map +1 -0
- package/dist/indexer/wal/index.d.ts +41 -0
- package/dist/indexer/wal/index.d.ts.map +1 -0
- package/dist/indexer/wal/index.js +61 -0
- package/dist/indexer/wal/index.js.map +1 -0
- package/dist/indexer/wal/integration.test.d.ts +11 -0
- package/dist/indexer/wal/integration.test.d.ts.map +1 -0
- package/dist/indexer/wal/integration.test.js +378 -0
- package/dist/indexer/wal/integration.test.js.map +1 -0
- package/dist/indexer/wal/lancedb-storage.d.ts +72 -0
- package/dist/indexer/wal/lancedb-storage.d.ts.map +1 -0
- package/dist/indexer/wal/lancedb-storage.js +462 -0
- package/dist/indexer/wal/lancedb-storage.js.map +1 -0
- package/dist/indexer/wal/lancedb-storage.test.d.ts +8 -0
- package/dist/indexer/wal/lancedb-storage.test.d.ts.map +1 -0
- package/dist/indexer/wal/lancedb-storage.test.js +415 -0
- package/dist/indexer/wal/lancedb-storage.test.js.map +1 -0
- package/dist/indexer/wal/sync-wal.d.ts +144 -0
- package/dist/indexer/wal/sync-wal.d.ts.map +1 -0
- package/dist/indexer/wal/sync-wal.js +863 -0
- package/dist/indexer/wal/sync-wal.js.map +1 -0
- package/dist/indexer/wal/sync-wal.test.d.ts +8 -0
- package/dist/indexer/wal/sync-wal.test.d.ts.map +1 -0
- package/dist/indexer/wal/sync-wal.test.js +752 -0
- package/dist/indexer/wal/sync-wal.test.js.map +1 -0
- package/dist/indexer/wal/types.d.ts +167 -0
- package/dist/indexer/wal/types.d.ts.map +1 -0
- package/dist/indexer/wal/types.js +12 -0
- package/dist/indexer/wal/types.js.map +1 -0
- package/dist/indexer/watcher.d.ts +36 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +110 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/search/explore.d.ts +62 -0
- package/dist/search/explore.d.ts.map +1 -0
- package/dist/search/explore.js +111 -0
- package/dist/search/explore.js.map +1 -0
- package/dist/search/fts.d.ts +23 -0
- package/dist/search/fts.d.ts.map +1 -0
- package/dist/search/fts.js +64 -0
- package/dist/search/fts.js.map +1 -0
- package/dist/search/fts.test.d.ts +2 -0
- package/dist/search/fts.test.d.ts.map +1 -0
- package/dist/search/fts.test.js +27 -0
- package/dist/search/fts.test.js.map +1 -0
- package/dist/search/grep.d.ts +75 -0
- package/dist/search/grep.d.ts.map +1 -0
- package/dist/search/grep.js +96 -0
- package/dist/search/grep.js.map +1 -0
- package/dist/search/grep.test.d.ts +2 -0
- package/dist/search/grep.test.d.ts.map +1 -0
- package/dist/search/grep.test.js +178 -0
- package/dist/search/grep.test.js.map +1 -0
- package/dist/search/hybrid-grep.d.ts +43 -0
- package/dist/search/hybrid-grep.d.ts.map +1 -0
- package/dist/search/hybrid-grep.js +130 -0
- package/dist/search/hybrid-grep.js.map +1 -0
- package/dist/search/hybrid-grep.test.d.ts +2 -0
- package/dist/search/hybrid-grep.test.d.ts.map +1 -0
- package/dist/search/hybrid-grep.test.js +133 -0
- package/dist/search/hybrid-grep.test.js.map +1 -0
- package/dist/search/rg-executor.d.ts +63 -0
- package/dist/search/rg-executor.d.ts.map +1 -0
- package/dist/search/rg-executor.js +146 -0
- package/dist/search/rg-executor.js.map +1 -0
- package/dist/search/rg-executor.test.d.ts +2 -0
- package/dist/search/rg-executor.test.d.ts.map +1 -0
- package/dist/search/rg-executor.test.js +104 -0
- package/dist/search/rg-executor.test.js.map +1 -0
- package/dist/search/rg-parser/extractor.d.ts +14 -0
- package/dist/search/rg-parser/extractor.d.ts.map +1 -0
- package/dist/search/rg-parser/extractor.js +82 -0
- package/dist/search/rg-parser/extractor.js.map +1 -0
- package/dist/search/rg-parser/extractor.test.d.ts +2 -0
- package/dist/search/rg-parser/extractor.test.d.ts.map +1 -0
- package/dist/search/rg-parser/extractor.test.js +35 -0
- package/dist/search/rg-parser/extractor.test.js.map +1 -0
- package/dist/search/rg-parser/fts-builder.d.ts +7 -0
- package/dist/search/rg-parser/fts-builder.d.ts.map +1 -0
- package/dist/search/rg-parser/fts-builder.js +18 -0
- package/dist/search/rg-parser/fts-builder.js.map +1 -0
- package/dist/search/rg-parser/fts-builder.test.d.ts +2 -0
- package/dist/search/rg-parser/fts-builder.test.d.ts.map +1 -0
- package/dist/search/rg-parser/fts-builder.test.js +26 -0
- package/dist/search/rg-parser/fts-builder.test.js.map +1 -0
- package/dist/search/rg-parser/index.d.ts +36 -0
- package/dist/search/rg-parser/index.d.ts.map +1 -0
- package/dist/search/rg-parser/index.js +83 -0
- package/dist/search/rg-parser/index.js.map +1 -0
- package/dist/search/rg-parser/index.test.d.ts +2 -0
- package/dist/search/rg-parser/index.test.d.ts.map +1 -0
- package/dist/search/rg-parser/index.test.js +34 -0
- package/dist/search/rg-parser/index.test.js.map +1 -0
- package/dist/search/rg-parser/strategy.d.ts +14 -0
- package/dist/search/rg-parser/strategy.d.ts.map +1 -0
- package/dist/search/rg-parser/strategy.js +31 -0
- package/dist/search/rg-parser/strategy.js.map +1 -0
- package/dist/search/rg-parser/strategy.test.d.ts +2 -0
- package/dist/search/rg-parser/strategy.test.d.ts.map +1 -0
- package/dist/search/rg-parser/strategy.test.js +29 -0
- package/dist/search/rg-parser/strategy.test.js.map +1 -0
- package/dist/types.d.ts +345 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/vault.d.ts +84 -0
- package/dist/utils/vault.d.ts.map +1 -0
- package/dist/utils/vault.js +138 -0
- package/dist/utils/vault.js.map +1 -0
- package/dist/utils/vault.test.d.ts +2 -0
- package/dist/utils/vault.test.d.ts.map +1 -0
- package/dist/utils/vault.test.js +153 -0
- package/dist/utils/vault.test.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,1876 @@
|
|
|
1
|
+
import * as lancedb from '@lancedb/lancedb';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
|
+
import { createError } from '../../../types.js';
|
|
6
|
+
import { generateContentHash } from '../chunker.js';
|
|
7
|
+
import { extractFacts } from './facts.js';
|
|
8
|
+
import { createEdgeId, createExportId } from './id.js';
|
|
9
|
+
import { extractEmbedded, parseCSSContent, parseGraphQLContent } from './embedded.js';
|
|
10
|
+
import { buildNodeEmbeddingText } from './queries.js';
|
|
11
|
+
import { ExportMapBuilder, loadTSConfig, resolveModule, clearResolverCache, WorkspaceResolver } from './resolver.js';
|
|
12
|
+
import { analyzeAllFilesSemantics, loadTypeScript } from './semantic.js';
|
|
13
|
+
/** Convert CodeEdge to LanceDB record */
|
|
14
|
+
function toEdgeRecord(edge) {
|
|
15
|
+
return {
|
|
16
|
+
id: edge.id,
|
|
17
|
+
source_id: edge.sourceId,
|
|
18
|
+
target_id: edge.targetId,
|
|
19
|
+
edge_type: edge.edgeType,
|
|
20
|
+
source_file: edge.sourceFile,
|
|
21
|
+
target_file: edge.targetFile ?? '',
|
|
22
|
+
target_module: edge.targetModule ?? '',
|
|
23
|
+
confidence: edge.confidence,
|
|
24
|
+
metadata: edge.metadata ?? '',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Convert LanceDB record to CodeEdge */
|
|
28
|
+
function fromEdgeRecord(r) {
|
|
29
|
+
return {
|
|
30
|
+
id: r.id,
|
|
31
|
+
sourceId: r.source_id,
|
|
32
|
+
targetId: r.target_id,
|
|
33
|
+
edgeType: r.edge_type,
|
|
34
|
+
sourceFile: r.source_file,
|
|
35
|
+
targetFile: r.target_file || null,
|
|
36
|
+
targetModule: r.target_module || null,
|
|
37
|
+
confidence: r.confidence,
|
|
38
|
+
metadata: r.metadata || null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Convert CodeExport to LanceDB record */
|
|
42
|
+
function toExportRecord(exp) {
|
|
43
|
+
return {
|
|
44
|
+
id: exp.id,
|
|
45
|
+
file_id: exp.fileId,
|
|
46
|
+
export_name: exp.exportName,
|
|
47
|
+
symbol_id: exp.symbolId,
|
|
48
|
+
is_reexport: exp.isReexport,
|
|
49
|
+
original_file: exp.originalFile ?? '',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Convert LanceDB record to CodeExport */
|
|
53
|
+
function fromExportRecord(r) {
|
|
54
|
+
return {
|
|
55
|
+
id: r.id,
|
|
56
|
+
fileId: r.file_id,
|
|
57
|
+
exportName: r.export_name,
|
|
58
|
+
symbolId: r.symbol_id,
|
|
59
|
+
isReexport: r.is_reexport,
|
|
60
|
+
originalFile: r.original_file || null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Convert CodeModule to LanceDB record */
|
|
64
|
+
function toModuleRecord(mod) {
|
|
65
|
+
return {
|
|
66
|
+
id: mod.id,
|
|
67
|
+
type: mod.type,
|
|
68
|
+
package_name: mod.packageName ?? '',
|
|
69
|
+
path: mod.path ?? '',
|
|
70
|
+
version: mod.version ?? '',
|
|
71
|
+
exports: mod.exports,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Convert LanceDB record to CodeModule */
|
|
75
|
+
function fromModuleRecord(r) {
|
|
76
|
+
return {
|
|
77
|
+
id: r.id,
|
|
78
|
+
type: r.type,
|
|
79
|
+
packageName: r.package_name || null,
|
|
80
|
+
path: r.path || null,
|
|
81
|
+
version: r.version || null,
|
|
82
|
+
exports: r.exports,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Convert EmbeddedChunk (camelCase) to EmbeddedRecord (snake_case) for LanceDB */
|
|
86
|
+
function toEmbeddedRecord(chunk) {
|
|
87
|
+
return {
|
|
88
|
+
id: chunk.id,
|
|
89
|
+
host_file_id: chunk.hostFileId,
|
|
90
|
+
host_symbol_id: chunk.hostSymbolId ?? '',
|
|
91
|
+
embedded_type: chunk.embeddedType,
|
|
92
|
+
content: chunk.content,
|
|
93
|
+
start_offset: chunk.startOffset,
|
|
94
|
+
end_offset: chunk.endOffset,
|
|
95
|
+
start_line: chunk.startLine,
|
|
96
|
+
end_line: chunk.endLine,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const DEFAULT_INCLUDE = [
|
|
100
|
+
'**/*.ts',
|
|
101
|
+
'**/*.tsx',
|
|
102
|
+
'**/*.js',
|
|
103
|
+
'**/*.jsx',
|
|
104
|
+
'**/*.mjs',
|
|
105
|
+
'**/*.cjs',
|
|
106
|
+
];
|
|
107
|
+
const DEFAULT_EXCLUDE = [
|
|
108
|
+
'**/node_modules/**',
|
|
109
|
+
'**/dist/**',
|
|
110
|
+
'**/build/**',
|
|
111
|
+
'**/.git/**',
|
|
112
|
+
];
|
|
113
|
+
export const QUALITY_PRESETS = {
|
|
114
|
+
fast: {
|
|
115
|
+
useTypeChecker: false,
|
|
116
|
+
maxClosureDepth: 0,
|
|
117
|
+
maxClosureFiles: 0,
|
|
118
|
+
embedEmbedded: false,
|
|
119
|
+
},
|
|
120
|
+
balanced: {
|
|
121
|
+
useTypeChecker: true,
|
|
122
|
+
maxClosureDepth: 2,
|
|
123
|
+
maxClosureFiles: 30,
|
|
124
|
+
embedEmbedded: true,
|
|
125
|
+
},
|
|
126
|
+
accurate: {
|
|
127
|
+
useTypeChecker: true,
|
|
128
|
+
maxClosureDepth: 5,
|
|
129
|
+
maxClosureFiles: 100,
|
|
130
|
+
embedEmbedded: true,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
function normalizePath(filePath) {
|
|
134
|
+
return filePath.replace(/\\/g, '/');
|
|
135
|
+
}
|
|
136
|
+
function stringifyMetadata(value) {
|
|
137
|
+
if (value == null)
|
|
138
|
+
return null;
|
|
139
|
+
try {
|
|
140
|
+
return JSON.stringify(value);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function parseMetadata(value) {
|
|
147
|
+
if (!value)
|
|
148
|
+
return {};
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(value);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function edgeTypeCount() {
|
|
157
|
+
return {
|
|
158
|
+
IMPORTS_FILE: 0,
|
|
159
|
+
IMPORTS_SYMBOL: 0,
|
|
160
|
+
EXPORTS: 0,
|
|
161
|
+
DEFINES: 0,
|
|
162
|
+
REFS: 0,
|
|
163
|
+
CALLS: 0,
|
|
164
|
+
EXTENDS: 0,
|
|
165
|
+
IMPLEMENTS: 0,
|
|
166
|
+
EMBEDS: 0,
|
|
167
|
+
USES_FRAGMENT: 0,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function buildStatsFromPrepared(filesIndexed, symbols, edges, embeddedChunks) {
|
|
171
|
+
const edgeTypes = edgeTypeCount();
|
|
172
|
+
for (const edge of edges) {
|
|
173
|
+
edgeTypes[edge.edgeType] += 1;
|
|
174
|
+
}
|
|
175
|
+
const embeddedByType = {
|
|
176
|
+
graphql: 0,
|
|
177
|
+
css: 0,
|
|
178
|
+
styled: 0,
|
|
179
|
+
};
|
|
180
|
+
for (const chunk of embeddedChunks) {
|
|
181
|
+
// Use camelCase from EmbeddedChunk
|
|
182
|
+
embeddedByType[chunk.embeddedType] += 1;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
filesIndexed,
|
|
186
|
+
symbols,
|
|
187
|
+
edges: edges.length,
|
|
188
|
+
edgeTypes,
|
|
189
|
+
unresolvedImports: edges.filter((edge) => edge.edgeType === 'IMPORTS_FILE' && edge.confidence === 'unresolved').length,
|
|
190
|
+
embeddedChunks: embeddedChunks.length,
|
|
191
|
+
embeddedByType,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export class ReverseIndexBuilder {
|
|
195
|
+
index = new Map();
|
|
196
|
+
buildFromEdges(edges) {
|
|
197
|
+
this.index.clear();
|
|
198
|
+
for (const edge of edges) {
|
|
199
|
+
if (edge.edgeType !== 'IMPORTS_FILE' || !edge.targetFile)
|
|
200
|
+
continue;
|
|
201
|
+
const importers = this.index.get(edge.targetFile) ?? new Set();
|
|
202
|
+
importers.add(edge.sourceFile);
|
|
203
|
+
this.index.set(edge.targetFile, importers);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
updateForFile(fileId, oldImports, newImports) {
|
|
207
|
+
for (const oldTarget of oldImports) {
|
|
208
|
+
this.index.get(oldTarget)?.delete(fileId);
|
|
209
|
+
}
|
|
210
|
+
for (const newTarget of newImports) {
|
|
211
|
+
const importers = this.index.get(newTarget) ?? new Set();
|
|
212
|
+
importers.add(fileId);
|
|
213
|
+
this.index.set(newTarget, importers);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
getImporters(fileId) {
|
|
217
|
+
return this.index.get(fileId) ?? new Set();
|
|
218
|
+
}
|
|
219
|
+
getTransitiveImporters(fileId, maxDepth = 2) {
|
|
220
|
+
const result = new Set();
|
|
221
|
+
const queue = [{ file: fileId, depth: 0 }];
|
|
222
|
+
while (queue.length > 0) {
|
|
223
|
+
const item = queue.shift();
|
|
224
|
+
if (!item)
|
|
225
|
+
break;
|
|
226
|
+
if (item.depth >= maxDepth)
|
|
227
|
+
continue;
|
|
228
|
+
for (const importer of this.getImporters(item.file)) {
|
|
229
|
+
if (result.has(importer))
|
|
230
|
+
continue;
|
|
231
|
+
result.add(importer);
|
|
232
|
+
queue.push({ file: importer, depth: item.depth + 1 });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export async function detectChanges(currentFiles, previousIndex, reverseIndex, transitiveDepth = 2) {
|
|
239
|
+
const modified = [];
|
|
240
|
+
const deleted = [];
|
|
241
|
+
for (const [filePath, meta] of currentFiles) {
|
|
242
|
+
const prev = previousIndex.get(filePath);
|
|
243
|
+
if (!prev || prev.contentHash !== meta.contentHash) {
|
|
244
|
+
modified.push(filePath);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const filePath of previousIndex.keys()) {
|
|
248
|
+
if (!currentFiles.has(filePath)) {
|
|
249
|
+
deleted.push(filePath);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Use transitive importers to handle re-exports properly
|
|
253
|
+
// When file A re-exports from B, and B changes, importers of A are also affected
|
|
254
|
+
const affectedSet = new Set();
|
|
255
|
+
const modifiedSet = new Set(modified);
|
|
256
|
+
const deletedSet = new Set(deleted);
|
|
257
|
+
for (const fileId of [...modified, ...deleted]) {
|
|
258
|
+
// Get transitive importers (handles re-export chains)
|
|
259
|
+
const transitiveImporters = reverseIndex.getTransitiveImporters(fileId, transitiveDepth);
|
|
260
|
+
for (const importer of transitiveImporters) {
|
|
261
|
+
if (modifiedSet.has(importer) || deletedSet.has(importer))
|
|
262
|
+
continue;
|
|
263
|
+
affectedSet.add(importer);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
modified,
|
|
268
|
+
deleted,
|
|
269
|
+
affected: [...affectedSet],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export class GraphIndexer {
|
|
273
|
+
rootPath;
|
|
274
|
+
dbPath;
|
|
275
|
+
codeConfig;
|
|
276
|
+
parser;
|
|
277
|
+
storageOptions;
|
|
278
|
+
db = null;
|
|
279
|
+
initialized = false;
|
|
280
|
+
chunksTable = null;
|
|
281
|
+
edgesTable = null;
|
|
282
|
+
modulesTable = null;
|
|
283
|
+
exportsTable = null;
|
|
284
|
+
embeddedTable = null;
|
|
285
|
+
graphFileTable = null;
|
|
286
|
+
reverseIndex = new ReverseIndexBuilder();
|
|
287
|
+
tsConfigLoaded = false;
|
|
288
|
+
tsConfig = null;
|
|
289
|
+
moduleResolveCache = new Map();
|
|
290
|
+
// Multi-tsconfig support: cache configs by directory
|
|
291
|
+
tsConfigCache = new Map();
|
|
292
|
+
tsConfigsDiscovered = false;
|
|
293
|
+
// Workspace resolution for monorepo cross-package imports
|
|
294
|
+
workspaceResolver = null;
|
|
295
|
+
constructor(deps) {
|
|
296
|
+
this.rootPath = path.resolve(deps.rootPath);
|
|
297
|
+
this.dbPath = deps.dbPath;
|
|
298
|
+
this.codeConfig = deps.codeConfig;
|
|
299
|
+
this.parser = deps.parser;
|
|
300
|
+
this.storageOptions = deps.storageOptions;
|
|
301
|
+
}
|
|
302
|
+
async initialize() {
|
|
303
|
+
if (this.initialized)
|
|
304
|
+
return;
|
|
305
|
+
const isRemoteDbPath = /^[a-z][a-z0-9+.-]*:\/\//i.test(this.dbPath);
|
|
306
|
+
if (!isRemoteDbPath) {
|
|
307
|
+
await fs.mkdir(this.dbPath, { recursive: true });
|
|
308
|
+
}
|
|
309
|
+
this.db = await lancedb.connect(this.dbPath, {
|
|
310
|
+
storageOptions: this.storageOptions,
|
|
311
|
+
});
|
|
312
|
+
this.initialized = true;
|
|
313
|
+
// Auto-create all tables at startup
|
|
314
|
+
await this.ensureAllTables();
|
|
315
|
+
// Discover workspace packages for cross-package resolution
|
|
316
|
+
this.workspaceResolver = new WorkspaceResolver(this.rootPath);
|
|
317
|
+
await this.workspaceResolver.discover();
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Ensure all graph tables exist with proper schema.
|
|
321
|
+
* Safe to call multiple times (idempotent).
|
|
322
|
+
*/
|
|
323
|
+
async ensureAllTables() {
|
|
324
|
+
if (!this.initialized) {
|
|
325
|
+
await this.initialize();
|
|
326
|
+
return; // initialize() already called ensureAllTables()
|
|
327
|
+
}
|
|
328
|
+
await this.ensureEdgesTable();
|
|
329
|
+
await this.ensureExportsTable();
|
|
330
|
+
await this.ensureModulesTable();
|
|
331
|
+
await this.ensureEmbeddedTable();
|
|
332
|
+
await this.ensureGraphFileTable();
|
|
333
|
+
}
|
|
334
|
+
async getTsConfig() {
|
|
335
|
+
if (this.tsConfigLoaded)
|
|
336
|
+
return this.tsConfig;
|
|
337
|
+
this.tsConfigLoaded = true;
|
|
338
|
+
this.tsConfig = await loadTSConfig(this.rootPath, this.codeConfig.graph.resolution.tsConfigPath);
|
|
339
|
+
return this.tsConfig;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Discover all tsconfig.json files in the project.
|
|
343
|
+
* Call once before processing to populate the cache.
|
|
344
|
+
*/
|
|
345
|
+
async discoverAllTsConfigs() {
|
|
346
|
+
if (this.tsConfigsDiscovered)
|
|
347
|
+
return;
|
|
348
|
+
this.tsConfigsDiscovered = true;
|
|
349
|
+
try {
|
|
350
|
+
const tsConfigFiles = await glob(path.join(this.rootPath, '**/tsconfig.json'), {
|
|
351
|
+
ignore: ['**/node_modules/**'],
|
|
352
|
+
nodir: true,
|
|
353
|
+
});
|
|
354
|
+
for (const configPath of tsConfigFiles) {
|
|
355
|
+
const dir = normalizePath(path.relative(this.rootPath, path.dirname(configPath)));
|
|
356
|
+
const config = await loadTSConfig(this.rootPath, path.join(dir, 'tsconfig.json'));
|
|
357
|
+
if (config) {
|
|
358
|
+
this.tsConfigCache.set(dir || '.', config);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// Ignore errors during discovery
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Get the tsconfig for a specific file by walking up directories.
|
|
368
|
+
* Falls back to root tsconfig or null if none found.
|
|
369
|
+
*/
|
|
370
|
+
async getTsConfigForFile(fileId) {
|
|
371
|
+
// Ensure configs are discovered
|
|
372
|
+
await this.discoverAllTsConfigs();
|
|
373
|
+
// Walk up from file's directory to find closest tsconfig
|
|
374
|
+
let dir = normalizePath(path.dirname(fileId));
|
|
375
|
+
while (true) {
|
|
376
|
+
const cachedConfig = this.tsConfigCache.get(dir);
|
|
377
|
+
if (cachedConfig !== undefined) {
|
|
378
|
+
return cachedConfig;
|
|
379
|
+
}
|
|
380
|
+
// Try root
|
|
381
|
+
if (dir === '.' || dir === '') {
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
// Go up one level
|
|
385
|
+
const parent = normalizePath(path.dirname(dir));
|
|
386
|
+
if (parent === dir)
|
|
387
|
+
break;
|
|
388
|
+
dir = parent;
|
|
389
|
+
}
|
|
390
|
+
// Fallback to root tsconfig (from config or auto-detect)
|
|
391
|
+
return this.getTsConfig();
|
|
392
|
+
}
|
|
393
|
+
async replaceTable(tableName, seedRow, rows) {
|
|
394
|
+
const table = await this.db.createTable(tableName, [seedRow, ...rows], { mode: 'overwrite' });
|
|
395
|
+
if ('id' in seedRow) {
|
|
396
|
+
await table.delete("id = '__init__'");
|
|
397
|
+
}
|
|
398
|
+
else if ('path' in seedRow) {
|
|
399
|
+
await table.delete("path = '__init__'");
|
|
400
|
+
}
|
|
401
|
+
return table;
|
|
402
|
+
}
|
|
403
|
+
async ensureChunksTable() {
|
|
404
|
+
if (this.chunksTable)
|
|
405
|
+
return this.chunksTable;
|
|
406
|
+
this.chunksTable = await this.db.openTable('code_chunks');
|
|
407
|
+
return this.chunksTable;
|
|
408
|
+
}
|
|
409
|
+
async ensureEdgesTable() {
|
|
410
|
+
if (this.edgesTable)
|
|
411
|
+
return this.edgesTable;
|
|
412
|
+
try {
|
|
413
|
+
this.edgesTable = await this.db.openTable('code_edges');
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// Table doesn't exist, create it
|
|
417
|
+
this.edgesTable = await this.db.createTable('code_edges', [{
|
|
418
|
+
id: '__init__',
|
|
419
|
+
source_id: '',
|
|
420
|
+
target_id: '',
|
|
421
|
+
edge_type: 'IMPORTS_FILE',
|
|
422
|
+
source_file: '',
|
|
423
|
+
target_file: '',
|
|
424
|
+
target_module: '',
|
|
425
|
+
confidence: 'resolved',
|
|
426
|
+
metadata: '',
|
|
427
|
+
}], { mode: 'overwrite' });
|
|
428
|
+
await this.edgesTable.delete("id = '__init__'");
|
|
429
|
+
}
|
|
430
|
+
return this.edgesTable;
|
|
431
|
+
}
|
|
432
|
+
async ensureExportsTable() {
|
|
433
|
+
if (this.exportsTable)
|
|
434
|
+
return this.exportsTable;
|
|
435
|
+
try {
|
|
436
|
+
this.exportsTable = await this.db.openTable('code_exports');
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
this.exportsTable = await this.db.createTable('code_exports', [{
|
|
440
|
+
id: '__init__',
|
|
441
|
+
file_id: '',
|
|
442
|
+
export_name: '',
|
|
443
|
+
symbol_id: '',
|
|
444
|
+
is_reexport: false,
|
|
445
|
+
original_file: '',
|
|
446
|
+
}], { mode: 'overwrite' });
|
|
447
|
+
await this.exportsTable.delete("id = '__init__'");
|
|
448
|
+
}
|
|
449
|
+
return this.exportsTable;
|
|
450
|
+
}
|
|
451
|
+
async ensureEmbeddedTable() {
|
|
452
|
+
if (this.embeddedTable)
|
|
453
|
+
return this.embeddedTable;
|
|
454
|
+
try {
|
|
455
|
+
this.embeddedTable = await this.db.openTable('code_embedded');
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
this.embeddedTable = await this.db.createTable('code_embedded', [{
|
|
459
|
+
id: '__init__',
|
|
460
|
+
host_file_id: '',
|
|
461
|
+
host_symbol_id: '',
|
|
462
|
+
embedded_type: 'graphql',
|
|
463
|
+
content: '',
|
|
464
|
+
start_offset: 0,
|
|
465
|
+
end_offset: 0,
|
|
466
|
+
start_line: 0,
|
|
467
|
+
end_line: 0,
|
|
468
|
+
}], { mode: 'overwrite' });
|
|
469
|
+
await this.embeddedTable.delete("id = '__init__'");
|
|
470
|
+
}
|
|
471
|
+
return this.embeddedTable;
|
|
472
|
+
}
|
|
473
|
+
async ensureGraphFileTable() {
|
|
474
|
+
if (this.graphFileTable)
|
|
475
|
+
return this.graphFileTable;
|
|
476
|
+
try {
|
|
477
|
+
this.graphFileTable = await this.db.openTable('code_graph_file_index');
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// Use snake_case column names for LanceDB SQL compatibility
|
|
481
|
+
this.graphFileTable = await this.db.createTable('code_graph_file_index', [{
|
|
482
|
+
path: '__init__',
|
|
483
|
+
content_hash: '',
|
|
484
|
+
indexed_at: 0,
|
|
485
|
+
}], { mode: 'overwrite' });
|
|
486
|
+
await this.graphFileTable.delete("path = '__init__'");
|
|
487
|
+
}
|
|
488
|
+
return this.graphFileTable;
|
|
489
|
+
}
|
|
490
|
+
async ensureModulesTable() {
|
|
491
|
+
if (this.modulesTable)
|
|
492
|
+
return this.modulesTable;
|
|
493
|
+
try {
|
|
494
|
+
this.modulesTable = await this.db.openTable('code_modules');
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// Use snake_case column names for LanceDB SQL compatibility
|
|
498
|
+
this.modulesTable = await this.db.createTable('code_modules', [{
|
|
499
|
+
id: '__init__',
|
|
500
|
+
type: 'npm',
|
|
501
|
+
package_name: '',
|
|
502
|
+
path: '',
|
|
503
|
+
version: '',
|
|
504
|
+
exports: '[]',
|
|
505
|
+
}], { mode: 'overwrite' });
|
|
506
|
+
await this.modulesTable.delete("id = '__init__'");
|
|
507
|
+
}
|
|
508
|
+
return this.modulesTable;
|
|
509
|
+
}
|
|
510
|
+
async addEdges(edges) {
|
|
511
|
+
if (edges.length === 0)
|
|
512
|
+
return;
|
|
513
|
+
const table = await this.ensureEdgesTable();
|
|
514
|
+
// Convert to snake_case records for LanceDB
|
|
515
|
+
await table.add(edges.map(toEdgeRecord));
|
|
516
|
+
}
|
|
517
|
+
async addExports(exports) {
|
|
518
|
+
if (exports.length === 0)
|
|
519
|
+
return;
|
|
520
|
+
const table = await this.ensureExportsTable();
|
|
521
|
+
// Convert to snake_case records for LanceDB
|
|
522
|
+
await table.add(exports.map(toExportRecord));
|
|
523
|
+
}
|
|
524
|
+
async addEmbedded(chunks) {
|
|
525
|
+
if (chunks.length === 0)
|
|
526
|
+
return;
|
|
527
|
+
const table = await this.ensureEmbeddedTable();
|
|
528
|
+
// Convert EmbeddedChunk (camelCase) to EmbeddedRecord (snake_case) for LanceDB
|
|
529
|
+
await table.add(chunks.map(toEmbeddedRecord));
|
|
530
|
+
}
|
|
531
|
+
async addModules(modules) {
|
|
532
|
+
if (modules.length === 0)
|
|
533
|
+
return;
|
|
534
|
+
const table = await this.ensureModulesTable();
|
|
535
|
+
// Filter out modules that already exist
|
|
536
|
+
const existing = new Set();
|
|
537
|
+
const existingRows = await table.query().select(['id']).toArray();
|
|
538
|
+
for (const row of existingRows) {
|
|
539
|
+
existing.add(row.id);
|
|
540
|
+
}
|
|
541
|
+
const newModules = modules.filter(m => !existing.has(m.id));
|
|
542
|
+
if (newModules.length > 0) {
|
|
543
|
+
// Convert to snake_case records for LanceDB
|
|
544
|
+
await table.add(newModules.map(toModuleRecord));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async updateGraphFileRecords(recordsOrFileId, contentHash, mtime) {
|
|
548
|
+
const table = await this.ensureGraphFileTable();
|
|
549
|
+
// Overload: single record
|
|
550
|
+
if (typeof recordsOrFileId === 'string') {
|
|
551
|
+
const path = recordsOrFileId;
|
|
552
|
+
await table.delete(`path = '${path.replace(/'/g, "''")}'`);
|
|
553
|
+
await table.add([{ path, content_hash: contentHash, indexed_at: mtime }]);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// Overload: array of records (already in snake_case)
|
|
557
|
+
const records = recordsOrFileId;
|
|
558
|
+
if (records.length === 0)
|
|
559
|
+
return;
|
|
560
|
+
for (const record of records) {
|
|
561
|
+
await table.delete(`path = '${record.path.replace(/'/g, "''")}'`);
|
|
562
|
+
}
|
|
563
|
+
await table.add(records);
|
|
564
|
+
}
|
|
565
|
+
async listCodeFiles() {
|
|
566
|
+
const include = this.codeConfig.include ?? DEFAULT_INCLUDE;
|
|
567
|
+
const exclude = this.codeConfig.exclude ?? DEFAULT_EXCLUDE;
|
|
568
|
+
const files = [];
|
|
569
|
+
for (const pattern of include) {
|
|
570
|
+
const matches = await glob(path.join(this.rootPath, pattern), {
|
|
571
|
+
ignore: exclude,
|
|
572
|
+
nodir: true,
|
|
573
|
+
});
|
|
574
|
+
files.push(...matches);
|
|
575
|
+
}
|
|
576
|
+
return [...new Set(files)]
|
|
577
|
+
.map((filePath) => normalizePath(path.relative(this.rootPath, filePath)));
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Parse a single file on-demand for graph analysis (without embeddings).
|
|
581
|
+
* Used when import resolution discovers files outside the indexed set.
|
|
582
|
+
* Returns the chunks or null if file cannot be parsed.
|
|
583
|
+
*/
|
|
584
|
+
async parseFileOnDemand(fileId) {
|
|
585
|
+
const absolutePath = path.resolve(this.rootPath, fileId);
|
|
586
|
+
// Check if file is within project and not excluded
|
|
587
|
+
if (!this.isFileAllowedForGraph(fileId)) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
592
|
+
const language = this.parser.getLanguageForFile(absolutePath);
|
|
593
|
+
if (!language)
|
|
594
|
+
return null;
|
|
595
|
+
const tree = await this.parser.parse(content, language);
|
|
596
|
+
if (!tree)
|
|
597
|
+
return null;
|
|
598
|
+
// Use tree-sitter to extract symbols (lightweight, no embeddings)
|
|
599
|
+
const extractedSymbols = this.parser.extractSymbols(tree, language);
|
|
600
|
+
const chunks = [];
|
|
601
|
+
for (const sym of extractedSymbols) {
|
|
602
|
+
const chunk = {
|
|
603
|
+
id: `${fileId}:${sym.name}:${sym.startLine}`,
|
|
604
|
+
path: fileId,
|
|
605
|
+
language,
|
|
606
|
+
symbolType: sym.type,
|
|
607
|
+
symbolName: sym.name,
|
|
608
|
+
signature: null,
|
|
609
|
+
parentSymbol: null,
|
|
610
|
+
scope: [],
|
|
611
|
+
content: '', // Empty - not needed for graph analysis
|
|
612
|
+
startLine: sym.startLine,
|
|
613
|
+
endLine: sym.endLine,
|
|
614
|
+
docstring: null,
|
|
615
|
+
modified: Date.now(),
|
|
616
|
+
contentHash: '',
|
|
617
|
+
};
|
|
618
|
+
chunks.push(chunk);
|
|
619
|
+
}
|
|
620
|
+
if (chunks.length > 0) {
|
|
621
|
+
chunks.sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
|
|
622
|
+
return chunks;
|
|
623
|
+
}
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Check if a file is allowed for graph analysis.
|
|
632
|
+
* Files must be within project root (or workspace root for monorepos) and not in node_modules.
|
|
633
|
+
*/
|
|
634
|
+
isFileAllowedForGraph(fileId) {
|
|
635
|
+
// Skip node_modules
|
|
636
|
+
if (fileId.includes('node_modules')) {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
// Must be a supported file type
|
|
640
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(fileId)) {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
// Absolute paths are not allowed
|
|
644
|
+
if (path.isAbsolute(fileId)) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
// Check if within project root
|
|
648
|
+
if (!fileId.startsWith('..')) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
// For paths starting with .., check if they're within workspace root
|
|
652
|
+
const workspaceRoot = this.workspaceResolver?.getWorkspaceRoot();
|
|
653
|
+
if (!workspaceRoot) {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
// Resolve the absolute path and check if it's within workspace root
|
|
657
|
+
const absolutePath = path.resolve(this.rootPath, fileId);
|
|
658
|
+
const relativeToWorkspace = path.relative(workspaceRoot, absolutePath);
|
|
659
|
+
// Must be within workspace root and not in node_modules
|
|
660
|
+
return !relativeToWorkspace.startsWith('..') &&
|
|
661
|
+
!path.isAbsolute(relativeToWorkspace) &&
|
|
662
|
+
!relativeToWorkspace.includes('node_modules');
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Ensure files are parsed for graph analysis.
|
|
666
|
+
* Parses any files that are not already in chunksByFile.
|
|
667
|
+
*/
|
|
668
|
+
async ensureFilesParsedForGraph(fileIds, chunksByFile, symbolToFile) {
|
|
669
|
+
for (const fileId of fileIds) {
|
|
670
|
+
if (chunksByFile.has(fileId))
|
|
671
|
+
continue;
|
|
672
|
+
const chunks = await this.parseFileOnDemand(fileId);
|
|
673
|
+
if (chunks) {
|
|
674
|
+
chunksByFile.set(fileId, chunks);
|
|
675
|
+
// Update symbolToFile map
|
|
676
|
+
for (const chunk of chunks) {
|
|
677
|
+
if (chunk.symbolType !== 'file') {
|
|
678
|
+
symbolToFile.set(chunk.id, fileId);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async loadFileMetadata(files) {
|
|
685
|
+
const metadata = new Map();
|
|
686
|
+
for (const fileId of files) {
|
|
687
|
+
const absolutePath = path.resolve(this.rootPath, fileId);
|
|
688
|
+
try {
|
|
689
|
+
const [stat, content] = await Promise.all([
|
|
690
|
+
fs.stat(absolutePath),
|
|
691
|
+
fs.readFile(absolutePath, 'utf-8'),
|
|
692
|
+
]);
|
|
693
|
+
metadata.set(fileId, {
|
|
694
|
+
contentHash: generateContentHash(content),
|
|
695
|
+
mtime: stat.mtimeMs,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
// ignore unreadable files
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return metadata;
|
|
703
|
+
}
|
|
704
|
+
async loadPreviousGraphFileIndex() {
|
|
705
|
+
if (!this.graphFileTable) {
|
|
706
|
+
try {
|
|
707
|
+
this.graphFileTable = await this.db.openTable('code_graph_file_index');
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
return new Map();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const rows = await this.graphFileTable.query().toArray();
|
|
714
|
+
const result = new Map();
|
|
715
|
+
for (const row of rows) {
|
|
716
|
+
const filePath = row.path;
|
|
717
|
+
// Read snake_case columns from LanceDB
|
|
718
|
+
result.set(filePath, {
|
|
719
|
+
contentHash: row.content_hash,
|
|
720
|
+
mtime: row.indexed_at,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
async loadChunksByFile() {
|
|
726
|
+
const table = await this.ensureChunksTable();
|
|
727
|
+
const rows = await table.query().toArray();
|
|
728
|
+
const byFile = new Map();
|
|
729
|
+
for (const row of rows) {
|
|
730
|
+
// Read from snake_case columns (code_chunks table was migrated)
|
|
731
|
+
const chunk = {
|
|
732
|
+
id: row.id,
|
|
733
|
+
path: row.path,
|
|
734
|
+
language: row.language,
|
|
735
|
+
symbolType: row.symbol_type,
|
|
736
|
+
symbolName: row.symbol_name,
|
|
737
|
+
signature: row.signature,
|
|
738
|
+
parentSymbol: row.parent_symbol,
|
|
739
|
+
scope: JSON.parse(row.scope || '[]'),
|
|
740
|
+
content: row.content,
|
|
741
|
+
startLine: row.start_line,
|
|
742
|
+
endLine: row.end_line,
|
|
743
|
+
docstring: row.docstring,
|
|
744
|
+
modified: row.modified,
|
|
745
|
+
contentHash: row.content_hash,
|
|
746
|
+
};
|
|
747
|
+
const arr = byFile.get(chunk.path) ?? [];
|
|
748
|
+
arr.push(chunk);
|
|
749
|
+
byFile.set(chunk.path, arr);
|
|
750
|
+
}
|
|
751
|
+
for (const chunks of byFile.values()) {
|
|
752
|
+
chunks.sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
|
|
753
|
+
}
|
|
754
|
+
return byFile;
|
|
755
|
+
}
|
|
756
|
+
async resolveImport(spec, basePath) {
|
|
757
|
+
const cacheKey = `${basePath}::${spec}`;
|
|
758
|
+
const cached = this.moduleResolveCache.get(cacheKey);
|
|
759
|
+
if (cached)
|
|
760
|
+
return cached;
|
|
761
|
+
// Use tsconfig closest to the importing file for proper path resolution
|
|
762
|
+
const resolved = await resolveModule(spec, {
|
|
763
|
+
basePath,
|
|
764
|
+
rootPath: this.rootPath,
|
|
765
|
+
tsConfig: await this.getTsConfigForFile(basePath),
|
|
766
|
+
nodeModulesPath: path.join(this.rootPath, 'node_modules'),
|
|
767
|
+
respectTSPaths: this.codeConfig.graph.resolution.respectTSPaths,
|
|
768
|
+
workspaceResolver: this.workspaceResolver ?? undefined,
|
|
769
|
+
});
|
|
770
|
+
this.moduleResolveCache.set(cacheKey, resolved);
|
|
771
|
+
return resolved;
|
|
772
|
+
}
|
|
773
|
+
async buildFacts(files, chunksByFile, embedEnabled, onProgress) {
|
|
774
|
+
const factsPerFile = new Map();
|
|
775
|
+
const fileMetadata = new Map();
|
|
776
|
+
const embeddedChunks = [];
|
|
777
|
+
for (let i = 0; i < files.length; i++) {
|
|
778
|
+
const fileId = files[i];
|
|
779
|
+
onProgress?.(i, files.length, 'parsing');
|
|
780
|
+
const absolutePath = path.resolve(this.rootPath, fileId);
|
|
781
|
+
try {
|
|
782
|
+
const [stat, content] = await Promise.all([
|
|
783
|
+
fs.stat(absolutePath),
|
|
784
|
+
fs.readFile(absolutePath, 'utf-8'),
|
|
785
|
+
]);
|
|
786
|
+
const language = this.parser.getLanguageForFile(absolutePath);
|
|
787
|
+
const chunks = chunksByFile.get(fileId) ?? [];
|
|
788
|
+
const facts = await extractFacts(content, fileId, {
|
|
789
|
+
language,
|
|
790
|
+
parser: this.parser,
|
|
791
|
+
chunks,
|
|
792
|
+
});
|
|
793
|
+
factsPerFile.set(fileId, facts);
|
|
794
|
+
fileMetadata.set(fileId, {
|
|
795
|
+
contentHash: generateContentHash(content),
|
|
796
|
+
mtime: stat.mtimeMs,
|
|
797
|
+
});
|
|
798
|
+
if (embedEnabled && (language === 'typescript' || language === 'tsx' || language === 'javascript' || language === 'jsx')) {
|
|
799
|
+
const embedded = await extractEmbedded({
|
|
800
|
+
fileId,
|
|
801
|
+
fileContent: content,
|
|
802
|
+
chunks,
|
|
803
|
+
});
|
|
804
|
+
for (const chunk of embedded) {
|
|
805
|
+
if (chunk.embeddedType === 'graphql' && !this.codeConfig.graph.embedded.graphql) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if ((chunk.embeddedType === 'css' || chunk.embeddedType === 'styled') && !this.codeConfig.graph.embedded.css) {
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (chunk.embeddedType === 'graphql') {
|
|
812
|
+
parseGraphQLContent(chunk.content);
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
parseCSSContent(chunk.content);
|
|
816
|
+
}
|
|
817
|
+
// Keep as EmbeddedChunk (camelCase) for internal use
|
|
818
|
+
embeddedChunks.push(chunk);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
// Skip unreadable files.
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return { factsPerFile, fileMetadata, embeddedChunks };
|
|
827
|
+
}
|
|
828
|
+
buildSemanticEdges(semanticFacts, fileId, symbolToFile) {
|
|
829
|
+
const edges = [];
|
|
830
|
+
const push = (edgeType, sourceId, targetId, line) => {
|
|
831
|
+
edges.push({
|
|
832
|
+
id: createEdgeId(sourceId, targetId, edgeType),
|
|
833
|
+
sourceId,
|
|
834
|
+
targetId,
|
|
835
|
+
edgeType,
|
|
836
|
+
sourceFile: fileId,
|
|
837
|
+
targetFile: symbolToFile.get(targetId) ?? null,
|
|
838
|
+
targetModule: null,
|
|
839
|
+
confidence: 'resolved',
|
|
840
|
+
metadata: stringifyMetadata({ line }),
|
|
841
|
+
});
|
|
842
|
+
};
|
|
843
|
+
for (const rel of semanticFacts.refs) {
|
|
844
|
+
push('REFS', rel.sourceSymbol, rel.targetSymbol, rel.line);
|
|
845
|
+
}
|
|
846
|
+
for (const rel of semanticFacts.calls) {
|
|
847
|
+
push('CALLS', rel.sourceSymbol, rel.targetSymbol, rel.line);
|
|
848
|
+
}
|
|
849
|
+
for (const rel of semanticFacts.extends) {
|
|
850
|
+
push('EXTENDS', rel.sourceSymbol, rel.targetSymbol, rel.line);
|
|
851
|
+
}
|
|
852
|
+
for (const rel of semanticFacts.implements) {
|
|
853
|
+
push('IMPLEMENTS', rel.sourceSymbol, rel.targetSymbol, rel.line);
|
|
854
|
+
}
|
|
855
|
+
return edges;
|
|
856
|
+
}
|
|
857
|
+
async buildGraphFromFacts(factsPerFile, chunksByFile, qualityConfig, embeddedChunks, filesToProcess, onProgress) {
|
|
858
|
+
const edges = [];
|
|
859
|
+
const exports = [];
|
|
860
|
+
const modules = new Map();
|
|
861
|
+
const fileGraph = new Map();
|
|
862
|
+
const symbolToFile = new Map();
|
|
863
|
+
for (const [fileId, chunks] of chunksByFile) {
|
|
864
|
+
for (const chunk of chunks) {
|
|
865
|
+
if (chunk.symbolType === 'file')
|
|
866
|
+
continue;
|
|
867
|
+
symbolToFile.set(chunk.id, fileId);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const resolveOptions = {
|
|
871
|
+
basePath: '',
|
|
872
|
+
rootPath: this.rootPath,
|
|
873
|
+
tsConfig: await this.getTsConfig(),
|
|
874
|
+
nodeModulesPath: path.join(this.rootPath, 'node_modules'),
|
|
875
|
+
respectTSPaths: this.codeConfig.graph.resolution.respectTSPaths,
|
|
876
|
+
workspaceResolver: this.workspaceResolver ?? undefined,
|
|
877
|
+
};
|
|
878
|
+
// Build export map from ALL files (needed for re-export resolution)
|
|
879
|
+
const exportMapBuilder = new ExportMapBuilder(resolveOptions);
|
|
880
|
+
await exportMapBuilder.buildExportMap(factsPerFile);
|
|
881
|
+
// Phase 1: Process definitions and exports (synchronous, no I/O)
|
|
882
|
+
for (const [fileId, facts] of factsPerFile) {
|
|
883
|
+
if (filesToProcess && !filesToProcess.has(fileId))
|
|
884
|
+
continue;
|
|
885
|
+
for (const definition of facts.definitions) {
|
|
886
|
+
edges.push({
|
|
887
|
+
id: createEdgeId(fileId, definition.symbolId, 'DEFINES'),
|
|
888
|
+
sourceId: fileId,
|
|
889
|
+
targetId: definition.symbolId,
|
|
890
|
+
edgeType: 'DEFINES',
|
|
891
|
+
sourceFile: fileId,
|
|
892
|
+
targetFile: fileId,
|
|
893
|
+
targetModule: null,
|
|
894
|
+
confidence: 'resolved',
|
|
895
|
+
metadata: stringifyMetadata({ line: definition.line, kind: definition.kind }),
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
for (const exp of facts.exports) {
|
|
899
|
+
edges.push({
|
|
900
|
+
id: createEdgeId(fileId, exp.symbolId, 'EXPORTS'),
|
|
901
|
+
sourceId: fileId,
|
|
902
|
+
targetId: exp.symbolId,
|
|
903
|
+
edgeType: 'EXPORTS',
|
|
904
|
+
sourceFile: fileId,
|
|
905
|
+
targetFile: symbolToFile.get(exp.symbolId) ?? fileId,
|
|
906
|
+
targetModule: null,
|
|
907
|
+
confidence: 'resolved',
|
|
908
|
+
metadata: stringifyMetadata({ name: exp.name, line: exp.line }),
|
|
909
|
+
});
|
|
910
|
+
exports.push({
|
|
911
|
+
id: createExportId(fileId, exp.name),
|
|
912
|
+
fileId,
|
|
913
|
+
exportName: exp.name,
|
|
914
|
+
symbolId: exp.symbolId,
|
|
915
|
+
isReexport: exp.kind === 'reexport',
|
|
916
|
+
originalFile: null,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const importTasks = [];
|
|
921
|
+
for (const [fileId, facts] of factsPerFile) {
|
|
922
|
+
if (filesToProcess && !filesToProcess.has(fileId))
|
|
923
|
+
continue;
|
|
924
|
+
for (const imp of facts.imports) {
|
|
925
|
+
importTasks.push({ fileId, imp });
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
// Resolve imports in parallel with concurrency limit
|
|
929
|
+
onProgress?.(0, importTasks.length, 'resolving');
|
|
930
|
+
const CONCURRENCY = 50;
|
|
931
|
+
const resolvedImports = [];
|
|
932
|
+
for (let i = 0; i < importTasks.length; i += CONCURRENCY) {
|
|
933
|
+
const batch = importTasks.slice(i, i + CONCURRENCY);
|
|
934
|
+
const batchResults = await Promise.all(batch.map(async (task) => ({
|
|
935
|
+
task,
|
|
936
|
+
result: await this.resolveImport(task.imp.spec, task.fileId),
|
|
937
|
+
})));
|
|
938
|
+
resolvedImports.push(...batchResults);
|
|
939
|
+
onProgress?.(Math.min(i + CONCURRENCY, importTasks.length), importTasks.length, 'resolving');
|
|
940
|
+
}
|
|
941
|
+
// Phase 3: Build edges from resolved imports
|
|
942
|
+
const localImportsByFile = new Map();
|
|
943
|
+
for (const { task, result: resolved } of resolvedImports) {
|
|
944
|
+
const { fileId, imp } = task;
|
|
945
|
+
if (!localImportsByFile.has(fileId)) {
|
|
946
|
+
localImportsByFile.set(fileId, new Set());
|
|
947
|
+
}
|
|
948
|
+
const localImports = localImportsByFile.get(fileId);
|
|
949
|
+
if (resolved.type === 'local' && resolved.fileId) {
|
|
950
|
+
localImports.add(resolved.fileId);
|
|
951
|
+
edges.push({
|
|
952
|
+
id: createEdgeId(fileId, resolved.fileId, 'IMPORTS_FILE'),
|
|
953
|
+
sourceId: fileId,
|
|
954
|
+
targetId: resolved.fileId,
|
|
955
|
+
edgeType: 'IMPORTS_FILE',
|
|
956
|
+
sourceFile: fileId,
|
|
957
|
+
targetFile: resolved.fileId,
|
|
958
|
+
targetModule: resolved.module ?? null,
|
|
959
|
+
confidence: 'resolved',
|
|
960
|
+
metadata: stringifyMetadata({ spec: imp.spec, line: imp.line }),
|
|
961
|
+
});
|
|
962
|
+
for (const name of imp.names) {
|
|
963
|
+
const targetSymbol = exportMapBuilder.resolveImport(resolved.fileId, name);
|
|
964
|
+
if (!targetSymbol)
|
|
965
|
+
continue;
|
|
966
|
+
edges.push({
|
|
967
|
+
id: createEdgeId(fileId, targetSymbol, 'IMPORTS_SYMBOL'),
|
|
968
|
+
sourceId: fileId,
|
|
969
|
+
targetId: targetSymbol,
|
|
970
|
+
edgeType: 'IMPORTS_SYMBOL',
|
|
971
|
+
sourceFile: fileId,
|
|
972
|
+
targetFile: resolved.fileId,
|
|
973
|
+
targetModule: null,
|
|
974
|
+
confidence: 'resolved',
|
|
975
|
+
metadata: stringifyMetadata({ name, alias: imp.alias, line: imp.line }),
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (resolved.type === 'workspace') {
|
|
981
|
+
const moduleId = resolved.module ?? imp.spec;
|
|
982
|
+
const workspaceTargetId = resolved.subpath
|
|
983
|
+
? `WORKSPACE:${moduleId}:${resolved.subpath}`
|
|
984
|
+
: `WORKSPACE:${moduleId}`;
|
|
985
|
+
edges.push({
|
|
986
|
+
id: createEdgeId(fileId, workspaceTargetId, 'IMPORTS_FILE'),
|
|
987
|
+
sourceId: fileId,
|
|
988
|
+
targetId: workspaceTargetId,
|
|
989
|
+
edgeType: 'IMPORTS_FILE',
|
|
990
|
+
sourceFile: fileId,
|
|
991
|
+
targetFile: null,
|
|
992
|
+
targetModule: moduleId,
|
|
993
|
+
confidence: 'resolved',
|
|
994
|
+
metadata: stringifyMetadata({ spec: imp.spec, line: imp.line, subpath: resolved.subpath }),
|
|
995
|
+
});
|
|
996
|
+
if (!modules.has(workspaceTargetId)) {
|
|
997
|
+
modules.set(workspaceTargetId, {
|
|
998
|
+
id: workspaceTargetId,
|
|
999
|
+
type: 'workspace',
|
|
1000
|
+
packageName: moduleId,
|
|
1001
|
+
path: resolved.path ?? null,
|
|
1002
|
+
version: null,
|
|
1003
|
+
exports: '[]',
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (resolved.type === 'npm' || resolved.type === 'builtin') {
|
|
1009
|
+
const moduleId = resolved.module ?? imp.spec;
|
|
1010
|
+
edges.push({
|
|
1011
|
+
id: createEdgeId(fileId, moduleId, 'IMPORTS_FILE'),
|
|
1012
|
+
sourceId: fileId,
|
|
1013
|
+
targetId: moduleId,
|
|
1014
|
+
edgeType: 'IMPORTS_FILE',
|
|
1015
|
+
sourceFile: fileId,
|
|
1016
|
+
targetFile: null,
|
|
1017
|
+
targetModule: moduleId,
|
|
1018
|
+
confidence: 'resolved',
|
|
1019
|
+
metadata: stringifyMetadata({ spec: imp.spec, line: imp.line, subpath: resolved.subpath }),
|
|
1020
|
+
});
|
|
1021
|
+
if (!modules.has(moduleId)) {
|
|
1022
|
+
modules.set(moduleId, {
|
|
1023
|
+
id: moduleId,
|
|
1024
|
+
type: resolved.type === 'builtin' ? 'builtin' : 'npm',
|
|
1025
|
+
packageName: resolved.type === 'npm' ? moduleId : null,
|
|
1026
|
+
path: null,
|
|
1027
|
+
version: null,
|
|
1028
|
+
exports: '[]',
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
edges.push({
|
|
1034
|
+
id: createEdgeId(fileId, `UNRESOLVED:${imp.spec}`, 'IMPORTS_FILE'),
|
|
1035
|
+
sourceId: fileId,
|
|
1036
|
+
targetId: `UNRESOLVED:${imp.spec}`,
|
|
1037
|
+
edgeType: 'IMPORTS_FILE',
|
|
1038
|
+
sourceFile: fileId,
|
|
1039
|
+
targetFile: null,
|
|
1040
|
+
targetModule: null,
|
|
1041
|
+
confidence: 'unresolved',
|
|
1042
|
+
metadata: stringifyMetadata({ spec: imp.spec, line: imp.line, error: resolved.error }),
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
// Build fileGraph from collected local imports
|
|
1046
|
+
for (const [fileId, localImports] of localImportsByFile) {
|
|
1047
|
+
fileGraph.set(fileId, [...localImports]);
|
|
1048
|
+
}
|
|
1049
|
+
// Filter embedded chunks if processing specific files (using camelCase properties)
|
|
1050
|
+
const filteredEmbedded = filesToProcess
|
|
1051
|
+
? embeddedChunks.filter(e => filesToProcess.has(e.hostFileId))
|
|
1052
|
+
: embeddedChunks;
|
|
1053
|
+
for (const embedded of filteredEmbedded) {
|
|
1054
|
+
const hostSymbol = embedded.hostSymbolId ?? embedded.hostFileId;
|
|
1055
|
+
edges.push({
|
|
1056
|
+
id: createEdgeId(hostSymbol, embedded.id, 'EMBEDS'),
|
|
1057
|
+
sourceId: hostSymbol,
|
|
1058
|
+
targetId: embedded.id,
|
|
1059
|
+
edgeType: 'EMBEDS',
|
|
1060
|
+
sourceFile: embedded.hostFileId,
|
|
1061
|
+
targetFile: embedded.hostFileId,
|
|
1062
|
+
targetModule: null,
|
|
1063
|
+
confidence: 'resolved',
|
|
1064
|
+
metadata: stringifyMetadata({ embeddedType: embedded.embeddedType, line: embedded.startLine }),
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
const fragmentToNodeId = new Map();
|
|
1068
|
+
for (const embedded of filteredEmbedded) {
|
|
1069
|
+
if (embedded.embeddedType !== 'graphql')
|
|
1070
|
+
continue;
|
|
1071
|
+
const gqlFacts = parseGraphQLContent(embedded.content);
|
|
1072
|
+
if (!gqlFacts)
|
|
1073
|
+
continue;
|
|
1074
|
+
for (const fragment of gqlFacts.fragments) {
|
|
1075
|
+
fragmentToNodeId.set(fragment.name, `${embedded.id}#fragment:${fragment.name}`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
for (const embedded of filteredEmbedded) {
|
|
1079
|
+
if (embedded.embeddedType !== 'graphql')
|
|
1080
|
+
continue;
|
|
1081
|
+
const gqlFacts = parseGraphQLContent(embedded.content);
|
|
1082
|
+
if (!gqlFacts)
|
|
1083
|
+
continue;
|
|
1084
|
+
for (const operation of gqlFacts.operations) {
|
|
1085
|
+
const operationId = `${embedded.id}#operation:${operation.name ?? 'anonymous'}`;
|
|
1086
|
+
for (const usedFragment of gqlFacts.usedFragments) {
|
|
1087
|
+
const fragmentId = fragmentToNodeId.get(usedFragment);
|
|
1088
|
+
if (!fragmentId)
|
|
1089
|
+
continue;
|
|
1090
|
+
edges.push({
|
|
1091
|
+
id: createEdgeId(operationId, fragmentId, 'USES_FRAGMENT'),
|
|
1092
|
+
sourceId: operationId,
|
|
1093
|
+
targetId: fragmentId,
|
|
1094
|
+
edgeType: 'USES_FRAGMENT',
|
|
1095
|
+
sourceFile: embedded.hostFileId,
|
|
1096
|
+
targetFile: embedded.hostFileId,
|
|
1097
|
+
targetModule: null,
|
|
1098
|
+
confidence: 'resolved',
|
|
1099
|
+
metadata: stringifyMetadata({ fragment: usedFragment }),
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (qualityConfig.useTypeChecker && this.codeConfig.graph.semantic.enabled) {
|
|
1105
|
+
const ts = await loadTypeScript();
|
|
1106
|
+
if (!ts && this.codeConfig.graph.quality === 'accurate') {
|
|
1107
|
+
throw new Error('Quality mode "accurate" requires TypeScript. Install it: npm install typescript');
|
|
1108
|
+
}
|
|
1109
|
+
if (ts) {
|
|
1110
|
+
// On-demand parsing: collect all resolved local files from import resolution
|
|
1111
|
+
// and parse any that aren't already in chunksByFile
|
|
1112
|
+
const resolvedLocalFiles = new Set();
|
|
1113
|
+
for (const { result } of resolvedImports) {
|
|
1114
|
+
if (result.type === 'local' && result.fileId) {
|
|
1115
|
+
resolvedLocalFiles.add(result.fileId);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// Also include files from reexport chains (e.g., index.ts -> embedding/factory.ts)
|
|
1119
|
+
const reexportFiles = exportMapBuilder.getResolvedLocalFiles();
|
|
1120
|
+
for (const fileId of reexportFiles) {
|
|
1121
|
+
resolvedLocalFiles.add(fileId);
|
|
1122
|
+
}
|
|
1123
|
+
// Parse files discovered through imports (outside original include patterns)
|
|
1124
|
+
await this.ensureFilesParsedForGraph([...resolvedLocalFiles], chunksByFile, symbolToFile);
|
|
1125
|
+
// Filter files for semantic analysis - include both indexed and on-demand parsed files
|
|
1126
|
+
const allFiles = [...chunksByFile.keys()].filter((fileId) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(fileId));
|
|
1127
|
+
const files = filesToProcess
|
|
1128
|
+
? allFiles.filter(f => filesToProcess.has(f))
|
|
1129
|
+
: allFiles;
|
|
1130
|
+
// Use shared TypeScript Program for all files (much faster than per-file analysis)
|
|
1131
|
+
onProgress?.(0, files.length, 'semantic');
|
|
1132
|
+
const allSemanticFacts = await analyzeAllFilesSemantics({
|
|
1133
|
+
fileIds: files,
|
|
1134
|
+
rootPath: this.rootPath,
|
|
1135
|
+
chunksByFile,
|
|
1136
|
+
onProgress: (current, total) => {
|
|
1137
|
+
onProgress?.(current, total, 'semantic');
|
|
1138
|
+
},
|
|
1139
|
+
});
|
|
1140
|
+
let semanticProcessed = 0;
|
|
1141
|
+
for (const [fileId, semanticFacts] of allSemanticFacts) {
|
|
1142
|
+
edges.push(...this.buildSemanticEdges(semanticFacts, fileId, symbolToFile));
|
|
1143
|
+
semanticProcessed++;
|
|
1144
|
+
}
|
|
1145
|
+
onProgress?.(files.length, files.length, 'semantic');
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
// Phase: Building final graph structure
|
|
1149
|
+
onProgress?.(0, 1, 'building');
|
|
1150
|
+
const dedupedEdges = new Map();
|
|
1151
|
+
for (const edge of edges) {
|
|
1152
|
+
dedupedEdges.set(edge.id, edge);
|
|
1153
|
+
}
|
|
1154
|
+
onProgress?.(1, 1, 'building');
|
|
1155
|
+
return {
|
|
1156
|
+
edges: [...dedupedEdges.values()],
|
|
1157
|
+
modules: [...modules.values()],
|
|
1158
|
+
exports,
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
buildEmbeddingRecords(files) {
|
|
1162
|
+
const records = [];
|
|
1163
|
+
for (const file of files) {
|
|
1164
|
+
const imports = [...new Set(file.facts.imports.map((imp) => imp.spec).filter(Boolean))];
|
|
1165
|
+
for (const chunk of file.chunks) {
|
|
1166
|
+
if (chunk.symbolType === 'file')
|
|
1167
|
+
continue;
|
|
1168
|
+
records.push({
|
|
1169
|
+
chunkId: chunk.id,
|
|
1170
|
+
fileId: file.fileId,
|
|
1171
|
+
symbolName: chunk.symbolName,
|
|
1172
|
+
symbolType: chunk.symbolType,
|
|
1173
|
+
startLine: chunk.startLine,
|
|
1174
|
+
endLine: chunk.endLine,
|
|
1175
|
+
imports,
|
|
1176
|
+
embeddingText: buildNodeEmbeddingText(chunk, file.facts),
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return records.sort((a, b) => a.fileId.localeCompare(b.fileId) || a.startLine - b.startLine);
|
|
1181
|
+
}
|
|
1182
|
+
async prepareGraph(quality = this.codeConfig.graph.quality, preParsedChunksByFile, onProgress) {
|
|
1183
|
+
await this.initialize();
|
|
1184
|
+
this.moduleResolveCache.clear();
|
|
1185
|
+
clearResolverCache();
|
|
1186
|
+
const qualityConfig = QUALITY_PRESETS[quality] ?? QUALITY_PRESETS.balanced;
|
|
1187
|
+
// Phase 1: Loading chunks
|
|
1188
|
+
onProgress?.(0, 1, 'loading');
|
|
1189
|
+
const chunksByFile = preParsedChunksByFile
|
|
1190
|
+
? new Map(preParsedChunksByFile)
|
|
1191
|
+
: await this.loadChunksByFile();
|
|
1192
|
+
const files = preParsedChunksByFile
|
|
1193
|
+
? [...new Set(chunksByFile.keys())]
|
|
1194
|
+
: await this.listCodeFiles();
|
|
1195
|
+
onProgress?.(1, 1, 'loading');
|
|
1196
|
+
// Phase 2: Parsing and extracting facts
|
|
1197
|
+
onProgress?.(0, files.length, 'parsing');
|
|
1198
|
+
const { factsPerFile, fileMetadata, embeddedChunks } = await this.buildFacts(files, chunksByFile, qualityConfig.embedEmbedded && (this.codeConfig.graph.embedded.css || this.codeConfig.graph.embedded.graphql), onProgress);
|
|
1199
|
+
onProgress?.(files.length, files.length, 'parsing');
|
|
1200
|
+
// Phase 3-5: Building graph (resolving, semantic, building)
|
|
1201
|
+
const graph = await this.buildGraphFromFacts(factsPerFile, chunksByFile, qualityConfig, embeddedChunks, undefined, onProgress);
|
|
1202
|
+
const embeddedByFile = new Map();
|
|
1203
|
+
for (const chunk of embeddedChunks) {
|
|
1204
|
+
// Use camelCase property from EmbeddedChunk
|
|
1205
|
+
const arr = embeddedByFile.get(chunk.hostFileId) ?? [];
|
|
1206
|
+
arr.push(chunk);
|
|
1207
|
+
embeddedByFile.set(chunk.hostFileId, arr);
|
|
1208
|
+
}
|
|
1209
|
+
const preparedFiles = [];
|
|
1210
|
+
const orderedFiles = [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
1211
|
+
for (const fileId of orderedFiles) {
|
|
1212
|
+
const facts = factsPerFile.get(fileId);
|
|
1213
|
+
const metadata = fileMetadata.get(fileId);
|
|
1214
|
+
if (!facts || !metadata)
|
|
1215
|
+
continue;
|
|
1216
|
+
const chunks = chunksByFile.get(fileId) ?? [];
|
|
1217
|
+
preparedFiles.push({
|
|
1218
|
+
fileId,
|
|
1219
|
+
language: chunks[0]?.language ?? null,
|
|
1220
|
+
contentHash: metadata.contentHash,
|
|
1221
|
+
mtime: metadata.mtime,
|
|
1222
|
+
chunks,
|
|
1223
|
+
facts,
|
|
1224
|
+
embedded: embeddedByFile.get(fileId) ?? [],
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
const symbols = preparedFiles.reduce((acc, file) => acc + file.chunks.length, 0);
|
|
1228
|
+
const stats = buildStatsFromPrepared(preparedFiles.length, symbols, graph.edges, embeddedChunks);
|
|
1229
|
+
return {
|
|
1230
|
+
quality,
|
|
1231
|
+
files: preparedFiles,
|
|
1232
|
+
edges: graph.edges,
|
|
1233
|
+
modules: graph.modules,
|
|
1234
|
+
exports: graph.exports,
|
|
1235
|
+
embeddingRecords: this.buildEmbeddingRecords(preparedFiles),
|
|
1236
|
+
stats,
|
|
1237
|
+
parseErrors: [],
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
async indexAll(quality = this.codeConfig.graph.quality, options = {}) {
|
|
1241
|
+
const { onProgress } = options;
|
|
1242
|
+
// Phase 1: Preparing graph (parsing, building facts, resolving, semantic)
|
|
1243
|
+
const prepared = await this.prepareGraph(quality, undefined, onProgress);
|
|
1244
|
+
this.reverseIndex.buildFromEdges(prepared.edges);
|
|
1245
|
+
// Phase 2: Indexing tables (5 tables total)
|
|
1246
|
+
const totalTables = 5;
|
|
1247
|
+
let tablesIndexed = 0;
|
|
1248
|
+
// Use snake_case column names for all LanceDB tables
|
|
1249
|
+
this.edgesTable = await this.replaceTable('code_edges', {
|
|
1250
|
+
id: '__init__',
|
|
1251
|
+
source_id: '',
|
|
1252
|
+
target_id: '',
|
|
1253
|
+
edge_type: 'IMPORTS_FILE',
|
|
1254
|
+
source_file: '',
|
|
1255
|
+
target_file: '',
|
|
1256
|
+
target_module: '',
|
|
1257
|
+
confidence: 'resolved',
|
|
1258
|
+
metadata: '',
|
|
1259
|
+
}, prepared.edges.map(toEdgeRecord));
|
|
1260
|
+
onProgress?.(++tablesIndexed, totalTables, 'indexing');
|
|
1261
|
+
this.modulesTable = await this.replaceTable('code_modules', {
|
|
1262
|
+
id: '__init__',
|
|
1263
|
+
type: 'npm',
|
|
1264
|
+
package_name: '',
|
|
1265
|
+
path: '',
|
|
1266
|
+
version: '',
|
|
1267
|
+
exports: '[]',
|
|
1268
|
+
}, prepared.modules.map(toModuleRecord));
|
|
1269
|
+
onProgress?.(++tablesIndexed, totalTables, 'indexing');
|
|
1270
|
+
this.exportsTable = await this.replaceTable('code_exports', {
|
|
1271
|
+
id: '__init__',
|
|
1272
|
+
file_id: '',
|
|
1273
|
+
export_name: '',
|
|
1274
|
+
symbol_id: '',
|
|
1275
|
+
is_reexport: false,
|
|
1276
|
+
original_file: '',
|
|
1277
|
+
}, prepared.exports.map(toExportRecord));
|
|
1278
|
+
onProgress?.(++tablesIndexed, totalTables, 'indexing');
|
|
1279
|
+
// Convert EmbeddedChunk (camelCase) to EmbeddedRecord (snake_case) for LanceDB
|
|
1280
|
+
const embeddedRecords = prepared.files.flatMap((file) => file.embedded).map(toEmbeddedRecord);
|
|
1281
|
+
this.embeddedTable = await this.replaceTable('code_embedded', {
|
|
1282
|
+
id: '__init__',
|
|
1283
|
+
host_file_id: '',
|
|
1284
|
+
host_symbol_id: '',
|
|
1285
|
+
embedded_type: 'graphql',
|
|
1286
|
+
content: '',
|
|
1287
|
+
start_offset: 0,
|
|
1288
|
+
end_offset: 0,
|
|
1289
|
+
start_line: 0,
|
|
1290
|
+
end_line: 0,
|
|
1291
|
+
}, embeddedRecords);
|
|
1292
|
+
onProgress?.(++tablesIndexed, totalTables, 'indexing');
|
|
1293
|
+
const graphFileRows = prepared.files.map((file) => ({
|
|
1294
|
+
path: file.fileId,
|
|
1295
|
+
content_hash: file.contentHash,
|
|
1296
|
+
indexed_at: file.mtime,
|
|
1297
|
+
}));
|
|
1298
|
+
this.graphFileTable = await this.replaceTable('code_graph_file_index', {
|
|
1299
|
+
path: '__init__',
|
|
1300
|
+
content_hash: '',
|
|
1301
|
+
indexed_at: 0,
|
|
1302
|
+
}, graphFileRows);
|
|
1303
|
+
onProgress?.(++tablesIndexed, totalTables, 'indexing');
|
|
1304
|
+
return prepared.stats;
|
|
1305
|
+
}
|
|
1306
|
+
async incrementalUpdate() {
|
|
1307
|
+
await this.initialize();
|
|
1308
|
+
this.moduleResolveCache.clear();
|
|
1309
|
+
clearResolverCache();
|
|
1310
|
+
const files = await this.listCodeFiles();
|
|
1311
|
+
const current = await this.loadFileMetadata(files);
|
|
1312
|
+
const previous = await this.loadPreviousGraphFileIndex();
|
|
1313
|
+
const existingEdges = await this.getAllEdges();
|
|
1314
|
+
this.reverseIndex.buildFromEdges(existingEdges);
|
|
1315
|
+
const changes = await detectChanges(current, previous, this.reverseIndex);
|
|
1316
|
+
if (changes.modified.length === 0 && changes.deleted.length === 0 && changes.affected.length === 0) {
|
|
1317
|
+
return { updated: 0, deleted: 0, affected: 0 };
|
|
1318
|
+
}
|
|
1319
|
+
// Calculate total files to update
|
|
1320
|
+
const filesToUpdate = new Set([...changes.modified, ...changes.affected]);
|
|
1321
|
+
const totalFiles = files.length;
|
|
1322
|
+
// If more than 30% of files changed, do full reindex (more efficient)
|
|
1323
|
+
if (filesToUpdate.size > totalFiles * 0.3) {
|
|
1324
|
+
await this.indexAll();
|
|
1325
|
+
return {
|
|
1326
|
+
updated: changes.modified.length,
|
|
1327
|
+
deleted: changes.deleted.length,
|
|
1328
|
+
affected: changes.affected.length,
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
// Incremental update: only process changed files
|
|
1332
|
+
const qualityConfig = QUALITY_PRESETS[this.codeConfig.graph.quality] ?? QUALITY_PRESETS.balanced;
|
|
1333
|
+
// Step 1: Delete old data for files that will be updated or deleted
|
|
1334
|
+
const filesToDelete = new Set([...filesToUpdate, ...changes.deleted]);
|
|
1335
|
+
for (const fileId of filesToDelete) {
|
|
1336
|
+
await this.deleteByFile(fileId);
|
|
1337
|
+
}
|
|
1338
|
+
// Step 2: Load existing chunks for ALL files (needed for symbolToFile mapping)
|
|
1339
|
+
const chunksByFile = await this.loadChunksByFile();
|
|
1340
|
+
// Step 3: Build facts for ALL files (needed for ExportMapBuilder to resolve re-exports)
|
|
1341
|
+
// This is fast as it doesn't involve embeddings
|
|
1342
|
+
const { factsPerFile, fileMetadata, embeddedChunks } = await this.buildFacts(files, chunksByFile, qualityConfig.embedEmbedded && (this.codeConfig.graph.embedded.css || this.codeConfig.graph.embedded.graphql));
|
|
1343
|
+
// Step 4: Build graph only for modified + affected files
|
|
1344
|
+
const graph = await this.buildGraphFromFacts(factsPerFile, chunksByFile, qualityConfig, embeddedChunks, filesToUpdate // Only process these files
|
|
1345
|
+
);
|
|
1346
|
+
// Step 5: Add new edges, exports, modules, embedded
|
|
1347
|
+
await this.addEdges(graph.edges);
|
|
1348
|
+
await this.addExports(graph.exports);
|
|
1349
|
+
await this.addModules(graph.modules);
|
|
1350
|
+
// Filter embedded for updated files only (using camelCase property)
|
|
1351
|
+
const embeddedForUpdatedFiles = embeddedChunks.filter(e => filesToUpdate.has(e.hostFileId));
|
|
1352
|
+
await this.addEmbedded(embeddedForUpdatedFiles);
|
|
1353
|
+
// Step 6: Update graph file index for modified + affected files (snake_case columns)
|
|
1354
|
+
const graphFileRecords = [];
|
|
1355
|
+
for (const fileId of filesToUpdate) {
|
|
1356
|
+
const metadata = fileMetadata.get(fileId);
|
|
1357
|
+
if (metadata) {
|
|
1358
|
+
graphFileRecords.push({
|
|
1359
|
+
path: fileId,
|
|
1360
|
+
content_hash: metadata.contentHash,
|
|
1361
|
+
indexed_at: metadata.mtime,
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
await this.updateGraphFileRecords(graphFileRecords);
|
|
1366
|
+
// Step 7: Update reverse index with new edges
|
|
1367
|
+
this.reverseIndex.buildFromEdges(await this.getAllEdges());
|
|
1368
|
+
return {
|
|
1369
|
+
updated: changes.modified.length,
|
|
1370
|
+
deleted: changes.deleted.length,
|
|
1371
|
+
affected: changes.affected.length,
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
async updateFiles(changedFiles) {
|
|
1375
|
+
if (changedFiles.length === 0) {
|
|
1376
|
+
return { updated: 0, deleted: 0, affected: 0 };
|
|
1377
|
+
}
|
|
1378
|
+
await this.initialize();
|
|
1379
|
+
await this.ensureAllTables();
|
|
1380
|
+
this.moduleResolveCache.clear();
|
|
1381
|
+
clearResolverCache();
|
|
1382
|
+
// Build reverse index from existing edges to find affected files
|
|
1383
|
+
const existingEdges = await this.getAllEdges();
|
|
1384
|
+
this.reverseIndex.buildFromEdges(existingEdges);
|
|
1385
|
+
// Find affected files (importers of changed files)
|
|
1386
|
+
const affectedSet = new Set();
|
|
1387
|
+
const changedSet = new Set(changedFiles.map(f => normalizePath(f)));
|
|
1388
|
+
for (const fileId of changedSet) {
|
|
1389
|
+
const transitiveImporters = this.reverseIndex.getTransitiveImporters(fileId, 2);
|
|
1390
|
+
for (const importer of transitiveImporters) {
|
|
1391
|
+
if (!changedSet.has(importer)) {
|
|
1392
|
+
affectedSet.add(importer);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const filesToUpdate = new Set([...changedSet, ...affectedSet]);
|
|
1397
|
+
const allFiles = await this.listCodeFiles();
|
|
1398
|
+
// If more than 30% of files affected, do full reindex
|
|
1399
|
+
if (filesToUpdate.size > allFiles.length * 0.3) {
|
|
1400
|
+
await this.indexAll();
|
|
1401
|
+
return { updated: changedFiles.length, deleted: 0, affected: affectedSet.size };
|
|
1402
|
+
}
|
|
1403
|
+
// Incremental update
|
|
1404
|
+
const qualityConfig = QUALITY_PRESETS[this.codeConfig.graph.quality] ?? QUALITY_PRESETS.balanced;
|
|
1405
|
+
// Delete old data for files to update
|
|
1406
|
+
for (const fileId of filesToUpdate) {
|
|
1407
|
+
await this.deleteByFile(fileId);
|
|
1408
|
+
}
|
|
1409
|
+
// Load chunks and build facts
|
|
1410
|
+
const chunksByFile = await this.loadChunksByFile();
|
|
1411
|
+
const { factsPerFile, fileMetadata, embeddedChunks } = await this.buildFacts(allFiles, chunksByFile, qualityConfig.embedEmbedded && (this.codeConfig.graph.embedded.css || this.codeConfig.graph.embedded.graphql));
|
|
1412
|
+
// Build graph only for updated files
|
|
1413
|
+
const graph = await this.buildGraphFromFacts(factsPerFile, chunksByFile, qualityConfig, embeddedChunks, filesToUpdate);
|
|
1414
|
+
// Add new data
|
|
1415
|
+
await this.addEdges(graph.edges);
|
|
1416
|
+
await this.addExports(graph.exports);
|
|
1417
|
+
await this.addModules(graph.modules);
|
|
1418
|
+
// Filter embedded for updated files (using camelCase property)
|
|
1419
|
+
const embeddedForUpdatedFiles = embeddedChunks.filter(e => filesToUpdate.has(e.hostFileId));
|
|
1420
|
+
await this.addEmbedded(embeddedForUpdatedFiles);
|
|
1421
|
+
// Update file index (snake_case columns)
|
|
1422
|
+
const graphFileRecords = [];
|
|
1423
|
+
for (const fileId of filesToUpdate) {
|
|
1424
|
+
const metadata = fileMetadata.get(fileId);
|
|
1425
|
+
if (metadata) {
|
|
1426
|
+
graphFileRecords.push({
|
|
1427
|
+
path: fileId,
|
|
1428
|
+
content_hash: metadata.contentHash,
|
|
1429
|
+
indexed_at: metadata.mtime,
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
await this.updateGraphFileRecords(graphFileRecords);
|
|
1434
|
+
// Update reverse index
|
|
1435
|
+
this.reverseIndex.buildFromEdges(await this.getAllEdges());
|
|
1436
|
+
return { updated: changedFiles.length, deleted: 0, affected: affectedSet.size };
|
|
1437
|
+
}
|
|
1438
|
+
async removeFile(filePath) {
|
|
1439
|
+
await this.initialize();
|
|
1440
|
+
const normalized = normalizePath(filePath);
|
|
1441
|
+
await this.deleteByFile(normalized);
|
|
1442
|
+
}
|
|
1443
|
+
async deleteByFile(fileId) {
|
|
1444
|
+
if (!this.edgesTable) {
|
|
1445
|
+
try {
|
|
1446
|
+
this.edgesTable = await this.db.openTable('code_edges');
|
|
1447
|
+
}
|
|
1448
|
+
catch {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
// Use snake_case column names for LanceDB SQL queries
|
|
1453
|
+
await this.edgesTable.delete(`source_file = '${fileId.replace(/'/g, "''")}'`);
|
|
1454
|
+
await this.edgesTable.delete(`target_file = '${fileId.replace(/'/g, "''")}'`);
|
|
1455
|
+
if (this.exportsTable) {
|
|
1456
|
+
await this.exportsTable.delete(`file_id = '${fileId.replace(/'/g, "''")}'`);
|
|
1457
|
+
}
|
|
1458
|
+
if (this.embeddedTable) {
|
|
1459
|
+
await this.embeddedTable.delete(`host_file_id = '${fileId.replace(/'/g, "''")}'`);
|
|
1460
|
+
}
|
|
1461
|
+
if (this.graphFileTable) {
|
|
1462
|
+
await this.graphFileTable.delete(`path = '${fileId.replace(/'/g, "''")}'`);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
async clear() {
|
|
1466
|
+
await this.initialize();
|
|
1467
|
+
const tables = await this.db.tableNames();
|
|
1468
|
+
for (const name of ['code_edges', 'code_modules', 'code_exports', 'code_embedded', 'code_graph_file_index']) {
|
|
1469
|
+
if (tables.includes(name)) {
|
|
1470
|
+
await this.db.dropTable(name);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
this.chunksTable = null;
|
|
1474
|
+
this.edgesTable = null;
|
|
1475
|
+
this.modulesTable = null;
|
|
1476
|
+
this.exportsTable = null;
|
|
1477
|
+
this.embeddedTable = null;
|
|
1478
|
+
this.graphFileTable = null;
|
|
1479
|
+
this.reverseIndex = new ReverseIndexBuilder();
|
|
1480
|
+
}
|
|
1481
|
+
async getAllEdges() {
|
|
1482
|
+
await this.initialize();
|
|
1483
|
+
if (!this.edgesTable) {
|
|
1484
|
+
try {
|
|
1485
|
+
this.edgesTable = await this.db.openTable('code_edges');
|
|
1486
|
+
}
|
|
1487
|
+
catch {
|
|
1488
|
+
return [];
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
const rows = await this.edgesTable.query().toArray();
|
|
1492
|
+
// Convert from snake_case LanceDB records to camelCase CodeEdge
|
|
1493
|
+
return rows.map(fromEdgeRecord);
|
|
1494
|
+
}
|
|
1495
|
+
async getChunkById(chunkId) {
|
|
1496
|
+
const table = await this.ensureChunksTable();
|
|
1497
|
+
const rows = await table.query().where(`id = '${chunkId.replace(/'/g, "''")}'`).toArray();
|
|
1498
|
+
const row = rows[0];
|
|
1499
|
+
if (!row)
|
|
1500
|
+
return null;
|
|
1501
|
+
// Read from snake_case columns
|
|
1502
|
+
return {
|
|
1503
|
+
id: row.id,
|
|
1504
|
+
path: row.path,
|
|
1505
|
+
language: row.language,
|
|
1506
|
+
symbolType: row.symbol_type,
|
|
1507
|
+
symbolName: row.symbol_name,
|
|
1508
|
+
signature: row.signature,
|
|
1509
|
+
parentSymbol: row.parent_symbol,
|
|
1510
|
+
scope: JSON.parse(row.scope || '[]'),
|
|
1511
|
+
content: row.content,
|
|
1512
|
+
startLine: row.start_line,
|
|
1513
|
+
endLine: row.end_line,
|
|
1514
|
+
docstring: row.docstring,
|
|
1515
|
+
modified: row.modified,
|
|
1516
|
+
contentHash: row.content_hash,
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
async findSymbolChunk(symbolId, symbolName) {
|
|
1520
|
+
if (symbolId) {
|
|
1521
|
+
return this.getChunkById(symbolId);
|
|
1522
|
+
}
|
|
1523
|
+
if (!symbolName)
|
|
1524
|
+
return null;
|
|
1525
|
+
const table = await this.ensureChunksTable();
|
|
1526
|
+
// Use snake_case column name in SQL query
|
|
1527
|
+
const rows = await table
|
|
1528
|
+
.query()
|
|
1529
|
+
.where(`symbol_name = '${symbolName.replace(/'/g, "''")}'`)
|
|
1530
|
+
.limit(1)
|
|
1531
|
+
.toArray();
|
|
1532
|
+
if (rows.length === 0)
|
|
1533
|
+
return null;
|
|
1534
|
+
const row = rows[0];
|
|
1535
|
+
// Read from snake_case columns
|
|
1536
|
+
return {
|
|
1537
|
+
id: row.id,
|
|
1538
|
+
path: row.path,
|
|
1539
|
+
language: row.language,
|
|
1540
|
+
symbolType: row.symbol_type,
|
|
1541
|
+
symbolName: row.symbol_name,
|
|
1542
|
+
signature: row.signature,
|
|
1543
|
+
parentSymbol: row.parent_symbol,
|
|
1544
|
+
scope: JSON.parse(row.scope || '[]'),
|
|
1545
|
+
content: row.content,
|
|
1546
|
+
startLine: row.start_line,
|
|
1547
|
+
endLine: row.end_line,
|
|
1548
|
+
docstring: row.docstring,
|
|
1549
|
+
modified: row.modified,
|
|
1550
|
+
contentHash: row.content_hash,
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
async readContextLine(fileId, line) {
|
|
1554
|
+
try {
|
|
1555
|
+
const content = await fs.readFile(path.resolve(this.rootPath, fileId), 'utf-8');
|
|
1556
|
+
const lines = content.split('\n');
|
|
1557
|
+
return (lines[line - 1] ?? '').trim();
|
|
1558
|
+
}
|
|
1559
|
+
catch {
|
|
1560
|
+
return '';
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
async graphRefs(input) {
|
|
1564
|
+
await this.initialize();
|
|
1565
|
+
const symbol = await this.findSymbolChunk(input.symbolId, input.symbolName);
|
|
1566
|
+
if (!symbol) {
|
|
1567
|
+
return createError('GRAPH_SYMBOL_NOT_FOUND', 'Symbol not found');
|
|
1568
|
+
}
|
|
1569
|
+
const includeImports = input.includeImports ?? true;
|
|
1570
|
+
const limit = input.limit ?? 50;
|
|
1571
|
+
const edges = await this.getAllEdges();
|
|
1572
|
+
const allowed = new Map([
|
|
1573
|
+
['REFS', 'ref'],
|
|
1574
|
+
['CALLS', 'call'],
|
|
1575
|
+
['EXTENDS', 'extend'],
|
|
1576
|
+
['IMPLEMENTS', 'implement'],
|
|
1577
|
+
['IMPORTS_SYMBOL', 'import'],
|
|
1578
|
+
]);
|
|
1579
|
+
const references = [];
|
|
1580
|
+
for (const edge of edges) {
|
|
1581
|
+
if (edge.targetId !== symbol.id)
|
|
1582
|
+
continue;
|
|
1583
|
+
if (!allowed.has(edge.edgeType))
|
|
1584
|
+
continue;
|
|
1585
|
+
if (!includeImports && edge.edgeType === 'IMPORTS_SYMBOL')
|
|
1586
|
+
continue;
|
|
1587
|
+
if (input.file && edge.sourceFile !== input.file)
|
|
1588
|
+
continue;
|
|
1589
|
+
const meta = parseMetadata(edge.metadata);
|
|
1590
|
+
const line = Number(meta.line ?? 1);
|
|
1591
|
+
const context = await this.readContextLine(edge.sourceFile, line);
|
|
1592
|
+
references.push({
|
|
1593
|
+
file: edge.sourceFile,
|
|
1594
|
+
line,
|
|
1595
|
+
context,
|
|
1596
|
+
type: allowed.get(edge.edgeType),
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
references.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
1600
|
+
const limited = references.slice(0, limit);
|
|
1601
|
+
return {
|
|
1602
|
+
symbol,
|
|
1603
|
+
references: limited,
|
|
1604
|
+
totalCount: references.length,
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
async buildCallNodes(rootId, direction, depth, limit) {
|
|
1608
|
+
const edges = (await this.getAllEdges()).filter((edge) => edge.edgeType === 'CALLS');
|
|
1609
|
+
const queue = [{ id: rootId, depth: 0 }];
|
|
1610
|
+
const visited = new Set([rootId]);
|
|
1611
|
+
const nodes = [];
|
|
1612
|
+
while (queue.length > 0 && nodes.length < limit) {
|
|
1613
|
+
const current = queue.shift();
|
|
1614
|
+
if (!current)
|
|
1615
|
+
break;
|
|
1616
|
+
if (current.depth >= depth)
|
|
1617
|
+
continue;
|
|
1618
|
+
const relevant = edges.filter((edge) => direction === 'callers'
|
|
1619
|
+
? edge.targetId === current.id
|
|
1620
|
+
: edge.sourceId === current.id);
|
|
1621
|
+
for (const edge of relevant) {
|
|
1622
|
+
const nextId = direction === 'callers' ? edge.sourceId : edge.targetId;
|
|
1623
|
+
if (visited.has(nextId))
|
|
1624
|
+
continue;
|
|
1625
|
+
visited.add(nextId);
|
|
1626
|
+
const chunk = await this.getChunkById(nextId);
|
|
1627
|
+
if (!chunk)
|
|
1628
|
+
continue;
|
|
1629
|
+
const meta = parseMetadata(edge.metadata);
|
|
1630
|
+
nodes.push({
|
|
1631
|
+
symbol: chunk,
|
|
1632
|
+
depth: current.depth + 1,
|
|
1633
|
+
callSites: [{
|
|
1634
|
+
file: edge.sourceFile,
|
|
1635
|
+
line: Number(meta.line ?? 1),
|
|
1636
|
+
}],
|
|
1637
|
+
children: [],
|
|
1638
|
+
});
|
|
1639
|
+
queue.push({ id: nextId, depth: current.depth + 1 });
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return nodes;
|
|
1643
|
+
}
|
|
1644
|
+
async graphCalls(input) {
|
|
1645
|
+
await this.initialize();
|
|
1646
|
+
const symbol = await this.findSymbolChunk(input.symbolId, input.symbolName);
|
|
1647
|
+
if (!symbol) {
|
|
1648
|
+
return createError('GRAPH_SYMBOL_NOT_FOUND', 'Symbol not found');
|
|
1649
|
+
}
|
|
1650
|
+
const depth = input.depth ?? 2;
|
|
1651
|
+
const limit = input.limit ?? 50;
|
|
1652
|
+
const direction = input.direction;
|
|
1653
|
+
const callers = direction === 'callees'
|
|
1654
|
+
? []
|
|
1655
|
+
: await this.buildCallNodes(symbol.id, 'callers', depth, limit);
|
|
1656
|
+
const callees = direction === 'callers'
|
|
1657
|
+
? []
|
|
1658
|
+
: await this.buildCallNodes(symbol.id, 'callees', depth, limit);
|
|
1659
|
+
return {
|
|
1660
|
+
root: symbol,
|
|
1661
|
+
callers,
|
|
1662
|
+
callees,
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
async dependencySymbols(sourceFile, targetFile) {
|
|
1666
|
+
const edges = await this.getAllEdges();
|
|
1667
|
+
return edges
|
|
1668
|
+
.filter((edge) => edge.edgeType === 'IMPORTS_SYMBOL' &&
|
|
1669
|
+
edge.sourceFile === sourceFile &&
|
|
1670
|
+
edge.targetFile === targetFile)
|
|
1671
|
+
.map((edge) => {
|
|
1672
|
+
const meta = parseMetadata(edge.metadata);
|
|
1673
|
+
return String(meta.name ?? meta.alias ?? edge.targetId);
|
|
1674
|
+
})
|
|
1675
|
+
.filter(Boolean);
|
|
1676
|
+
}
|
|
1677
|
+
async traverseImports(fileId, includeExternal, maxDepth) {
|
|
1678
|
+
const edges = (await this.getAllEdges()).filter((edge) => edge.edgeType === 'IMPORTS_FILE');
|
|
1679
|
+
const queue = [{ file: fileId, depth: 0 }];
|
|
1680
|
+
const visited = new Set([fileId]);
|
|
1681
|
+
const result = [];
|
|
1682
|
+
while (queue.length > 0) {
|
|
1683
|
+
const current = queue.shift();
|
|
1684
|
+
if (!current)
|
|
1685
|
+
break;
|
|
1686
|
+
if (current.depth >= maxDepth)
|
|
1687
|
+
continue;
|
|
1688
|
+
const outgoing = edges.filter((edge) => edge.sourceFile === current.file);
|
|
1689
|
+
for (const edge of outgoing) {
|
|
1690
|
+
const nextFile = edge.targetFile ?? edge.targetModule ?? edge.targetId;
|
|
1691
|
+
if (!includeExternal && !edge.targetFile)
|
|
1692
|
+
continue;
|
|
1693
|
+
if (!nextFile || visited.has(nextFile))
|
|
1694
|
+
continue;
|
|
1695
|
+
visited.add(nextFile);
|
|
1696
|
+
const symbols = edge.targetFile
|
|
1697
|
+
? await this.dependencySymbols(current.file, edge.targetFile)
|
|
1698
|
+
: [];
|
|
1699
|
+
result.push({
|
|
1700
|
+
file: nextFile,
|
|
1701
|
+
type: edge.targetFile ? 'local' : edge.targetModule && edge.targetModule.startsWith('node:') ? 'builtin' : 'npm',
|
|
1702
|
+
symbols,
|
|
1703
|
+
depth: current.depth + 1,
|
|
1704
|
+
children: [],
|
|
1705
|
+
});
|
|
1706
|
+
if (edge.targetFile) {
|
|
1707
|
+
queue.push({ file: edge.targetFile, depth: current.depth + 1 });
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
return result;
|
|
1712
|
+
}
|
|
1713
|
+
async traverseImporters(fileId, maxDepth) {
|
|
1714
|
+
const edges = (await this.getAllEdges()).filter((edge) => edge.edgeType === 'IMPORTS_FILE');
|
|
1715
|
+
const queue = [{ file: fileId, depth: 0 }];
|
|
1716
|
+
const visited = new Set([fileId]);
|
|
1717
|
+
const result = [];
|
|
1718
|
+
while (queue.length > 0) {
|
|
1719
|
+
const current = queue.shift();
|
|
1720
|
+
if (!current)
|
|
1721
|
+
break;
|
|
1722
|
+
if (current.depth >= maxDepth)
|
|
1723
|
+
continue;
|
|
1724
|
+
const incoming = edges.filter((edge) => edge.targetFile === current.file);
|
|
1725
|
+
for (const edge of incoming) {
|
|
1726
|
+
if (visited.has(edge.sourceFile))
|
|
1727
|
+
continue;
|
|
1728
|
+
visited.add(edge.sourceFile);
|
|
1729
|
+
result.push({
|
|
1730
|
+
file: edge.sourceFile,
|
|
1731
|
+
type: 'local',
|
|
1732
|
+
symbols: [],
|
|
1733
|
+
depth: current.depth + 1,
|
|
1734
|
+
children: [],
|
|
1735
|
+
});
|
|
1736
|
+
queue.push({ file: edge.sourceFile, depth: current.depth + 1 });
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return result;
|
|
1740
|
+
}
|
|
1741
|
+
async graphDeps(input) {
|
|
1742
|
+
await this.initialize();
|
|
1743
|
+
const depth = input.depth ?? 1;
|
|
1744
|
+
const includeExternal = input.includeExternal ?? false;
|
|
1745
|
+
// Normalize file path to match stored paths in database
|
|
1746
|
+
const fileId = normalizePath(input.file);
|
|
1747
|
+
const imports = input.direction === 'importedBy'
|
|
1748
|
+
? []
|
|
1749
|
+
: await this.traverseImports(fileId, includeExternal, depth);
|
|
1750
|
+
const importedBy = input.direction === 'imports'
|
|
1751
|
+
? []
|
|
1752
|
+
: await this.traverseImporters(fileId, depth);
|
|
1753
|
+
return {
|
|
1754
|
+
file: fileId,
|
|
1755
|
+
imports,
|
|
1756
|
+
importedBy,
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
async collectHierarchyIds(rootId, edgeType, incoming) {
|
|
1760
|
+
const edges = (await this.getAllEdges()).filter((edge) => edge.edgeType === edgeType);
|
|
1761
|
+
const queue = [rootId];
|
|
1762
|
+
const result = new Set();
|
|
1763
|
+
while (queue.length > 0) {
|
|
1764
|
+
const current = queue.shift();
|
|
1765
|
+
if (!current)
|
|
1766
|
+
break;
|
|
1767
|
+
const matches = edges.filter((edge) => incoming ? edge.targetId === current : edge.sourceId === current);
|
|
1768
|
+
for (const edge of matches) {
|
|
1769
|
+
const next = incoming ? edge.sourceId : edge.targetId;
|
|
1770
|
+
if (next === rootId || result.has(next))
|
|
1771
|
+
continue;
|
|
1772
|
+
result.add(next);
|
|
1773
|
+
queue.push(next);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return [...result];
|
|
1777
|
+
}
|
|
1778
|
+
async getChunksByIds(ids) {
|
|
1779
|
+
const chunks = [];
|
|
1780
|
+
for (const id of ids) {
|
|
1781
|
+
const chunk = await this.getChunkById(id);
|
|
1782
|
+
if (chunk)
|
|
1783
|
+
chunks.push(chunk);
|
|
1784
|
+
}
|
|
1785
|
+
return chunks;
|
|
1786
|
+
}
|
|
1787
|
+
async graphHierarchy(input) {
|
|
1788
|
+
await this.initialize();
|
|
1789
|
+
const symbol = await this.findSymbolChunk(input.symbolId, input.symbolName);
|
|
1790
|
+
if (!symbol) {
|
|
1791
|
+
return createError('GRAPH_SYMBOL_NOT_FOUND', 'Symbol not found');
|
|
1792
|
+
}
|
|
1793
|
+
const extendsIds = await this.collectHierarchyIds(symbol.id, 'EXTENDS', false);
|
|
1794
|
+
const implementsIds = await this.collectHierarchyIds(symbol.id, 'IMPLEMENTS', false);
|
|
1795
|
+
const extendedByIds = await this.collectHierarchyIds(symbol.id, 'EXTENDS', true);
|
|
1796
|
+
const implementedByIds = await this.collectHierarchyIds(symbol.id, 'IMPLEMENTS', true);
|
|
1797
|
+
return {
|
|
1798
|
+
symbol,
|
|
1799
|
+
extends: await this.getChunksByIds(extendsIds),
|
|
1800
|
+
implements: await this.getChunksByIds(implementsIds),
|
|
1801
|
+
extendedBy: await this.getChunksByIds(extendedByIds),
|
|
1802
|
+
implementedBy: await this.getChunksByIds(implementedByIds),
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
async getStats() {
|
|
1806
|
+
await this.initialize();
|
|
1807
|
+
const edges = await this.getAllEdges();
|
|
1808
|
+
const edgeTypes = edgeTypeCount();
|
|
1809
|
+
for (const edge of edges) {
|
|
1810
|
+
edgeTypes[edge.edgeType] += 1;
|
|
1811
|
+
}
|
|
1812
|
+
const chunksTable = await this.ensureChunksTable();
|
|
1813
|
+
const chunksCount = await chunksTable.countRows();
|
|
1814
|
+
let graphFiles = 0;
|
|
1815
|
+
if (this.graphFileTable) {
|
|
1816
|
+
graphFiles = await this.graphFileTable.countRows();
|
|
1817
|
+
}
|
|
1818
|
+
else {
|
|
1819
|
+
try {
|
|
1820
|
+
this.graphFileTable = await this.db.openTable('code_graph_file_index');
|
|
1821
|
+
graphFiles = await this.graphFileTable.countRows();
|
|
1822
|
+
}
|
|
1823
|
+
catch {
|
|
1824
|
+
graphFiles = 0;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
let embeddedRows = [];
|
|
1828
|
+
if (!this.embeddedTable) {
|
|
1829
|
+
try {
|
|
1830
|
+
this.embeddedTable = await this.db.openTable('code_embedded');
|
|
1831
|
+
}
|
|
1832
|
+
catch {
|
|
1833
|
+
this.embeddedTable = null;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
if (this.embeddedTable) {
|
|
1837
|
+
embeddedRows = (await this.embeddedTable.query().toArray());
|
|
1838
|
+
}
|
|
1839
|
+
const embeddedByType = {
|
|
1840
|
+
graphql: 0,
|
|
1841
|
+
css: 0,
|
|
1842
|
+
styled: 0,
|
|
1843
|
+
};
|
|
1844
|
+
for (const row of embeddedRows) {
|
|
1845
|
+
// Use snake_case column name from LanceDB record
|
|
1846
|
+
const embeddedType = row.embedded_type;
|
|
1847
|
+
if (embeddedType) {
|
|
1848
|
+
embeddedByType[embeddedType] += 1;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return {
|
|
1852
|
+
filesIndexed: graphFiles,
|
|
1853
|
+
symbols: chunksCount,
|
|
1854
|
+
edges: edges.length,
|
|
1855
|
+
edgeTypes,
|
|
1856
|
+
unresolvedImports: edges.filter((edge) => edge.edgeType === 'IMPORTS_FILE' && edge.confidence === 'unresolved').length,
|
|
1857
|
+
embeddedChunks: embeddedRows.length,
|
|
1858
|
+
embeddedByType,
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
async exportGraph(format = 'json') {
|
|
1862
|
+
const edges = await this.getAllEdges();
|
|
1863
|
+
if (format === 'json') {
|
|
1864
|
+
return JSON.stringify(edges, null, 2);
|
|
1865
|
+
}
|
|
1866
|
+
const lines = ['digraph CodeGraph {'];
|
|
1867
|
+
for (const edge of edges) {
|
|
1868
|
+
const source = edge.sourceId.replace(/"/g, '\\"');
|
|
1869
|
+
const target = edge.targetId.replace(/"/g, '\\"');
|
|
1870
|
+
lines.push(` "${source}" -> "${target}" [label="${edge.edgeType}"];`);
|
|
1871
|
+
}
|
|
1872
|
+
lines.push('}');
|
|
1873
|
+
return lines.join('\n');
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
//# sourceMappingURL=index.js.map
|