attocode-code-intel 0.1.0__py3-none-any.whl
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.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Fuzzy string matching using Smith-Waterman algorithm.
|
|
2
|
+
|
|
3
|
+
Provides typo-resistant search that can find "mtxlk" matching "mutex_lock".
|
|
4
|
+
Uses the Smith-Waterman algorithm for local sequence alignment with
|
|
5
|
+
affine gaps for optimal matching.
|
|
6
|
+
|
|
7
|
+
Based on fff.nvim's fuzzy grep implementation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Sequence
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Scoring constants
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
_MATCH_SCORE: int = 2
|
|
23
|
+
_MISMATCH_SCORE: int = -1
|
|
24
|
+
_GAP_OPEN: int = -3
|
|
25
|
+
_GAP_EXTEND: int = -1
|
|
26
|
+
|
|
27
|
+
# Minimum score threshold (normalized to 0-100)
|
|
28
|
+
_MIN_SCORE_THRESHOLD: float = 30.0
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Data structures
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class FuzzyMatch:
|
|
37
|
+
"""A fuzzy match result."""
|
|
38
|
+
text: str
|
|
39
|
+
score: float # 0-100 normalized score
|
|
40
|
+
matched_indices: list[int] # Positions in text that matched the pattern
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class FuzzyResult:
|
|
45
|
+
"""Result of a fuzzy search."""
|
|
46
|
+
query: str
|
|
47
|
+
matches: list[FuzzyMatch]
|
|
48
|
+
mode: str = "fuzzy"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Smith-Waterman implementation
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _smith_waterman(
|
|
57
|
+
pattern: str,
|
|
58
|
+
text: str,
|
|
59
|
+
*,
|
|
60
|
+
case_sensitive: bool = True,
|
|
61
|
+
) -> tuple[float, list[int]]:
|
|
62
|
+
"""Perform Smith-Waterman local alignment.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
pattern: The search pattern (query).
|
|
66
|
+
text: The text to search in.
|
|
67
|
+
case_sensitive: Whether to match case-sensitively.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Tuple of (normalized_score 0-100, list of matched character indices).
|
|
71
|
+
"""
|
|
72
|
+
if not pattern or not text:
|
|
73
|
+
return 0.0, []
|
|
74
|
+
|
|
75
|
+
p_len = len(pattern)
|
|
76
|
+
t_len = len(text)
|
|
77
|
+
|
|
78
|
+
# Convert to uppercase for case-insensitive matching
|
|
79
|
+
if not case_sensitive:
|
|
80
|
+
p_upper = pattern.upper()
|
|
81
|
+
t_upper = text.upper()
|
|
82
|
+
else:
|
|
83
|
+
p_upper = pattern
|
|
84
|
+
t_upper = text
|
|
85
|
+
|
|
86
|
+
# Initialize matrices
|
|
87
|
+
# We only need two rows for memory efficiency
|
|
88
|
+
prev_row = [0] * (t_len + 1)
|
|
89
|
+
curr_row = [0] * (t_len + 1)
|
|
90
|
+
|
|
91
|
+
# Traceback matrix (only for matched indices)
|
|
92
|
+
# Store: 0=end, 1=diag, 2=up, 3=left
|
|
93
|
+
trace: list[list[int]] = [[0] * (t_len + 1) for _ in range(p_len + 1)]
|
|
94
|
+
|
|
95
|
+
max_score = 0
|
|
96
|
+
max_pos = (0, 0)
|
|
97
|
+
|
|
98
|
+
# Fill matrices
|
|
99
|
+
for i in range(1, p_len + 1):
|
|
100
|
+
for j in range(1, t_len + 1):
|
|
101
|
+
p_char = p_upper[i - 1]
|
|
102
|
+
t_char = t_upper[j - 1]
|
|
103
|
+
|
|
104
|
+
# Match/mismatch score
|
|
105
|
+
if p_char == t_char:
|
|
106
|
+
diag_score = prev_row[j - 1] + _MATCH_SCORE
|
|
107
|
+
else:
|
|
108
|
+
diag_score = prev_row[j - 1] + _MISMATCH_SCORE
|
|
109
|
+
|
|
110
|
+
# Gap open (from top)
|
|
111
|
+
up_score = prev_row[j] + _GAP_OPEN if prev_row[j] < 0 else prev_row[j] + _GAP_EXTEND
|
|
112
|
+
|
|
113
|
+
# Gap extend (from left)
|
|
114
|
+
left_score = curr_row[j - 1] + _GAP_OPEN if curr_row[j - 1] < 0 else curr_row[j - 1] + _GAP_EXTEND
|
|
115
|
+
|
|
116
|
+
# Take maximum (local alignment allows 0)
|
|
117
|
+
scores = [0, diag_score, up_score, left_score]
|
|
118
|
+
curr_row[j] = max(scores)
|
|
119
|
+
trace[i][j] = scores.index(curr_row[j])
|
|
120
|
+
|
|
121
|
+
if curr_row[j] > max_score:
|
|
122
|
+
max_score = curr_row[j]
|
|
123
|
+
max_pos = (i, j)
|
|
124
|
+
|
|
125
|
+
# Swap rows
|
|
126
|
+
prev_row, curr_row = curr_row, prev_row
|
|
127
|
+
|
|
128
|
+
if max_score <= 0:
|
|
129
|
+
return 0.0, []
|
|
130
|
+
|
|
131
|
+
# Traceback to find matched indices
|
|
132
|
+
matched_indices: list[int] = []
|
|
133
|
+
i, j = max_pos
|
|
134
|
+
|
|
135
|
+
while trace[i][j] != 0 and i > 0 and j > 0:
|
|
136
|
+
if trace[i][j] == 1: # Diagonal (match/mismatch)
|
|
137
|
+
matched_indices.append(j - 1)
|
|
138
|
+
i -= 1
|
|
139
|
+
j -= 1
|
|
140
|
+
elif trace[i][j] == 2: # Up (gap in text)
|
|
141
|
+
i -= 1
|
|
142
|
+
else: # Left (gap in pattern)
|
|
143
|
+
j -= 1
|
|
144
|
+
|
|
145
|
+
matched_indices.reverse()
|
|
146
|
+
|
|
147
|
+
# Normalize score to 0-100 range
|
|
148
|
+
# Maximum possible score is len(pattern) * MATCH_SCORE
|
|
149
|
+
max_possible = p_len * _MATCH_SCORE
|
|
150
|
+
normalized = (max_score / max_possible) * 100.0 if max_possible > 0 else 0.0
|
|
151
|
+
|
|
152
|
+
return normalized, matched_indices
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def fuzzy_match(
|
|
156
|
+
pattern: str,
|
|
157
|
+
text: str,
|
|
158
|
+
*,
|
|
159
|
+
case_sensitive: bool = True,
|
|
160
|
+
min_score: float = _MIN_SCORE_THRESHOLD,
|
|
161
|
+
) -> FuzzyMatch | None:
|
|
162
|
+
"""Check if pattern fuzzy-matches text.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
pattern: The pattern to search for.
|
|
166
|
+
text: The text to search in.
|
|
167
|
+
case_sensitive: Whether matching is case-sensitive.
|
|
168
|
+
min_score: Minimum score threshold (0-100).
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
FuzzyMatch if score >= min_score, None otherwise.
|
|
172
|
+
"""
|
|
173
|
+
score, indices = _smith_waterman(pattern, text, case_sensitive=case_sensitive)
|
|
174
|
+
|
|
175
|
+
if score >= min_score:
|
|
176
|
+
return FuzzyMatch(text=text, score=score, matched_indices=indices)
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def fuzzy_match_in_lines(
|
|
181
|
+
pattern: str,
|
|
182
|
+
lines: Sequence[str],
|
|
183
|
+
*,
|
|
184
|
+
case_sensitive: bool = True,
|
|
185
|
+
min_score: float = _MIN_SCORE_THRESHOLD,
|
|
186
|
+
max_results: int = 100,
|
|
187
|
+
) -> list[FuzzyMatch]:
|
|
188
|
+
"""Find fuzzy matches of pattern in lines.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
pattern: The pattern to search for.
|
|
192
|
+
lines: Lines to search in.
|
|
193
|
+
case_sensitive: Whether matching is case-sensitive.
|
|
194
|
+
min_score: Minimum score threshold (0-100).
|
|
195
|
+
max_results: Maximum number of results to return.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
List of FuzzyMatch objects for matching lines.
|
|
199
|
+
"""
|
|
200
|
+
results: list[FuzzyMatch] = []
|
|
201
|
+
|
|
202
|
+
for line in lines:
|
|
203
|
+
match = fuzzy_match(pattern, line, case_sensitive=case_sensitive, min_score=min_score)
|
|
204
|
+
if match is not None:
|
|
205
|
+
results.append(match)
|
|
206
|
+
if len(results) >= max_results:
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
# Sort by score descending
|
|
210
|
+
results.sort(key=lambda m: m.score, reverse=True)
|
|
211
|
+
return results
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def fuzzy_search(
|
|
215
|
+
pattern: str,
|
|
216
|
+
text: str,
|
|
217
|
+
*,
|
|
218
|
+
case_sensitive: bool = False,
|
|
219
|
+
min_score: float = _MIN_SCORE_THRESHOLD,
|
|
220
|
+
) -> FuzzyResult:
|
|
221
|
+
"""Perform fuzzy search on text.
|
|
222
|
+
|
|
223
|
+
This splits text into lines and finds fuzzy matches.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
pattern: The pattern to search for.
|
|
227
|
+
text: The text to search in.
|
|
228
|
+
case_sensitive: Whether matching is case-sensitive.
|
|
229
|
+
min_score: Minimum score threshold (0-100).
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
FuzzyResult with all matching lines.
|
|
233
|
+
"""
|
|
234
|
+
lines = text.splitlines()
|
|
235
|
+
matches = fuzzy_match_in_lines(
|
|
236
|
+
pattern,
|
|
237
|
+
lines,
|
|
238
|
+
case_sensitive=case_sensitive,
|
|
239
|
+
min_score=min_score,
|
|
240
|
+
)
|
|
241
|
+
return FuzzyResult(query=pattern, matches=matches)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Quick pattern matching for file names
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def fuzzy_match_filename(pattern: str, filename: str) -> float:
|
|
250
|
+
"""Check how well a pattern matches a filename.
|
|
251
|
+
|
|
252
|
+
Uses a simpler scoring for filename matching.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
pattern: The search pattern.
|
|
256
|
+
filename: The filename to match against.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Score 0-100 indicating match quality.
|
|
260
|
+
"""
|
|
261
|
+
pattern_lower = pattern.lower()
|
|
262
|
+
filename_lower = filename.lower()
|
|
263
|
+
|
|
264
|
+
# Exact substring match gets high score
|
|
265
|
+
if pattern_lower in filename_lower:
|
|
266
|
+
# Longer patterns that match are better
|
|
267
|
+
return 50.0 + (len(pattern) / len(filename)) * 50.0
|
|
268
|
+
|
|
269
|
+
# Fall back to Smith-Waterman for fuzzy matching
|
|
270
|
+
score, _ = _smith_waterman(pattern, filename, case_sensitive=False)
|
|
271
|
+
return score
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
# High-level API for search integration
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class FuzzyMatcher:
|
|
280
|
+
"""Fuzzy matcher for text search."""
|
|
281
|
+
|
|
282
|
+
def __init__(
|
|
283
|
+
self,
|
|
284
|
+
pattern: str,
|
|
285
|
+
*,
|
|
286
|
+
case_sensitive: bool = False,
|
|
287
|
+
min_score: float = _MIN_SCORE_THRESHOLD,
|
|
288
|
+
) -> None:
|
|
289
|
+
self.pattern = pattern
|
|
290
|
+
self.case_sensitive = case_sensitive
|
|
291
|
+
self.min_score = min_score
|
|
292
|
+
|
|
293
|
+
def matches(self, text: str) -> bool:
|
|
294
|
+
"""Check if text matches the pattern."""
|
|
295
|
+
match = fuzzy_match(
|
|
296
|
+
self.pattern,
|
|
297
|
+
text,
|
|
298
|
+
case_sensitive=self.case_sensitive,
|
|
299
|
+
min_score=self.min_score,
|
|
300
|
+
)
|
|
301
|
+
return match is not None
|
|
302
|
+
|
|
303
|
+
def get_score(self, text: str) -> float:
|
|
304
|
+
"""Get the match score for text."""
|
|
305
|
+
match = fuzzy_match(
|
|
306
|
+
self.pattern,
|
|
307
|
+
text,
|
|
308
|
+
case_sensitive=self.case_sensitive,
|
|
309
|
+
min_score=0.0, # Get score even if below threshold
|
|
310
|
+
)
|
|
311
|
+
return match.score if match else 0.0
|
|
312
|
+
|
|
313
|
+
def search(self, lines: Sequence[str]) -> list[FuzzyMatch]:
|
|
314
|
+
"""Find all matching lines."""
|
|
315
|
+
return fuzzy_match_in_lines(
|
|
316
|
+
self.pattern,
|
|
317
|
+
lines,
|
|
318
|
+
case_sensitive=self.case_sensitive,
|
|
319
|
+
min_score=self.min_score,
|
|
320
|
+
)
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Hierarchical BFS codebase explorer.
|
|
2
|
+
|
|
3
|
+
Provides drill-down navigation through the repository, showing one
|
|
4
|
+
directory level at a time with statistics and importance scoring.
|
|
5
|
+
This avoids overwhelming the LLM context with a flat repo map on
|
|
6
|
+
large codebases (1000+ files).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from collections import OrderedDict
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class DirectoryNode:
|
|
19
|
+
"""A directory in the explorer view."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
path: str
|
|
23
|
+
file_count: int = 0
|
|
24
|
+
total_lines: int = 0
|
|
25
|
+
languages: dict[str, int] = field(default_factory=dict)
|
|
26
|
+
subdirs: int = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class FileNode:
|
|
31
|
+
"""A file in the explorer view."""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
path: str
|
|
35
|
+
relative_path: str
|
|
36
|
+
importance: float = 0.0
|
|
37
|
+
line_count: int = 0
|
|
38
|
+
language: str = ""
|
|
39
|
+
top_symbols: list[str] = field(default_factory=list)
|
|
40
|
+
tags: list[str] = field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(slots=True)
|
|
44
|
+
class ExplorerResult:
|
|
45
|
+
"""Result of exploring a directory."""
|
|
46
|
+
|
|
47
|
+
path: str
|
|
48
|
+
breadcrumbs: list[str]
|
|
49
|
+
total_files: int
|
|
50
|
+
total_lines: int
|
|
51
|
+
directories: list[DirectoryNode]
|
|
52
|
+
files: list[FileNode]
|
|
53
|
+
omitted_files: int = 0
|
|
54
|
+
omitted_dirs: int = 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class HierarchicalExplorer:
|
|
58
|
+
"""Interactive BFS codebase explorer with drill-down navigation.
|
|
59
|
+
|
|
60
|
+
Exactly ONE directory level deep per call, with predictable
|
|
61
|
+
token cost (~500-2000 tokens per result).
|
|
62
|
+
|
|
63
|
+
Usage::
|
|
64
|
+
|
|
65
|
+
explorer = HierarchicalExplorer(codebase_context)
|
|
66
|
+
|
|
67
|
+
# Top-level overview
|
|
68
|
+
result = explorer.explore()
|
|
69
|
+
|
|
70
|
+
# Drill into a directory
|
|
71
|
+
result = explorer.explore("src/attocode/integrations")
|
|
72
|
+
|
|
73
|
+
# With filters
|
|
74
|
+
result = explorer.explore("src", max_items=50, importance_threshold=0.3)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, context_manager: Any, ast_service: Any = None) -> None:
|
|
78
|
+
self._ctx = context_manager
|
|
79
|
+
self._ast = ast_service
|
|
80
|
+
self._cache: OrderedDict[str, ExplorerResult] = OrderedDict()
|
|
81
|
+
self._max_cache = 100
|
|
82
|
+
|
|
83
|
+
def explore(
|
|
84
|
+
self,
|
|
85
|
+
path: str = "",
|
|
86
|
+
*,
|
|
87
|
+
max_items: int = 30,
|
|
88
|
+
importance_threshold: float = 0.3,
|
|
89
|
+
task_scores: dict[str, float] | None = None,
|
|
90
|
+
) -> ExplorerResult:
|
|
91
|
+
"""Explore one level of a directory.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
path: Relative path to explore ("" for root).
|
|
95
|
+
max_items: Maximum items (dirs + files) to return.
|
|
96
|
+
importance_threshold: Minimum importance for files to be shown.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
ExplorerResult with dirs, files, and breadcrumbs.
|
|
100
|
+
"""
|
|
101
|
+
cache_key = f"{path}|{max_items}|{importance_threshold}" if not task_scores else None
|
|
102
|
+
if cache_key is not None and cache_key in self._cache:
|
|
103
|
+
self._cache.move_to_end(cache_key)
|
|
104
|
+
return self._cache[cache_key]
|
|
105
|
+
|
|
106
|
+
self._ctx._ensure_fresh()
|
|
107
|
+
files = self._ctx._files
|
|
108
|
+
|
|
109
|
+
# Normalize path
|
|
110
|
+
path = path.strip("/").rstrip("/") if path else ""
|
|
111
|
+
|
|
112
|
+
# Filter files to those in this directory (direct children or descendants)
|
|
113
|
+
if path:
|
|
114
|
+
prefix = path + "/"
|
|
115
|
+
relevant = [f for f in files if f.relative_path.startswith(prefix)]
|
|
116
|
+
else:
|
|
117
|
+
relevant = list(files)
|
|
118
|
+
|
|
119
|
+
if not relevant and path:
|
|
120
|
+
# Try exact match (file, not directory)
|
|
121
|
+
exact = [f for f in files if f.relative_path == path]
|
|
122
|
+
if exact:
|
|
123
|
+
return self._file_detail(exact[0], path)
|
|
124
|
+
|
|
125
|
+
# Group into direct children (dirs and files at this level)
|
|
126
|
+
dir_stats: dict[str, _DirAccum] = {}
|
|
127
|
+
direct_files: list[Any] = []
|
|
128
|
+
_depth = path.count("/") + 1 if path else 0
|
|
129
|
+
|
|
130
|
+
for f in relevant:
|
|
131
|
+
rel = f.relative_path
|
|
132
|
+
child = rel[len(path) + 1:] if path else rel
|
|
133
|
+
|
|
134
|
+
parts = child.split("/")
|
|
135
|
+
if len(parts) == 1:
|
|
136
|
+
# Direct file child
|
|
137
|
+
direct_files.append(f)
|
|
138
|
+
else:
|
|
139
|
+
# In a subdirectory
|
|
140
|
+
subdir_name = parts[0]
|
|
141
|
+
subdir_path = f"{path}/{subdir_name}" if path else subdir_name
|
|
142
|
+
if subdir_path not in dir_stats:
|
|
143
|
+
dir_stats[subdir_path] = _DirAccum(name=subdir_name, path=subdir_path)
|
|
144
|
+
acc = dir_stats[subdir_path]
|
|
145
|
+
acc.file_count += 1
|
|
146
|
+
acc.total_lines += f.line_count
|
|
147
|
+
if f.language:
|
|
148
|
+
acc.languages[f.language] = acc.languages.get(f.language, 0) + 1
|
|
149
|
+
# Count unique immediate subdirectories within this subdir
|
|
150
|
+
if len(parts) > 2:
|
|
151
|
+
acc.seen_subdirs.add(parts[1])
|
|
152
|
+
|
|
153
|
+
# Build directory nodes
|
|
154
|
+
directories: list[DirectoryNode] = []
|
|
155
|
+
for _sp, acc in sorted(dir_stats.items()):
|
|
156
|
+
directories.append(DirectoryNode(
|
|
157
|
+
name=acc.name,
|
|
158
|
+
path=acc.path,
|
|
159
|
+
file_count=acc.file_count,
|
|
160
|
+
total_lines=acc.total_lines,
|
|
161
|
+
languages=dict(acc.languages),
|
|
162
|
+
subdirs=len(acc.seen_subdirs),
|
|
163
|
+
))
|
|
164
|
+
if task_scores:
|
|
165
|
+
directories.sort(key=lambda node: (
|
|
166
|
+
-max((score for file_path, score in task_scores.items()
|
|
167
|
+
if file_path.startswith(node.path + "/")), default=0.0),
|
|
168
|
+
node.path,
|
|
169
|
+
))
|
|
170
|
+
|
|
171
|
+
# Build file nodes (filtered by importance threshold)
|
|
172
|
+
file_nodes: list[FileNode] = []
|
|
173
|
+
for f in sorted(direct_files, key=lambda x: (
|
|
174
|
+
-(task_scores or {}).get(x.relative_path, 0.0), -x.importance, x.relative_path,
|
|
175
|
+
)):
|
|
176
|
+
if (f.importance < importance_threshold and len(file_nodes) >= 5
|
|
177
|
+
and not (task_scores or {}).get(f.relative_path)):
|
|
178
|
+
break
|
|
179
|
+
tags: list[str] = []
|
|
180
|
+
if f.is_config:
|
|
181
|
+
tags.append("config")
|
|
182
|
+
if f.is_test:
|
|
183
|
+
tags.append("test")
|
|
184
|
+
name = os.path.basename(f.relative_path).lower()
|
|
185
|
+
if name in ("main.py", "main.ts", "app.py", "cli.py", "__init__.py",
|
|
186
|
+
"index.ts", "index.js"):
|
|
187
|
+
tags.append("entry")
|
|
188
|
+
|
|
189
|
+
# Get top symbols if AST service is available
|
|
190
|
+
top_syms: list[str] = []
|
|
191
|
+
if self._ast and self._ast.initialized:
|
|
192
|
+
cached = self._ast._ast_cache.get(f.relative_path)
|
|
193
|
+
if cached:
|
|
194
|
+
top_syms = cached.get_symbols()[:5]
|
|
195
|
+
|
|
196
|
+
file_nodes.append(FileNode(
|
|
197
|
+
name=os.path.basename(f.relative_path),
|
|
198
|
+
path=f.path,
|
|
199
|
+
relative_path=f.relative_path,
|
|
200
|
+
importance=round(f.importance, 2),
|
|
201
|
+
line_count=f.line_count,
|
|
202
|
+
language=f.language,
|
|
203
|
+
top_symbols=top_syms,
|
|
204
|
+
tags=tags,
|
|
205
|
+
))
|
|
206
|
+
|
|
207
|
+
# Enforce max_items limit
|
|
208
|
+
total_items = len(directories) + len(file_nodes)
|
|
209
|
+
omitted_dir_count = 0
|
|
210
|
+
if total_items > max_items:
|
|
211
|
+
# Prioritize directories, then high-importance files
|
|
212
|
+
dir_budget = min(len(directories), max_items // 2)
|
|
213
|
+
file_budget = max_items - dir_budget
|
|
214
|
+
omitted_dir_count = max(0, len(directories) - dir_budget)
|
|
215
|
+
directories = directories[:dir_budget]
|
|
216
|
+
file_nodes = file_nodes[:file_budget]
|
|
217
|
+
|
|
218
|
+
omitted = len(direct_files) - len(file_nodes)
|
|
219
|
+
|
|
220
|
+
# Build breadcrumbs
|
|
221
|
+
breadcrumbs = ["(root)"]
|
|
222
|
+
if path:
|
|
223
|
+
parts = path.split("/")
|
|
224
|
+
for i in range(len(parts)):
|
|
225
|
+
breadcrumbs.append("/".join(parts[:i + 1]))
|
|
226
|
+
|
|
227
|
+
result = ExplorerResult(
|
|
228
|
+
path=path or "(root)",
|
|
229
|
+
breadcrumbs=breadcrumbs,
|
|
230
|
+
total_files=len(relevant),
|
|
231
|
+
total_lines=sum(f.line_count for f in relevant),
|
|
232
|
+
directories=directories,
|
|
233
|
+
files=file_nodes,
|
|
234
|
+
omitted_files=max(0, omitted),
|
|
235
|
+
omitted_dirs=omitted_dir_count,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Cache with LRU eviction
|
|
239
|
+
if cache_key is not None:
|
|
240
|
+
self._cache[cache_key] = result
|
|
241
|
+
if len(self._cache) > self._max_cache:
|
|
242
|
+
self._cache.popitem(last=False)
|
|
243
|
+
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
def _file_detail(self, file_info: Any, path: str) -> ExplorerResult:
|
|
247
|
+
"""Return a single file as an explorer result."""
|
|
248
|
+
tags: list[str] = []
|
|
249
|
+
if file_info.is_config:
|
|
250
|
+
tags.append("config")
|
|
251
|
+
if file_info.is_test:
|
|
252
|
+
tags.append("test")
|
|
253
|
+
fn = FileNode(
|
|
254
|
+
name=os.path.basename(file_info.relative_path),
|
|
255
|
+
path=file_info.path,
|
|
256
|
+
relative_path=file_info.relative_path,
|
|
257
|
+
importance=round(file_info.importance, 2),
|
|
258
|
+
line_count=file_info.line_count,
|
|
259
|
+
language=file_info.language,
|
|
260
|
+
tags=tags,
|
|
261
|
+
)
|
|
262
|
+
return ExplorerResult(
|
|
263
|
+
path=path,
|
|
264
|
+
breadcrumbs=["(root)"] + path.split("/"),
|
|
265
|
+
total_files=1,
|
|
266
|
+
total_lines=file_info.line_count,
|
|
267
|
+
directories=[],
|
|
268
|
+
files=[fn],
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def invalidate(self, path: str = "") -> None:
|
|
272
|
+
"""Invalidate cached results for a path or all paths."""
|
|
273
|
+
if not path:
|
|
274
|
+
self._cache.clear()
|
|
275
|
+
return
|
|
276
|
+
keys_to_remove = [k for k in self._cache if k.startswith(path + "|") or k.startswith(path + "/")]
|
|
277
|
+
for k in keys_to_remove:
|
|
278
|
+
del self._cache[k]
|
|
279
|
+
|
|
280
|
+
def format_result(self, result: ExplorerResult) -> str:
|
|
281
|
+
"""Format an ExplorerResult as a human-readable string."""
|
|
282
|
+
lines: list[str] = []
|
|
283
|
+
|
|
284
|
+
# Header
|
|
285
|
+
lines.append(f"{result.path} ({result.total_files} files, {result.total_lines:,} lines)")
|
|
286
|
+
lines.append(f"Breadcrumb: {' > '.join(result.breadcrumbs)}")
|
|
287
|
+
lines.append("")
|
|
288
|
+
|
|
289
|
+
# Directories
|
|
290
|
+
for d in result.directories:
|
|
291
|
+
lang_str = ""
|
|
292
|
+
if d.languages:
|
|
293
|
+
top_langs = sorted(d.languages.items(), key=lambda x: -x[1])[:3]
|
|
294
|
+
lang_str = f" [{', '.join(line for line, _ in top_langs)}]"
|
|
295
|
+
lines.append(
|
|
296
|
+
f" [dir] {d.name}/ "
|
|
297
|
+
f"{d.file_count} files {d.total_lines:,} lines{lang_str}"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Files
|
|
301
|
+
for f in result.files:
|
|
302
|
+
tags_str = f" [{', '.join(f.tags)}]" if f.tags else ""
|
|
303
|
+
syms_str = ""
|
|
304
|
+
if f.top_symbols:
|
|
305
|
+
syms_str = f" [{', '.join(f.top_symbols)}]"
|
|
306
|
+
lines.append(
|
|
307
|
+
f" [file] {f.name} "
|
|
308
|
+
f"{f.line_count}L imp={f.importance}{tags_str}{syms_str}"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if result.omitted_files > 0 or result.omitted_dirs > 0:
|
|
312
|
+
parts = []
|
|
313
|
+
if result.omitted_files > 0:
|
|
314
|
+
parts.append(f"{result.omitted_files} more files")
|
|
315
|
+
if result.omitted_dirs > 0:
|
|
316
|
+
parts.append(f"{result.omitted_dirs} more directories")
|
|
317
|
+
lines.append(f" ... +{', '.join(parts)}")
|
|
318
|
+
|
|
319
|
+
return "\n".join(lines)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@dataclass(slots=True)
|
|
323
|
+
class _DirAccum:
|
|
324
|
+
"""Accumulator for directory statistics."""
|
|
325
|
+
|
|
326
|
+
name: str
|
|
327
|
+
path: str
|
|
328
|
+
file_count: int = 0
|
|
329
|
+
total_lines: int = 0
|
|
330
|
+
languages: dict[str, int] = field(default_factory=dict)
|
|
331
|
+
seen_subdirs: set[str] = field(default_factory=set)
|