sylvan 1.0.0__tar.gz
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.
- sylvan-1.0.0/PKG-INFO +105 -0
- sylvan-1.0.0/README.md +75 -0
- sylvan-1.0.0/pyproject.toml +70 -0
- sylvan-1.0.0/src/sylvan/__init__.py +8 -0
- sylvan-1.0.0/src/sylvan/__main__.py +6 -0
- sylvan-1.0.0/src/sylvan/analysis/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/analysis/impact/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/analysis/impact/blast_radius.py +135 -0
- sylvan-1.0.0/src/sylvan/analysis/impact/cross_repo.py +223 -0
- sylvan-1.0.0/src/sylvan/analysis/quality/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/analysis/quality/code_smells.py +162 -0
- sylvan-1.0.0/src/sylvan/analysis/quality/dead_code.py +90 -0
- sylvan-1.0.0/src/sylvan/analysis/quality/duplication.py +132 -0
- sylvan-1.0.0/src/sylvan/analysis/quality/quality_metrics.py +207 -0
- sylvan-1.0.0/src/sylvan/analysis/quality/security_scanner.py +156 -0
- sylvan-1.0.0/src/sylvan/analysis/quality/test_coverage.py +124 -0
- sylvan-1.0.0/src/sylvan/analysis/structure/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/analysis/structure/class_hierarchy.py +160 -0
- sylvan-1.0.0/src/sylvan/analysis/structure/reference_graph.py +209 -0
- sylvan-1.0.0/src/sylvan/cli.py +839 -0
- sylvan-1.0.0/src/sylvan/cluster/__init__.py +1 -0
- sylvan-1.0.0/src/sylvan/cluster/api.py +114 -0
- sylvan-1.0.0/src/sylvan/cluster/discovery.py +153 -0
- sylvan-1.0.0/src/sylvan/cluster/heartbeat.py +246 -0
- sylvan-1.0.0/src/sylvan/cluster/proxy.py +65 -0
- sylvan-1.0.0/src/sylvan/cluster/state.py +63 -0
- sylvan-1.0.0/src/sylvan/config.py +599 -0
- sylvan-1.0.0/src/sylvan/context.py +146 -0
- sylvan-1.0.0/src/sylvan/dashboard/__init__.py +1 -0
- sylvan-1.0.0/src/sylvan/dashboard/app.py +874 -0
- sylvan-1.0.0/src/sylvan/dashboard/server.py +73 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/base.html +584 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/blast_radius.html +85 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/libraries.html +41 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/overview.html +8 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/partials/blast_radius_result.html +92 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/partials/quality_report.html +128 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/partials/search_results.html +37 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/partials/session_stats.html +197 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/partials/stats.html +172 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/quality.html +35 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/search.html +43 -0
- sylvan-1.0.0/src/sylvan/dashboard/templates/session.html +8 -0
- sylvan-1.0.0/src/sylvan/database/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/database/backends/__init__.py +1 -0
- sylvan-1.0.0/src/sylvan/database/backends/base.py +318 -0
- sylvan-1.0.0/src/sylvan/database/backends/postgres/__init__.py +6 -0
- sylvan-1.0.0/src/sylvan/database/backends/postgres/backend.py +214 -0
- sylvan-1.0.0/src/sylvan/database/backends/postgres/dialect.py +185 -0
- sylvan-1.0.0/src/sylvan/database/backends/sqlite/__init__.py +6 -0
- sylvan-1.0.0/src/sylvan/database/backends/sqlite/backend.py +206 -0
- sylvan-1.0.0/src/sylvan/database/backends/sqlite/dialect.py +164 -0
- sylvan-1.0.0/src/sylvan/database/builder/__init__.py +30 -0
- sylvan-1.0.0/src/sylvan/database/builder/blueprint.py +401 -0
- sylvan-1.0.0/src/sylvan/database/builder/schema.py +448 -0
- sylvan-1.0.0/src/sylvan/database/connection.py +40 -0
- sylvan-1.0.0/src/sylvan/database/migrations/001_initial_schema.py +243 -0
- sylvan-1.0.0/src/sylvan/database/migrations/__init__.py +27 -0
- sylvan-1.0.0/src/sylvan/database/migrations/runner.py +234 -0
- sylvan-1.0.0/src/sylvan/database/orm/__init__.py +94 -0
- sylvan-1.0.0/src/sylvan/database/orm/exceptions.py +17 -0
- sylvan-1.0.0/src/sylvan/database/orm/mixins/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/database/orm/mixins/hooks.py +5 -0
- sylvan-1.0.0/src/sylvan/database/orm/mixins/soft_deletes.py +6 -0
- sylvan-1.0.0/src/sylvan/database/orm/model/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/database/orm/model/base.py +346 -0
- sylvan-1.0.0/src/sylvan/database/orm/model/bulk.py +305 -0
- sylvan-1.0.0/src/sylvan/database/orm/model/finders.py +184 -0
- sylvan-1.0.0/src/sylvan/database/orm/model/metaclass.py +89 -0
- sylvan-1.0.0/src/sylvan/database/orm/model/persistence.py +153 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/__init__.py +34 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/blob.py +61 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/coding_session.py +58 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/file_import.py +36 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/file_record.py +63 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/instance.py +73 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/quality.py +35 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/reference.py +29 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/repo.py +66 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/section.py +153 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/symbol.py +224 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/usage_stats.py +46 -0
- sylvan-1.0.0/src/sylvan/database/orm/models/workspace.py +35 -0
- sylvan-1.0.0/src/sylvan/database/orm/primitives/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/database/orm/primitives/fields.py +157 -0
- sylvan-1.0.0/src/sylvan/database/orm/primitives/relations.py +257 -0
- sylvan-1.0.0/src/sylvan/database/orm/primitives/scopes.py +86 -0
- sylvan-1.0.0/src/sylvan/database/orm/query/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/database/orm/query/builder.py +360 -0
- sylvan-1.0.0/src/sylvan/database/orm/query/eager_loading.py +174 -0
- sylvan-1.0.0/src/sylvan/database/orm/query/execution.py +450 -0
- sylvan-1.0.0/src/sylvan/database/orm/query/relations.py +150 -0
- sylvan-1.0.0/src/sylvan/database/orm/query/search.py +158 -0
- sylvan-1.0.0/src/sylvan/database/orm/query/sql.py +193 -0
- sylvan-1.0.0/src/sylvan/database/orm/query/where.py +329 -0
- sylvan-1.0.0/src/sylvan/database/orm/runtime/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/database/orm/runtime/connection_manager.py +28 -0
- sylvan-1.0.0/src/sylvan/database/orm/runtime/identity_map.py +65 -0
- sylvan-1.0.0/src/sylvan/database/orm/runtime/model_registry.py +30 -0
- sylvan-1.0.0/src/sylvan/database/orm/runtime/query_cache.py +119 -0
- sylvan-1.0.0/src/sylvan/database/orm/runtime/search_helpers.py +105 -0
- sylvan-1.0.0/src/sylvan/database/validation.py +178 -0
- sylvan-1.0.0/src/sylvan/database/workspace.py +149 -0
- sylvan-1.0.0/src/sylvan/error_codes.py +375 -0
- sylvan-1.0.0/src/sylvan/git/__init__.py +40 -0
- sylvan-1.0.0/src/sylvan/git/blame.py +122 -0
- sylvan-1.0.0/src/sylvan/git/dependency_files.py +180 -0
- sylvan-1.0.0/src/sylvan/git/diff.py +92 -0
- sylvan-1.0.0/src/sylvan/hooks.py +109 -0
- sylvan-1.0.0/src/sylvan/indexing/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/indexing/discovery/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/indexing/discovery/file_discovery.py +286 -0
- sylvan-1.0.0/src/sylvan/indexing/discovery/incremental.py +54 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/formats/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/formats/asciidoc.py +98 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/formats/html.py +200 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/formats/json_parser.py +186 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/formats/markdown.py +152 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/formats/notebook.py +160 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/formats/openapi.py +226 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/formats/rst.py +156 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/formats/text.py +83 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/formats/xml_parser.py +137 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/formats/yaml_parser.py +37 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/parser.py +40 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/registry.py +101 -0
- sylvan-1.0.0/src/sylvan/indexing/documents/section_builder.py +178 -0
- sylvan-1.0.0/src/sylvan/indexing/pipeline/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/indexing/pipeline/file_processor.py +236 -0
- sylvan-1.0.0/src/sylvan/indexing/pipeline/import_resolver.py +558 -0
- sylvan-1.0.0/src/sylvan/indexing/pipeline/orchestrator.py +286 -0
- sylvan-1.0.0/src/sylvan/indexing/post_processing/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/indexing/post_processing/background_tasks.py +92 -0
- sylvan-1.0.0/src/sylvan/indexing/post_processing/file_watcher.py +114 -0
- sylvan-1.0.0/src/sylvan/indexing/post_processing/summarizer.py +301 -0
- sylvan-1.0.0/src/sylvan/indexing/source_code/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/indexing/source_code/extractor.py +308 -0
- sylvan-1.0.0/src/sylvan/indexing/source_code/import_extraction.py +299 -0
- sylvan-1.0.0/src/sylvan/indexing/source_code/language_registry.py +63 -0
- sylvan-1.0.0/src/sylvan/indexing/source_code/language_specs.py +744 -0
- sylvan-1.0.0/src/sylvan/indexing/source_code/parse_orchestration.py +56 -0
- sylvan-1.0.0/src/sylvan/indexing/source_code/symbol_details.py +244 -0
- sylvan-1.0.0/src/sylvan/indexing/source_code/symbol_enrichment.py +267 -0
- sylvan-1.0.0/src/sylvan/libraries/__init__.py +5 -0
- sylvan-1.0.0/src/sylvan/libraries/manager.py +373 -0
- sylvan-1.0.0/src/sylvan/libraries/resolution/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/libraries/resolution/package_registry.py +185 -0
- sylvan-1.0.0/src/sylvan/libraries/resolution/package_resolvers.py +284 -0
- sylvan-1.0.0/src/sylvan/libraries/resolution/url_overrides.py +57 -0
- sylvan-1.0.0/src/sylvan/libraries/source_fetcher.py +293 -0
- sylvan-1.0.0/src/sylvan/logging.py +179 -0
- sylvan-1.0.0/src/sylvan/providers/__init__.py +53 -0
- sylvan-1.0.0/src/sylvan/providers/base.py +422 -0
- sylvan-1.0.0/src/sylvan/providers/builtin/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/providers/builtin/heuristic.py +98 -0
- sylvan-1.0.0/src/sylvan/providers/builtin/sentence_transformers.py +85 -0
- sylvan-1.0.0/src/sylvan/providers/ecosystem_context/__init__.py +2 -0
- sylvan-1.0.0/src/sylvan/providers/ecosystem_context/base.py +227 -0
- sylvan-1.0.0/src/sylvan/providers/ecosystem_context/dbt.py +161 -0
- sylvan-1.0.0/src/sylvan/providers/external/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/providers/external/claude_code.py +87 -0
- sylvan-1.0.0/src/sylvan/providers/external/codex.py +48 -0
- sylvan-1.0.0/src/sylvan/providers/external/ollama/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/providers/external/ollama/provider.py +125 -0
- sylvan-1.0.0/src/sylvan/providers/external/ollama/setup.py +122 -0
- sylvan-1.0.0/src/sylvan/providers/registry.py +74 -0
- sylvan-1.0.0/src/sylvan/py.typed +0 -0
- sylvan-1.0.0/src/sylvan/scaffold/__init__.py +6 -0
- sylvan-1.0.0/src/sylvan/scaffold/agent_config.py +230 -0
- sylvan-1.0.0/src/sylvan/scaffold/auto_docs.py +179 -0
- sylvan-1.0.0/src/sylvan/scaffold/auto_reports.py +214 -0
- sylvan-1.0.0/src/sylvan/scaffold/directory_structure.py +95 -0
- sylvan-1.0.0/src/sylvan/scaffold/generator.py +203 -0
- sylvan-1.0.0/src/sylvan/search/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/search/embeddings.py +209 -0
- sylvan-1.0.0/src/sylvan/security/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/security/filters.py +125 -0
- sylvan-1.0.0/src/sylvan/security/patterns.py +136 -0
- sylvan-1.0.0/src/sylvan/server/__init__.py +713 -0
- sylvan-1.0.0/src/sylvan/server/startup.py +167 -0
- sylvan-1.0.0/src/sylvan/server/transports.py +121 -0
- sylvan-1.0.0/src/sylvan/session/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/session/tracker.py +286 -0
- sylvan-1.0.0/src/sylvan/session/usage_stats.py +451 -0
- sylvan-1.0.0/src/sylvan/tools/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/find_importers.py +145 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/get_blast_radius.py +67 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/get_class_hierarchy.py +25 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/get_dependency_graph.py +185 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/get_git_context.py +102 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/get_quality.py +57 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/get_quality_report.py +202 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/get_recent_changes.py +100 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/get_references.py +30 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/get_related.py +123 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/get_symbol_diff.py +238 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/rename_symbol.py +129 -0
- sylvan-1.0.0/src/sylvan/tools/analysis/search_columns.py +156 -0
- sylvan-1.0.0/src/sylvan/tools/browsing/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/tools/browsing/get_context_bundle.py +114 -0
- sylvan-1.0.0/src/sylvan/tools/browsing/get_file_outline.py +176 -0
- sylvan-1.0.0/src/sylvan/tools/browsing/get_file_tree.py +131 -0
- sylvan-1.0.0/src/sylvan/tools/browsing/get_repo_outline.py +73 -0
- sylvan-1.0.0/src/sylvan/tools/browsing/get_section.py +108 -0
- sylvan-1.0.0/src/sylvan/tools/browsing/get_symbol.py +125 -0
- sylvan-1.0.0/src/sylvan/tools/browsing/get_toc.py +100 -0
- sylvan-1.0.0/src/sylvan/tools/definitions/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/tools/definitions/analysis.py +358 -0
- sylvan-1.0.0/src/sylvan/tools/definitions/core.py +308 -0
- sylvan-1.0.0/src/sylvan/tools/definitions/support.py +384 -0
- sylvan-1.0.0/src/sylvan/tools/indexing/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/tools/indexing/index_file.py +299 -0
- sylvan-1.0.0/src/sylvan/tools/indexing/index_folder.py +35 -0
- sylvan-1.0.0/src/sylvan/tools/library/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/tools/library/add.py +30 -0
- sylvan-1.0.0/src/sylvan/tools/library/check.py +102 -0
- sylvan-1.0.0/src/sylvan/tools/library/compare.py +124 -0
- sylvan-1.0.0/src/sylvan/tools/library/list.py +18 -0
- sylvan-1.0.0/src/sylvan/tools/library/remove.py +21 -0
- sylvan-1.0.0/src/sylvan/tools/meta/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/tools/meta/get_logs.py +64 -0
- sylvan-1.0.0/src/sylvan/tools/meta/get_server_config.py +57 -0
- sylvan-1.0.0/src/sylvan/tools/meta/get_workflow_guide.py +296 -0
- sylvan-1.0.0/src/sylvan/tools/meta/list_repos.py +36 -0
- sylvan-1.0.0/src/sylvan/tools/meta/remove_repo.py +71 -0
- sylvan-1.0.0/src/sylvan/tools/meta/scaffold.py +41 -0
- sylvan-1.0.0/src/sylvan/tools/meta/suggest_queries.py +190 -0
- sylvan-1.0.0/src/sylvan/tools/search/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/tools/search/search_sections.py +78 -0
- sylvan-1.0.0/src/sylvan/tools/search/search_similar.py +96 -0
- sylvan-1.0.0/src/sylvan/tools/search/search_symbols.py +260 -0
- sylvan-1.0.0/src/sylvan/tools/search/search_text.py +127 -0
- sylvan-1.0.0/src/sylvan/tools/support/__init__.py +0 -0
- sylvan-1.0.0/src/sylvan/tools/support/response.py +380 -0
- sylvan-1.0.0/src/sylvan/tools/support/token_counting.py +73 -0
- sylvan-1.0.0/src/sylvan/tools/workspace/__init__.py +194 -0
- sylvan-1.0.0/src/sylvan/tools/workspace/pin_library.py +56 -0
sylvan-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sylvan
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Unified code + documentation retrieval MCP server
|
|
5
|
+
Author: darki73
|
|
6
|
+
Author-email: darki73 <apple.zhivolupov@gmail.com>
|
|
7
|
+
Requires-Dist: mcp>=1.10.0,<2.0.0
|
|
8
|
+
Requires-Dist: tree-sitter-language-pack>=0.7.0,<1.0.0
|
|
9
|
+
Requires-Dist: sqlite-vec>=0.1.0,<1.0.0
|
|
10
|
+
Requires-Dist: pathspec>=0.12.0,<1.0.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0,<7.0
|
|
12
|
+
Requires-Dist: tiktoken>=0.12.0,<1.0.0
|
|
13
|
+
Requires-Dist: fastembed>=0.4.0,<1.0.0
|
|
14
|
+
Requires-Dist: ollama>=0.6.1,<1.0.0
|
|
15
|
+
Requires-Dist: structlog>=25.5.0,<27.0.0
|
|
16
|
+
Requires-Dist: claude-agent-sdk==0.1.48
|
|
17
|
+
Requires-Dist: aiosqlite>=0.21.0,<1.0.0
|
|
18
|
+
Requires-Dist: jinja2>=3.1,<4.0
|
|
19
|
+
Requires-Dist: uvicorn>=0.30.0,<1.0.0
|
|
20
|
+
Requires-Dist: starlette>=0.38.0,<1.0.0
|
|
21
|
+
Requires-Dist: watchfiles>=1.0.0 ; extra == 'all'
|
|
22
|
+
Requires-Dist: asyncpg>=0.30.0 ; extra == 'all'
|
|
23
|
+
Requires-Dist: asyncpg>=0.30.0 ; extra == 'postgres'
|
|
24
|
+
Requires-Dist: watchfiles>=1.0.0 ; extra == 'watch'
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Provides-Extra: all
|
|
27
|
+
Provides-Extra: postgres
|
|
28
|
+
Provides-Extra: watch
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Sylvan
|
|
32
|
+
|
|
33
|
+
Code intelligence platform for AI agents. Search, analyze, and navigate codebases through MCP tools — returning exactly the code your agent needs at a fraction of the token cost.
|
|
34
|
+
|
|
35
|
+
## Why
|
|
36
|
+
|
|
37
|
+
AI agents burn tokens reading entire files when they need one function. They grep across directories to trace a dependency. They piece together call chains one file at a time. Every wasted read costs money and context window space.
|
|
38
|
+
|
|
39
|
+
Sylvan indexes your codebase into a structured database of symbols, sections, and import relationships, then exposes it through 52 MCP tools. Your agent asks for what it needs and gets exactly that — function signatures, blast radius, dependency graphs, semantic search results. Typical token savings exceed 80%.
|
|
40
|
+
|
|
41
|
+
## Dashboard
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
<details>
|
|
46
|
+
<summary>More screenshots</summary>
|
|
47
|
+
|
|
48
|
+
**Search** — find code by name, signature, or keywords with syntax-highlighted source
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
**Session** — live token efficiency tracking per session and all-time
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
**Quality Report** — code smells, security findings, test/doc coverage
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
**Blast Radius** — visualize impact before changing a symbol
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
**Libraries** — indexed third-party packages with symbol counts
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
</details>
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
- 52 MCP tools for search, browsing, analysis, and refactoring
|
|
68
|
+
- 34 programming languages via tree-sitter
|
|
69
|
+
- Hybrid search — full-text (FTS5) + vector similarity with ranked fusion
|
|
70
|
+
- Blast radius analysis before any refactor
|
|
71
|
+
- Dependency graphs, call chains, class hierarchies
|
|
72
|
+
- Third-party library indexing (pip, npm, cargo, go)
|
|
73
|
+
- Multi-repo workspaces with cross-repo analysis
|
|
74
|
+
- Code quality reports — smells, security, duplication, dead code
|
|
75
|
+
- Web dashboard with live token efficiency tracking
|
|
76
|
+
- Multi-instance cluster support
|
|
77
|
+
|
|
78
|
+
## Quick start
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install sylvan
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Add to your MCP client config:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"sylvan": {
|
|
90
|
+
"command": "uv",
|
|
91
|
+
"args": ["run", "--directory", "/path/to/sylvan", "sylvan", "serve"]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Your agent handles the rest — index a project, search for code, navigate with precision.
|
|
98
|
+
|
|
99
|
+
## Documentation
|
|
100
|
+
|
|
101
|
+
Full docs at [darki73.github.io/sylvan](https://darki73.github.io/sylvan/)
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
Non-commercial open source. Free to use, modify, and distribute with attribution. See [LICENSE](LICENSE) for details.
|
sylvan-1.0.0/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Sylvan
|
|
2
|
+
|
|
3
|
+
Code intelligence platform for AI agents. Search, analyze, and navigate codebases through MCP tools — returning exactly the code your agent needs at a fraction of the token cost.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
AI agents burn tokens reading entire files when they need one function. They grep across directories to trace a dependency. They piece together call chains one file at a time. Every wasted read costs money and context window space.
|
|
8
|
+
|
|
9
|
+
Sylvan indexes your codebase into a structured database of symbols, sections, and import relationships, then exposes it through 52 MCP tools. Your agent asks for what it needs and gets exactly that — function signatures, blast radius, dependency graphs, semantic search results. Typical token savings exceed 80%.
|
|
10
|
+
|
|
11
|
+
## Dashboard
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary>More screenshots</summary>
|
|
17
|
+
|
|
18
|
+
**Search** — find code by name, signature, or keywords with syntax-highlighted source
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
**Session** — live token efficiency tracking per session and all-time
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
**Quality Report** — code smells, security findings, test/doc coverage
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
**Blast Radius** — visualize impact before changing a symbol
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
**Libraries** — indexed third-party packages with symbol counts
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
</details>
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- 52 MCP tools for search, browsing, analysis, and refactoring
|
|
38
|
+
- 34 programming languages via tree-sitter
|
|
39
|
+
- Hybrid search — full-text (FTS5) + vector similarity with ranked fusion
|
|
40
|
+
- Blast radius analysis before any refactor
|
|
41
|
+
- Dependency graphs, call chains, class hierarchies
|
|
42
|
+
- Third-party library indexing (pip, npm, cargo, go)
|
|
43
|
+
- Multi-repo workspaces with cross-repo analysis
|
|
44
|
+
- Code quality reports — smells, security, duplication, dead code
|
|
45
|
+
- Web dashboard with live token efficiency tracking
|
|
46
|
+
- Multi-instance cluster support
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install sylvan
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Add to your MCP client config:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"sylvan": {
|
|
60
|
+
"command": "uv",
|
|
61
|
+
"args": ["run", "--directory", "/path/to/sylvan", "sylvan", "serve"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Your agent handles the rest — index a project, search for code, navigate with precision.
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
Full docs at [darki73.github.io/sylvan](https://darki73.github.io/sylvan/)
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
Non-commercial open source. Free to use, modify, and distribute with attribution. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sylvan"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Unified code + documentation retrieval MCP server"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "darki73", email = "apple.zhivolupov@gmail.com" },
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"mcp>=1.10.0,<2.0.0",
|
|
12
|
+
"tree-sitter-language-pack>=0.7.0,<1.0.0",
|
|
13
|
+
"sqlite-vec>=0.1.0,<1.0.0",
|
|
14
|
+
"pathspec>=0.12.0,<1.0.0",
|
|
15
|
+
"pyyaml>=6.0,<7.0",
|
|
16
|
+
"tiktoken>=0.12.0,<1.0.0",
|
|
17
|
+
"fastembed>=0.4.0,<1.0.0",
|
|
18
|
+
"ollama>=0.6.1,<1.0.0",
|
|
19
|
+
"structlog>=25.5.0,<27.0.0",
|
|
20
|
+
"claude-agent-sdk==0.1.48",
|
|
21
|
+
"aiosqlite>=0.21.0,<1.0.0",
|
|
22
|
+
"jinja2>=3.1,<4.0",
|
|
23
|
+
"uvicorn>=0.30.0,<1.0.0",
|
|
24
|
+
"starlette>=0.38.0,<1.0.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
watch = ["watchfiles>=1.0.0"]
|
|
29
|
+
postgres = ["asyncpg>=0.30.0"]
|
|
30
|
+
all = ["watchfiles>=1.0.0", "asyncpg>=0.30.0"]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
sylvan = "sylvan.cli:main"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["uv_build>=0.10.6,<0.11.0"]
|
|
37
|
+
build-backend = "uv_build"
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
python_files = "test_*.py"
|
|
42
|
+
python_classes = "Test*"
|
|
43
|
+
python_functions = "test_*"
|
|
44
|
+
asyncio_mode = "auto"
|
|
45
|
+
filterwarnings = [
|
|
46
|
+
"ignore::pytest.PytestUnraisableExceptionWarning",
|
|
47
|
+
"ignore::pytest.PytestUnhandledThreadExceptionWarning",
|
|
48
|
+
"ignore:coroutine 'watch_folder' was never awaited:RuntimeWarning",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
python_version = "3.12"
|
|
53
|
+
warn_return_any = true
|
|
54
|
+
warn_unused_configs = true
|
|
55
|
+
disallow_untyped_defs = false
|
|
56
|
+
check_untyped_defs = true
|
|
57
|
+
ignore_missing_imports = true
|
|
58
|
+
|
|
59
|
+
[dependency-groups]
|
|
60
|
+
dev = [
|
|
61
|
+
"pytest>=9.0",
|
|
62
|
+
"pytest-asyncio>=1.3",
|
|
63
|
+
"pytest-cov>=7.0",
|
|
64
|
+
"ruff>=0.9.0",
|
|
65
|
+
"mypy>=1.14",
|
|
66
|
+
"pre-commit>=4.0",
|
|
67
|
+
"mkdocs>=1.6",
|
|
68
|
+
"mkdocs-material>=9.5",
|
|
69
|
+
"mkdocstrings[python]>=0.27",
|
|
70
|
+
]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Blast radius analysis -- estimate impact of changing a symbol."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections import deque
|
|
5
|
+
|
|
6
|
+
from sylvan.database.orm import Symbol
|
|
7
|
+
from sylvan.database.orm.models.blob import Blob
|
|
8
|
+
from sylvan.database.orm.runtime.connection_manager import get_backend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def get_blast_radius(
|
|
12
|
+
symbol_id: str,
|
|
13
|
+
max_depth: int = 3,
|
|
14
|
+
) -> dict:
|
|
15
|
+
"""Estimate the blast radius of changing a symbol.
|
|
16
|
+
|
|
17
|
+
Uses BFS through the reference graph + text confirmation.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
symbol_id: Unique identifier of the target symbol.
|
|
21
|
+
max_depth: Maximum BFS traversal depth.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Dictionary with "symbol", "confirmed", "potential",
|
|
25
|
+
"depth_reached", and "total_affected" keys. Returns an error
|
|
26
|
+
dict if the symbol is not found.
|
|
27
|
+
"""
|
|
28
|
+
backend = get_backend()
|
|
29
|
+
|
|
30
|
+
target = await (
|
|
31
|
+
Symbol.query()
|
|
32
|
+
.select("symbols.*", "f.path as file_path", "f.content_hash", "f.repo_id")
|
|
33
|
+
.join("files f", "f.id = symbols.file_id")
|
|
34
|
+
.where("symbols.symbol_id", symbol_id)
|
|
35
|
+
.first()
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if target is None:
|
|
39
|
+
return {"error": "symbol_not_found", "symbol_id": symbol_id}
|
|
40
|
+
|
|
41
|
+
target_name = target.name
|
|
42
|
+
target_file_id = target.file_id
|
|
43
|
+
target_file_path = getattr(target, "file_path", "")
|
|
44
|
+
repo_id = getattr(target, "repo_id", None)
|
|
45
|
+
|
|
46
|
+
visited_files: set[int] = {target_file_id}
|
|
47
|
+
queue: deque[tuple[int, int]] = deque() # (file_id, depth)
|
|
48
|
+
|
|
49
|
+
importers = await backend.fetch_all(
|
|
50
|
+
"""SELECT DISTINCT fi.file_id
|
|
51
|
+
FROM file_imports fi
|
|
52
|
+
JOIN files f ON f.id = fi.file_id
|
|
53
|
+
WHERE f.repo_id = ?
|
|
54
|
+
AND (fi.resolved_file_id = ?
|
|
55
|
+
OR fi.specifier LIKE ?)""",
|
|
56
|
+
[repo_id, target_file_id, f"%{target_file_path.rsplit('/', 1)[-1].rsplit('.', 1)[0]}%"],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
for imp in importers:
|
|
60
|
+
fid = imp["file_id"]
|
|
61
|
+
if fid not in visited_files:
|
|
62
|
+
queue.append((fid, 1))
|
|
63
|
+
visited_files.add(fid)
|
|
64
|
+
|
|
65
|
+
confirmed = []
|
|
66
|
+
potential = []
|
|
67
|
+
depth_reached = 0
|
|
68
|
+
|
|
69
|
+
while queue:
|
|
70
|
+
file_id, depth = queue.popleft()
|
|
71
|
+
depth_reached = max(depth_reached, depth)
|
|
72
|
+
|
|
73
|
+
if depth > max_depth:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
file_row = await backend.fetch_one(
|
|
77
|
+
"SELECT path, content_hash FROM files WHERE id = ?", [file_id]
|
|
78
|
+
)
|
|
79
|
+
if file_row is None:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
content = await Blob.get(file_row["content_hash"])
|
|
83
|
+
if content is None:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
text = content.decode("utf-8", errors="replace")
|
|
87
|
+
occurrences = len(re.findall(r'\b' + re.escape(target_name) + r'\b', text))
|
|
88
|
+
|
|
89
|
+
entry = {
|
|
90
|
+
"file": file_row["path"],
|
|
91
|
+
"depth": depth,
|
|
92
|
+
"occurrences": occurrences,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
file_symbols = await (
|
|
96
|
+
Symbol.where(file_id=file_id)
|
|
97
|
+
.select("symbol_id", "name", "kind", "line_start")
|
|
98
|
+
.limit(10)
|
|
99
|
+
.get()
|
|
100
|
+
)
|
|
101
|
+
entry["symbols"] = [
|
|
102
|
+
{"symbol_id": s.symbol_id, "name": s.name, "kind": s.kind, "line_start": s.line_start}
|
|
103
|
+
for s in file_symbols
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
if occurrences > 0:
|
|
107
|
+
confirmed.append(entry)
|
|
108
|
+
else:
|
|
109
|
+
potential.append(entry)
|
|
110
|
+
|
|
111
|
+
if depth < max_depth:
|
|
112
|
+
next_importers = await backend.fetch_all(
|
|
113
|
+
"""SELECT DISTINCT fi.file_id FROM file_imports fi
|
|
114
|
+
JOIN files f ON f.id = fi.file_id
|
|
115
|
+
WHERE f.repo_id = ? AND fi.resolved_file_id = ?""",
|
|
116
|
+
[repo_id, file_id],
|
|
117
|
+
)
|
|
118
|
+
for ni in next_importers:
|
|
119
|
+
nfid = ni["file_id"]
|
|
120
|
+
if nfid not in visited_files:
|
|
121
|
+
queue.append((nfid, depth + 1))
|
|
122
|
+
visited_files.add(nfid)
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
"symbol": {
|
|
126
|
+
"symbol_id": symbol_id,
|
|
127
|
+
"name": target_name,
|
|
128
|
+
"kind": target.kind,
|
|
129
|
+
"file": target_file_path,
|
|
130
|
+
},
|
|
131
|
+
"confirmed": confirmed,
|
|
132
|
+
"potential": potential,
|
|
133
|
+
"depth_reached": depth_reached,
|
|
134
|
+
"total_affected": len(confirmed) + len(potential),
|
|
135
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Cross-repo analysis -- resolve imports and blast radius across repo boundaries."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections import deque
|
|
5
|
+
|
|
6
|
+
from sylvan.database.orm.models.blob import Blob
|
|
7
|
+
from sylvan.database.orm.runtime.connection_manager import get_backend
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def resolve_cross_repo_imports(
|
|
11
|
+
repo_ids: list[int],
|
|
12
|
+
) -> int:
|
|
13
|
+
"""Resolve file imports that cross repo boundaries.
|
|
14
|
+
|
|
15
|
+
For each unresolved import in any of the given repos, try to match the
|
|
16
|
+
specifier against files in the other repos. Updates resolved_file_id
|
|
17
|
+
in the file_imports table.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
repo_ids: List of repository database IDs to scan.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Number of cross-repo imports resolved.
|
|
24
|
+
"""
|
|
25
|
+
backend = get_backend()
|
|
26
|
+
|
|
27
|
+
file_lookup: dict[str, int] = {}
|
|
28
|
+
for row in await backend.fetch_all(
|
|
29
|
+
f"""SELECT id, path, repo_id FROM files
|
|
30
|
+
WHERE repo_id IN ({",".join("?" * len(repo_ids))})""",
|
|
31
|
+
repo_ids,
|
|
32
|
+
):
|
|
33
|
+
fid = row["id"]
|
|
34
|
+
path = row["path"]
|
|
35
|
+
file_lookup[path] = fid
|
|
36
|
+
filename = path.rsplit("/", 1)[-1]
|
|
37
|
+
file_lookup.setdefault(filename, fid)
|
|
38
|
+
stem = filename.rsplit(".", 1)[0]
|
|
39
|
+
file_lookup.setdefault(stem, fid)
|
|
40
|
+
dotpath = path.replace("/", ".").rsplit(".", 1)[0]
|
|
41
|
+
file_lookup.setdefault(dotpath, fid)
|
|
42
|
+
|
|
43
|
+
unresolved = await backend.fetch_all(
|
|
44
|
+
f"""SELECT fi.id, fi.specifier, fi.file_id, f.repo_id
|
|
45
|
+
FROM file_imports fi
|
|
46
|
+
JOIN files f ON f.id = fi.file_id
|
|
47
|
+
WHERE f.repo_id IN ({",".join("?" * len(repo_ids))})
|
|
48
|
+
AND fi.resolved_file_id IS NULL""",
|
|
49
|
+
repo_ids,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
resolved_count = 0
|
|
53
|
+
for imp in unresolved:
|
|
54
|
+
spec = imp["specifier"]
|
|
55
|
+
source_repo_id = imp["repo_id"]
|
|
56
|
+
|
|
57
|
+
candidates = []
|
|
58
|
+
|
|
59
|
+
if spec in file_lookup:
|
|
60
|
+
target_fid = file_lookup[spec]
|
|
61
|
+
target_repo = await backend.fetch_one(
|
|
62
|
+
"SELECT repo_id FROM files WHERE id = ?", [target_fid]
|
|
63
|
+
)
|
|
64
|
+
if target_repo and target_repo["repo_id"] != source_repo_id:
|
|
65
|
+
candidates.append(target_fid)
|
|
66
|
+
|
|
67
|
+
stem = spec.rsplit("/", 1)[-1].rsplit(".", 1)[0]
|
|
68
|
+
if stem in file_lookup and not candidates:
|
|
69
|
+
target_fid = file_lookup[stem]
|
|
70
|
+
target_repo = await backend.fetch_one(
|
|
71
|
+
"SELECT repo_id FROM files WHERE id = ?", [target_fid]
|
|
72
|
+
)
|
|
73
|
+
if target_repo and target_repo["repo_id"] != source_repo_id:
|
|
74
|
+
candidates.append(target_fid)
|
|
75
|
+
|
|
76
|
+
dotpath = spec.replace(".", "/")
|
|
77
|
+
for suffix in ("", ".py", ".ts", ".js", ".go"):
|
|
78
|
+
key = dotpath + suffix
|
|
79
|
+
if key in file_lookup and not candidates:
|
|
80
|
+
target_fid = file_lookup[key]
|
|
81
|
+
target_repo = await backend.fetch_one(
|
|
82
|
+
"SELECT repo_id FROM files WHERE id = ?", [target_fid]
|
|
83
|
+
)
|
|
84
|
+
if target_repo and target_repo["repo_id"] != source_repo_id:
|
|
85
|
+
candidates.append(target_fid)
|
|
86
|
+
|
|
87
|
+
if candidates:
|
|
88
|
+
await backend.execute(
|
|
89
|
+
"UPDATE file_imports SET resolved_file_id = ? WHERE id = ?",
|
|
90
|
+
[candidates[0], imp["id"]],
|
|
91
|
+
)
|
|
92
|
+
resolved_count += 1
|
|
93
|
+
|
|
94
|
+
if resolved_count:
|
|
95
|
+
await backend.commit()
|
|
96
|
+
return resolved_count
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def cross_repo_blast_radius(
|
|
100
|
+
symbol_id: str,
|
|
101
|
+
repo_ids: list[int],
|
|
102
|
+
max_depth: int = 3,
|
|
103
|
+
) -> dict:
|
|
104
|
+
"""Blast radius analysis that crosses repo boundaries.
|
|
105
|
+
|
|
106
|
+
Like get_blast_radius but follows imports across all repos in the workspace.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
symbol_id: Unique identifier of the target symbol.
|
|
110
|
+
repo_ids: List of repository database IDs to scan.
|
|
111
|
+
max_depth: Maximum BFS traversal depth.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Dictionary with "symbol", "confirmed", "potential",
|
|
115
|
+
"depth_reached", "total_affected", "cross_repo_affected",
|
|
116
|
+
and "repos_scanned" keys. Returns an error dict if the
|
|
117
|
+
symbol is not found.
|
|
118
|
+
"""
|
|
119
|
+
backend = get_backend()
|
|
120
|
+
|
|
121
|
+
target = await backend.fetch_one(
|
|
122
|
+
"""SELECT s.*, f.path as file_path, f.repo_id, f.content_hash,
|
|
123
|
+
r.name as repo_name
|
|
124
|
+
FROM symbols s
|
|
125
|
+
JOIN files f ON f.id = s.file_id
|
|
126
|
+
JOIN repos r ON r.id = f.repo_id
|
|
127
|
+
WHERE s.symbol_id = ?""",
|
|
128
|
+
[symbol_id],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if target is None:
|
|
132
|
+
return {"error": "symbol_not_found", "symbol_id": symbol_id}
|
|
133
|
+
|
|
134
|
+
target_dict = dict(target)
|
|
135
|
+
target_name = target_dict["name"]
|
|
136
|
+
target_file_id = target_dict["file_id"]
|
|
137
|
+
|
|
138
|
+
visited_files: set[int] = {target_file_id}
|
|
139
|
+
queue: deque[tuple[int, int]] = deque()
|
|
140
|
+
|
|
141
|
+
repo_filter = ",".join("?" * len(repo_ids))
|
|
142
|
+
importers = await backend.fetch_all(
|
|
143
|
+
f"""SELECT DISTINCT fi.file_id
|
|
144
|
+
FROM file_imports fi
|
|
145
|
+
JOIN files f ON f.id = fi.file_id
|
|
146
|
+
WHERE f.repo_id IN ({repo_filter})
|
|
147
|
+
AND (fi.resolved_file_id = ? OR fi.specifier LIKE ?)""",
|
|
148
|
+
[*repo_ids, target_file_id, f"%{target_dict['file_path'].rsplit('/', 1)[-1].rsplit('.', 1)[0]}%"],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
for imp in importers:
|
|
152
|
+
fid = imp["file_id"]
|
|
153
|
+
if fid not in visited_files:
|
|
154
|
+
queue.append((fid, 1))
|
|
155
|
+
visited_files.add(fid)
|
|
156
|
+
|
|
157
|
+
confirmed = []
|
|
158
|
+
potential = []
|
|
159
|
+
depth_reached = 0
|
|
160
|
+
|
|
161
|
+
while queue:
|
|
162
|
+
file_id, depth = queue.popleft()
|
|
163
|
+
depth_reached = max(depth_reached, depth)
|
|
164
|
+
if depth > max_depth:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
file_row = await backend.fetch_one(
|
|
168
|
+
"""SELECT f.path, f.content_hash, r.name as repo_name
|
|
169
|
+
FROM files f JOIN repos r ON r.id = f.repo_id
|
|
170
|
+
WHERE f.id = ?""",
|
|
171
|
+
[file_id],
|
|
172
|
+
)
|
|
173
|
+
if file_row is None:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
content = await Blob.get(file_row["content_hash"])
|
|
177
|
+
if content is None:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
text = content.decode("utf-8", errors="replace")
|
|
181
|
+
occurrences = len(re.findall(r"\b" + re.escape(target_name) + r"\b", text))
|
|
182
|
+
|
|
183
|
+
entry = {
|
|
184
|
+
"file": file_row["path"],
|
|
185
|
+
"repo": file_row["repo_name"],
|
|
186
|
+
"depth": depth,
|
|
187
|
+
"occurrences": occurrences,
|
|
188
|
+
"cross_repo": file_row["repo_name"] != target_dict["repo_name"],
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if occurrences > 0:
|
|
192
|
+
confirmed.append(entry)
|
|
193
|
+
else:
|
|
194
|
+
potential.append(entry)
|
|
195
|
+
|
|
196
|
+
if depth < max_depth:
|
|
197
|
+
next_importers = await backend.fetch_all(
|
|
198
|
+
f"""SELECT DISTINCT fi2.file_id FROM file_imports fi2
|
|
199
|
+
JOIN files f2 ON f2.id = fi2.file_id
|
|
200
|
+
WHERE f2.repo_id IN ({repo_filter})
|
|
201
|
+
AND fi2.resolved_file_id = ?""",
|
|
202
|
+
[*repo_ids, file_id],
|
|
203
|
+
)
|
|
204
|
+
for ni in next_importers:
|
|
205
|
+
if ni["file_id"] not in visited_files:
|
|
206
|
+
queue.append((ni["file_id"], depth + 1))
|
|
207
|
+
visited_files.add(ni["file_id"])
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"symbol": {
|
|
211
|
+
"symbol_id": target_dict["symbol_id"],
|
|
212
|
+
"name": target_name,
|
|
213
|
+
"kind": target_dict["kind"],
|
|
214
|
+
"file": target_dict["file_path"],
|
|
215
|
+
"repo": target_dict["repo_name"],
|
|
216
|
+
},
|
|
217
|
+
"confirmed": confirmed,
|
|
218
|
+
"potential": potential,
|
|
219
|
+
"depth_reached": depth_reached,
|
|
220
|
+
"total_affected": len(confirmed) + len(potential),
|
|
221
|
+
"cross_repo_affected": sum(1 for c in confirmed + potential if c.get("cross_repo")),
|
|
222
|
+
"repos_scanned": len(repo_ids),
|
|
223
|
+
}
|
|
File without changes
|