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,361 @@
|
|
|
1
|
+
"""Query constraints parser and processor.
|
|
2
|
+
|
|
3
|
+
Supports fff-style query constraints:
|
|
4
|
+
- git:modified, git:staged, git:deleted, git:renamed, git:untracked, git:ignored
|
|
5
|
+
- !pattern (negation)
|
|
6
|
+
- test/ (path filter)
|
|
7
|
+
- ./{glob} (glob patterns)
|
|
8
|
+
- Extension filters: *.py, *.{rs,lua}
|
|
9
|
+
|
|
10
|
+
Based on fff.nvim's constraints.rs.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Constraint types
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GitStatus(Enum):
|
|
26
|
+
"""Git status filter types."""
|
|
27
|
+
MODIFIED = "modified"
|
|
28
|
+
STAGED = "staged"
|
|
29
|
+
DELETED = "deleted"
|
|
30
|
+
RENAMED = "renamed"
|
|
31
|
+
UNTRACKED = "untracked"
|
|
32
|
+
IGNORED = "ignored"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Constraint:
|
|
37
|
+
"""A parsed constraint."""
|
|
38
|
+
type: str # "git", "path", "negation", "glob", "extension"
|
|
39
|
+
value: str
|
|
40
|
+
negated: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ParsedQuery:
|
|
45
|
+
"""A query with extracted constraints."""
|
|
46
|
+
query: str # The remaining search pattern after constraints
|
|
47
|
+
constraints: list[Constraint]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Constraint parsing
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_query_constraints(query: str) -> ParsedQuery:
|
|
56
|
+
"""Parse a query with fff-style constraints.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
query: The query string with possible constraints.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
ParsedQuery with the main query and list of constraints.
|
|
63
|
+
"""
|
|
64
|
+
constraints: list[Constraint] = []
|
|
65
|
+
remaining_parts: list[str] = []
|
|
66
|
+
|
|
67
|
+
parts = query.split()
|
|
68
|
+
|
|
69
|
+
for part in parts:
|
|
70
|
+
if not part:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Check for git: constraint
|
|
74
|
+
if part.startswith("git:"):
|
|
75
|
+
status_str = part[4:] # Remove "git:"
|
|
76
|
+
if status_str.startswith("!"):
|
|
77
|
+
negated = True
|
|
78
|
+
status_str = status_str[1:]
|
|
79
|
+
else:
|
|
80
|
+
negated = False
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
status = GitStatus(status_str)
|
|
84
|
+
constraints.append(Constraint(
|
|
85
|
+
type="git",
|
|
86
|
+
value=status.value,
|
|
87
|
+
negated=negated,
|
|
88
|
+
))
|
|
89
|
+
except ValueError:
|
|
90
|
+
# Not a valid git status, treat as regular query
|
|
91
|
+
remaining_parts.append(part)
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Check for negation: !something
|
|
95
|
+
if part.startswith("!"):
|
|
96
|
+
negated = True
|
|
97
|
+
part = part[1:]
|
|
98
|
+
if part:
|
|
99
|
+
constraints.append(Constraint(
|
|
100
|
+
type="negation",
|
|
101
|
+
value=part.lower(),
|
|
102
|
+
negated=negated,
|
|
103
|
+
))
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
# Check for path filter: test/
|
|
107
|
+
if "/" in part and not part.startswith("./"):
|
|
108
|
+
# This looks like a path filter
|
|
109
|
+
constraints.append(Constraint(
|
|
110
|
+
type="path",
|
|
111
|
+
value=part.rstrip("/"),
|
|
112
|
+
negated=False,
|
|
113
|
+
))
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
# Check for glob pattern: ./**/*.py
|
|
117
|
+
if part.startswith("./") or "*" in part or "{" in part:
|
|
118
|
+
constraints.append(Constraint(
|
|
119
|
+
type="glob",
|
|
120
|
+
value=part,
|
|
121
|
+
negated=False,
|
|
122
|
+
))
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# Check for extension filter: *.py or *.{rs,lua}
|
|
126
|
+
if part.startswith("*."):
|
|
127
|
+
constraints.append(Constraint(
|
|
128
|
+
type="extension",
|
|
129
|
+
value=part[1:], # Remove leading *
|
|
130
|
+
negated=False,
|
|
131
|
+
))
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# Regular query part
|
|
135
|
+
remaining_parts.append(part)
|
|
136
|
+
|
|
137
|
+
return ParsedQuery(
|
|
138
|
+
query=" ".join(remaining_parts),
|
|
139
|
+
constraints=constraints,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Constraint matching
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def matches_constraints(
|
|
149
|
+
file_path: str,
|
|
150
|
+
constraints: list[Constraint],
|
|
151
|
+
git_status: GitStatus | None = None,
|
|
152
|
+
) -> bool:
|
|
153
|
+
"""Check if a file matches the given constraints.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
file_path: Path to check.
|
|
157
|
+
constraints: List of constraints to match.
|
|
158
|
+
git_status: Current git status of the file (if known).
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
True if all constraints pass, False otherwise.
|
|
162
|
+
"""
|
|
163
|
+
path_obj = Path(file_path)
|
|
164
|
+
|
|
165
|
+
for constraint in constraints:
|
|
166
|
+
if constraint.type == "git":
|
|
167
|
+
if constraint.negated:
|
|
168
|
+
if git_status is not None and git_status.value == constraint.value:
|
|
169
|
+
return False
|
|
170
|
+
else:
|
|
171
|
+
if git_status is None or git_status.value != constraint.value:
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
elif constraint.type == "negation":
|
|
175
|
+
# Check if file path contains the negated pattern
|
|
176
|
+
if constraint.value in file_path.lower():
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
elif constraint.type == "path":
|
|
180
|
+
path_filter = constraint.value
|
|
181
|
+
if constraint.negated:
|
|
182
|
+
# Negated path filter - file should NOT be in this path
|
|
183
|
+
if path_filter in file_path:
|
|
184
|
+
return False
|
|
185
|
+
else:
|
|
186
|
+
# Positive path filter - file MUST be in this path
|
|
187
|
+
if path_filter not in file_path:
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
elif constraint.type == "glob":
|
|
191
|
+
# Simple glob matching
|
|
192
|
+
if not _matches_glob(file_path, constraint.value):
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
elif constraint.type == "extension":
|
|
196
|
+
ext = constraint.value
|
|
197
|
+
if not ext.startswith("."):
|
|
198
|
+
ext = "." + ext
|
|
199
|
+
if path_obj.suffix != ext:
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _matches_glob(path: str, pattern: str) -> bool:
|
|
206
|
+
"""Simple glob matching for path patterns.
|
|
207
|
+
|
|
208
|
+
Supports:
|
|
209
|
+
- **/*.py (recursive)
|
|
210
|
+
- *.py (single segment)
|
|
211
|
+
- *.{rs,lua} (brace expansion)
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
path: Path to check.
|
|
215
|
+
pattern: Glob pattern.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
True if path matches pattern.
|
|
219
|
+
"""
|
|
220
|
+
import fnmatch
|
|
221
|
+
|
|
222
|
+
# Convert fff-style glob to fnmatch pattern
|
|
223
|
+
# ./**/*.py -> **/*.py -> match recursively
|
|
224
|
+
# *.py -> *.py -> match in single directory
|
|
225
|
+
|
|
226
|
+
if pattern.startswith("./"):
|
|
227
|
+
pattern = pattern[2:]
|
|
228
|
+
|
|
229
|
+
# Handle brace expansion *.{rs,lua}
|
|
230
|
+
if "{" in pattern and "}" in pattern:
|
|
231
|
+
# Try each alternative
|
|
232
|
+
brace_match = re.match(r'^(.*)\{([^}]+)\}(.*)$', pattern)
|
|
233
|
+
if brace_match:
|
|
234
|
+
prefix, alternatives, suffix = brace_match.groups()
|
|
235
|
+
for alt in alternatives.split(","):
|
|
236
|
+
alt_pattern = f"{prefix}{alt}{suffix}"
|
|
237
|
+
if _matches_glob(path, alt_pattern):
|
|
238
|
+
return True
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
# Handle ** for recursive matching
|
|
242
|
+
if "**" in pattern:
|
|
243
|
+
# Convert **/* to recursive fnmatch
|
|
244
|
+
pattern = pattern.replace("**", "*")
|
|
245
|
+
# But ** matches any path, so we need to check if path ends with the pattern
|
|
246
|
+
pattern = pattern.lstrip("*/")
|
|
247
|
+
|
|
248
|
+
if pattern.startswith("*"):
|
|
249
|
+
# Ends with something - check if path contains it
|
|
250
|
+
pattern = pattern.lstrip("*")
|
|
251
|
+
return pattern in path
|
|
252
|
+
|
|
253
|
+
return fnmatch.fnmatch(path, f"*/{pattern}") or fnmatch.fnmatch(path, pattern)
|
|
254
|
+
|
|
255
|
+
return fnmatch.fnmatch(Path(path).name, pattern)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# Constraint application to search results
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def filter_files_by_constraints(
|
|
264
|
+
files: list[str],
|
|
265
|
+
constraints: list[Constraint],
|
|
266
|
+
git_statuses: dict[str, GitStatus] | None = None,
|
|
267
|
+
) -> list[str]:
|
|
268
|
+
"""Filter a list of files by constraints.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
files: List of file paths to filter.
|
|
272
|
+
constraints: Constraints to apply.
|
|
273
|
+
git_statuses: Dict mapping file path to git status.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Filtered list of files.
|
|
277
|
+
"""
|
|
278
|
+
if not constraints:
|
|
279
|
+
return files
|
|
280
|
+
|
|
281
|
+
git_statuses = git_statuses or {}
|
|
282
|
+
filtered: list[str] = []
|
|
283
|
+
|
|
284
|
+
for file_path in files:
|
|
285
|
+
git_status = git_statuses.get(file_path)
|
|
286
|
+
if matches_constraints(file_path, constraints, git_status):
|
|
287
|
+
filtered.append(file_path)
|
|
288
|
+
|
|
289
|
+
return filtered
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
# High-level API
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class QueryConstraintProcessor:
|
|
298
|
+
"""Process queries with constraints."""
|
|
299
|
+
|
|
300
|
+
def __init__(self, project_dir: str) -> None:
|
|
301
|
+
self.project_dir = project_dir
|
|
302
|
+
|
|
303
|
+
def parse(self, query: str) -> ParsedQuery:
|
|
304
|
+
"""Parse a query with constraints."""
|
|
305
|
+
return parse_query_constraints(query)
|
|
306
|
+
|
|
307
|
+
def filter_files(
|
|
308
|
+
self,
|
|
309
|
+
files: list[str],
|
|
310
|
+
constraints: list[Constraint],
|
|
311
|
+
git_statuses: dict[str, GitStatus] | None = None,
|
|
312
|
+
) -> list[str]:
|
|
313
|
+
"""Filter files by constraints."""
|
|
314
|
+
return filter_files_by_constraints(files, constraints, git_statuses)
|
|
315
|
+
|
|
316
|
+
def get_git_status(self, file_path: str) -> GitStatus | None:
|
|
317
|
+
"""Get the git status for a file.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
file_path: Path to check.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
GitStatus if file is tracked and status can be determined, None otherwise.
|
|
324
|
+
"""
|
|
325
|
+
import subprocess
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
result = subprocess.run(
|
|
329
|
+
["git", "status", "--porcelain", "--", file_path],
|
|
330
|
+
cwd=self.project_dir,
|
|
331
|
+
capture_output=True,
|
|
332
|
+
text=True,
|
|
333
|
+
timeout=10,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if not result.stdout.strip():
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
status_char = result.stdout[0] if result.stdout else ""
|
|
340
|
+
|
|
341
|
+
# Status codes:
|
|
342
|
+
# M = modified
|
|
343
|
+
# A = staged (added)
|
|
344
|
+
# D = deleted
|
|
345
|
+
# R = renamed
|
|
346
|
+
# ?? = untracked
|
|
347
|
+
# !! = ignored
|
|
348
|
+
|
|
349
|
+
status_map = {
|
|
350
|
+
"M": GitStatus.MODIFIED,
|
|
351
|
+
"A": GitStatus.STAGED,
|
|
352
|
+
"D": GitStatus.DELETED,
|
|
353
|
+
"R": GitStatus.RENAMED,
|
|
354
|
+
"?": GitStatus.UNTRACKED,
|
|
355
|
+
"!": GitStatus.IGNORED,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return status_map.get(status_char)
|
|
359
|
+
|
|
360
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
361
|
+
return None
|