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,354 @@
|
|
|
1
|
+
"""Architecture drift detection.
|
|
2
|
+
|
|
3
|
+
Compares actual file dependencies against declared architecture boundaries
|
|
4
|
+
to detect layering violations, circular dependencies, and unauthorized imports.
|
|
5
|
+
|
|
6
|
+
Loads rules from ``.attocode/architecture.yaml`` and checks them against the
|
|
7
|
+
real dependency graph built by the AST service.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Data classes
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True)
|
|
25
|
+
class ArchViolation:
|
|
26
|
+
"""A single architecture boundary violation."""
|
|
27
|
+
|
|
28
|
+
source_file: str # file that has the import
|
|
29
|
+
target_file: str # file being imported
|
|
30
|
+
source_layer: str # layer the source belongs to
|
|
31
|
+
target_layer: str # layer the target belongs to
|
|
32
|
+
rule: str # human-readable rule description
|
|
33
|
+
severity: str = "high" # high for deny violations, medium for unlisted
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(slots=True)
|
|
37
|
+
class ArchReport:
|
|
38
|
+
"""Architecture drift detection report."""
|
|
39
|
+
|
|
40
|
+
violations: list[ArchViolation]
|
|
41
|
+
layers_defined: int
|
|
42
|
+
rules_defined: int
|
|
43
|
+
files_checked: int
|
|
44
|
+
compliant_files: int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ArchLayer:
|
|
49
|
+
"""A named architecture layer with path patterns."""
|
|
50
|
+
|
|
51
|
+
name: str
|
|
52
|
+
paths: list[str]
|
|
53
|
+
|
|
54
|
+
def matches(self, file_path: str) -> bool:
|
|
55
|
+
"""Check if a file belongs to this layer.
|
|
56
|
+
|
|
57
|
+
Uses forward-slash normalised prefix matching so that OS-specific
|
|
58
|
+
separators do not cause false negatives.
|
|
59
|
+
"""
|
|
60
|
+
normalised = file_path.replace(os.sep, "/")
|
|
61
|
+
for path_prefix in self.paths:
|
|
62
|
+
prefix = path_prefix.replace(os.sep, "/")
|
|
63
|
+
if normalised.startswith(prefix):
|
|
64
|
+
return True
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ArchRule:
|
|
70
|
+
"""A dependency rule between layers."""
|
|
71
|
+
|
|
72
|
+
from_layer: str
|
|
73
|
+
allowed: list[str] = field(default_factory=list)
|
|
74
|
+
denied: list[str] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Loading
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_architecture(
|
|
83
|
+
project_dir: str,
|
|
84
|
+
) -> tuple[list[ArchLayer], list[ArchRule], dict[str, list[str]]]:
|
|
85
|
+
"""Load architecture definitions from ``.attocode/architecture.yaml``.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
A 3-tuple of ``(layers, rules, exceptions)`` where *exceptions* maps
|
|
89
|
+
a source file path to a list of explicitly-allowed target paths.
|
|
90
|
+
|
|
91
|
+
If the YAML file does not exist or PyYAML is not installed the function
|
|
92
|
+
returns empty collections without raising.
|
|
93
|
+
"""
|
|
94
|
+
config_path = os.path.join(project_dir, ".attocode", "architecture.yaml")
|
|
95
|
+
if not os.path.isfile(config_path):
|
|
96
|
+
logger.debug("Architecture config not found: %s", config_path)
|
|
97
|
+
return [], [], {}
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
import yaml # type: ignore[import-untyped]
|
|
101
|
+
except ImportError:
|
|
102
|
+
logger.warning(
|
|
103
|
+
"PyYAML is not installed -- cannot load architecture config. "
|
|
104
|
+
"Install it with: pip install pyyaml"
|
|
105
|
+
)
|
|
106
|
+
return [], [], {}
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
with open(config_path, encoding="utf-8") as fh:
|
|
110
|
+
data = yaml.safe_load(fh) or {}
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
logger.warning("Failed to parse architecture config: %s", exc)
|
|
113
|
+
return [], [], {}
|
|
114
|
+
|
|
115
|
+
# --- Layers ---
|
|
116
|
+
layers: list[ArchLayer] = []
|
|
117
|
+
for entry in data.get("layers", []):
|
|
118
|
+
name = entry.get("name", "")
|
|
119
|
+
paths = entry.get("paths", [])
|
|
120
|
+
if name and paths:
|
|
121
|
+
layers.append(ArchLayer(name=name, paths=paths))
|
|
122
|
+
|
|
123
|
+
# --- Rules ---
|
|
124
|
+
rules: list[ArchRule] = []
|
|
125
|
+
for entry in data.get("rules", []):
|
|
126
|
+
from_layer = entry.get("from", "")
|
|
127
|
+
if not from_layer:
|
|
128
|
+
continue
|
|
129
|
+
allowed = entry.get("to", [])
|
|
130
|
+
denied = entry.get("deny", [])
|
|
131
|
+
rules.append(ArchRule(from_layer=from_layer, allowed=allowed, denied=denied))
|
|
132
|
+
|
|
133
|
+
# --- Exceptions ---
|
|
134
|
+
exceptions: dict[str, list[str]] = {}
|
|
135
|
+
for entry in data.get("exceptions", []):
|
|
136
|
+
src = entry.get("file", "")
|
|
137
|
+
allowed_targets = entry.get("allowed", [])
|
|
138
|
+
if src and allowed_targets:
|
|
139
|
+
exceptions[src] = allowed_targets
|
|
140
|
+
|
|
141
|
+
return layers, rules, exceptions
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# Classification
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def classify_file(file_path: str, layers: list[ArchLayer]) -> str:
|
|
150
|
+
"""Determine which layer *file_path* belongs to.
|
|
151
|
+
|
|
152
|
+
Returns the layer name, or ``""`` if the file does not match any layer.
|
|
153
|
+
Matches are evaluated in order; the first matching layer wins.
|
|
154
|
+
"""
|
|
155
|
+
for layer in layers:
|
|
156
|
+
if layer.matches(file_path):
|
|
157
|
+
return layer.name
|
|
158
|
+
return ""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Core check
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _build_deps_from_index(project_dir: str) -> dict[str, set[str]]:
|
|
167
|
+
"""Build a dependency map from the AST service's CrossRefIndex."""
|
|
168
|
+
try:
|
|
169
|
+
from attocode_intel._internal.integrations.context.ast_service import ASTService
|
|
170
|
+
|
|
171
|
+
svc = ASTService.get_instance(project_dir)
|
|
172
|
+
if not svc.initialized:
|
|
173
|
+
svc.initialize_skeleton(indexing_depth="auto")
|
|
174
|
+
return dict(svc.index.file_dependencies)
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
logger.debug("Could not build dependency map from AST index: %s", exc)
|
|
177
|
+
return {}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def check_drift(
|
|
181
|
+
project_dir: str,
|
|
182
|
+
dependencies: dict[str, set[str]] | None = None,
|
|
183
|
+
) -> ArchReport:
|
|
184
|
+
"""Check actual dependencies against architecture rules.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
project_dir: Project root.
|
|
188
|
+
dependencies: Optional pre-computed dependency map
|
|
189
|
+
(file -> set of imported files). If *None*, will build from
|
|
190
|
+
the AST service's ``CrossRefIndex.file_dependencies``.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
An :class:`ArchReport` summarising all violations found.
|
|
194
|
+
"""
|
|
195
|
+
layers, rules, exceptions = load_architecture(project_dir)
|
|
196
|
+
if not layers:
|
|
197
|
+
return ArchReport(
|
|
198
|
+
violations=[],
|
|
199
|
+
layers_defined=0,
|
|
200
|
+
rules_defined=0,
|
|
201
|
+
files_checked=0,
|
|
202
|
+
compliant_files=0,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if dependencies is None:
|
|
206
|
+
dependencies = _build_deps_from_index(project_dir)
|
|
207
|
+
|
|
208
|
+
# Build a quick rule lookup: from_layer -> ArchRule
|
|
209
|
+
rule_map: dict[str, ArchRule] = {}
|
|
210
|
+
for rule in rules:
|
|
211
|
+
rule_map[rule.from_layer] = rule
|
|
212
|
+
|
|
213
|
+
violations: list[ArchViolation] = []
|
|
214
|
+
files_checked = 0
|
|
215
|
+
compliant_files = 0
|
|
216
|
+
|
|
217
|
+
for source_file, targets in dependencies.items():
|
|
218
|
+
source_layer = classify_file(source_file, layers)
|
|
219
|
+
if not source_layer:
|
|
220
|
+
# File is outside any declared layer -- skip
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
files_checked += 1
|
|
224
|
+
file_has_violation = False
|
|
225
|
+
|
|
226
|
+
for target_file in targets:
|
|
227
|
+
target_layer = classify_file(target_file, layers)
|
|
228
|
+
if not target_layer:
|
|
229
|
+
# Target outside all layers -- nothing to enforce
|
|
230
|
+
continue
|
|
231
|
+
if source_layer == target_layer:
|
|
232
|
+
# Intra-layer dependency -- always allowed
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Check file-level exceptions
|
|
236
|
+
exception_list = exceptions.get(source_file, [])
|
|
237
|
+
if target_file in exception_list:
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
rule = rule_map.get(source_layer)
|
|
241
|
+
if rule is None:
|
|
242
|
+
# No rule defined for this layer -- nothing to enforce
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
# Check explicit deny list first (high severity)
|
|
246
|
+
if target_layer in rule.denied:
|
|
247
|
+
violations.append(
|
|
248
|
+
ArchViolation(
|
|
249
|
+
source_file=source_file,
|
|
250
|
+
target_file=target_file,
|
|
251
|
+
source_layer=source_layer,
|
|
252
|
+
target_layer=target_layer,
|
|
253
|
+
rule=(
|
|
254
|
+
f"Layer '{source_layer}' must NOT depend on "
|
|
255
|
+
f"'{target_layer}' (deny rule)"
|
|
256
|
+
),
|
|
257
|
+
severity="high",
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
file_has_violation = True
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# Check allowed list (medium severity if not explicitly listed)
|
|
264
|
+
if rule.allowed and target_layer not in rule.allowed:
|
|
265
|
+
violations.append(
|
|
266
|
+
ArchViolation(
|
|
267
|
+
source_file=source_file,
|
|
268
|
+
target_file=target_file,
|
|
269
|
+
source_layer=source_layer,
|
|
270
|
+
target_layer=target_layer,
|
|
271
|
+
rule=(
|
|
272
|
+
f"Layer '{source_layer}' does not list "
|
|
273
|
+
f"'{target_layer}' as an allowed dependency"
|
|
274
|
+
),
|
|
275
|
+
severity="medium",
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
file_has_violation = True
|
|
279
|
+
|
|
280
|
+
if not file_has_violation:
|
|
281
|
+
compliant_files += 1
|
|
282
|
+
|
|
283
|
+
# Sort: high severity first, then by source file for stability
|
|
284
|
+
_severity_order = {"high": 0, "medium": 1, "low": 2}
|
|
285
|
+
violations.sort(
|
|
286
|
+
key=lambda v: (_severity_order.get(v.severity, 9), v.source_file, v.target_file)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return ArchReport(
|
|
290
|
+
violations=violations,
|
|
291
|
+
layers_defined=len(layers),
|
|
292
|
+
rules_defined=len(rules),
|
|
293
|
+
files_checked=files_checked,
|
|
294
|
+
compliant_files=compliant_files,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
# Formatting
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def format_report(report: ArchReport) -> str:
|
|
304
|
+
"""Format an :class:`ArchReport` as human-readable text."""
|
|
305
|
+
lines: list[str] = []
|
|
306
|
+
lines.append("# Architecture Drift Report\n")
|
|
307
|
+
|
|
308
|
+
lines.append(
|
|
309
|
+
f"Layers defined: {report.layers_defined} | "
|
|
310
|
+
f"Rules defined: {report.rules_defined}"
|
|
311
|
+
)
|
|
312
|
+
lines.append(
|
|
313
|
+
f"Files checked: {report.files_checked} | "
|
|
314
|
+
f"Compliant: {report.compliant_files}"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if not report.violations:
|
|
318
|
+
lines.append("\nNo architecture violations detected. All checked files comply.")
|
|
319
|
+
return "\n".join(lines)
|
|
320
|
+
|
|
321
|
+
# Group by severity
|
|
322
|
+
high: list[ArchViolation] = []
|
|
323
|
+
medium: list[ArchViolation] = []
|
|
324
|
+
other: list[ArchViolation] = []
|
|
325
|
+
for v in report.violations:
|
|
326
|
+
if v.severity == "high":
|
|
327
|
+
high.append(v)
|
|
328
|
+
elif v.severity == "medium":
|
|
329
|
+
medium.append(v)
|
|
330
|
+
else:
|
|
331
|
+
other.append(v)
|
|
332
|
+
|
|
333
|
+
total = len(report.violations)
|
|
334
|
+
lines.append(f"\n{total} violation(s) found.\n")
|
|
335
|
+
|
|
336
|
+
if high:
|
|
337
|
+
lines.append(f"## HIGH severity ({len(high)})\n")
|
|
338
|
+
for v in high:
|
|
339
|
+
lines.append(f" {v.source_file} -> {v.target_file}")
|
|
340
|
+
lines.append(f" [{v.source_layer} -> {v.target_layer}] {v.rule}")
|
|
341
|
+
|
|
342
|
+
if medium:
|
|
343
|
+
lines.append(f"\n## MEDIUM severity ({len(medium)})\n")
|
|
344
|
+
for v in medium:
|
|
345
|
+
lines.append(f" {v.source_file} -> {v.target_file}")
|
|
346
|
+
lines.append(f" [{v.source_layer} -> {v.target_layer}] {v.rule}")
|
|
347
|
+
|
|
348
|
+
if other:
|
|
349
|
+
lines.append(f"\n## OTHER ({len(other)})\n")
|
|
350
|
+
for v in other:
|
|
351
|
+
lines.append(f" {v.source_file} -> {v.target_file}")
|
|
352
|
+
lines.append(f" [{v.source_layer} -> {v.target_layer}] {v.rule}")
|
|
353
|
+
|
|
354
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""AST-aware code chunking for semantic search.
|
|
2
|
+
|
|
3
|
+
Produces structural chunks at function/method/class boundaries that include
|
|
4
|
+
actual source code, not just metadata summaries. This significantly improves
|
|
5
|
+
search recall for code-understanding queries (cAST paper: +4.3pts Recall@5).
|
|
6
|
+
|
|
7
|
+
Used by ``SemanticSearchManager`` in two-stage retrieval mode.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class CodeChunk:
|
|
22
|
+
"""A structural code chunk extracted from AST boundaries."""
|
|
23
|
+
|
|
24
|
+
id: str # unique chunk ID (e.g. "func:path:name")
|
|
25
|
+
file_path: str # relative path
|
|
26
|
+
chunk_type: str # "file", "function", "class", "method"
|
|
27
|
+
name: str # symbol name
|
|
28
|
+
text: str # actual code content (for embedding)
|
|
29
|
+
start_line: int
|
|
30
|
+
end_line: int
|
|
31
|
+
metadata: str # additional context (imports, params, bases)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def chunk_file(
|
|
35
|
+
file_path: str,
|
|
36
|
+
relative_path: str,
|
|
37
|
+
content: str | None = None,
|
|
38
|
+
max_chunk_lines: int = 80,
|
|
39
|
+
) -> list[CodeChunk]:
|
|
40
|
+
"""Extract structural code chunks from a file using AST boundaries.
|
|
41
|
+
|
|
42
|
+
Chunks at function, method, and class boundaries. Each chunk includes
|
|
43
|
+
the actual source code (truncated to ``max_chunk_lines``) plus metadata
|
|
44
|
+
(imports, parameters, return types, base classes).
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
file_path: Absolute path to the source file.
|
|
48
|
+
relative_path: Relative path for chunk IDs.
|
|
49
|
+
content: Optional file content (read from disk if not provided).
|
|
50
|
+
max_chunk_lines: Maximum lines of code to include per chunk.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of CodeChunk objects.
|
|
54
|
+
"""
|
|
55
|
+
if content is None:
|
|
56
|
+
try:
|
|
57
|
+
content = Path(file_path).read_text(encoding="utf-8", errors="replace")
|
|
58
|
+
except OSError:
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
from attocode_intel._internal.integrations.context.codebase_ast import parse_file
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
ast = parse_file(file_path, content=content)
|
|
65
|
+
except Exception:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
lines = content.splitlines()
|
|
69
|
+
chunks: list[CodeChunk] = []
|
|
70
|
+
|
|
71
|
+
# File-level chunk: imports + top-level summary
|
|
72
|
+
imports = [imp.module for imp in ast.imports[:15]]
|
|
73
|
+
symbols = ast.get_symbols()[:15]
|
|
74
|
+
file_meta_parts = []
|
|
75
|
+
if imports:
|
|
76
|
+
file_meta_parts.append(f"imports: {', '.join(imports)}")
|
|
77
|
+
if symbols:
|
|
78
|
+
file_meta_parts.append(f"defines: {', '.join(symbols)}")
|
|
79
|
+
|
|
80
|
+
# Take first N lines as file overview (docstring + imports area)
|
|
81
|
+
file_preview_end = min(40, len(lines))
|
|
82
|
+
file_text = "\n".join(lines[:file_preview_end])
|
|
83
|
+
if len(lines) > file_preview_end:
|
|
84
|
+
file_text += f"\n... ({len(lines) - file_preview_end} more lines)"
|
|
85
|
+
|
|
86
|
+
chunks.append(CodeChunk(
|
|
87
|
+
id=f"file:{relative_path}",
|
|
88
|
+
file_path=relative_path,
|
|
89
|
+
chunk_type="file",
|
|
90
|
+
name=os.path.basename(relative_path),
|
|
91
|
+
text=file_text,
|
|
92
|
+
start_line=1,
|
|
93
|
+
end_line=len(lines),
|
|
94
|
+
metadata=" | ".join(file_meta_parts),
|
|
95
|
+
))
|
|
96
|
+
|
|
97
|
+
# Function-level chunks
|
|
98
|
+
for func in ast.functions:
|
|
99
|
+
start = max(0, func.start_line - 1)
|
|
100
|
+
end = min(len(lines), func.end_line)
|
|
101
|
+
code_lines = lines[start:end]
|
|
102
|
+
if len(code_lines) > max_chunk_lines:
|
|
103
|
+
code_lines = code_lines[:max_chunk_lines]
|
|
104
|
+
code_lines.append(f" # ... ({func.end_line - func.start_line - max_chunk_lines} more lines)")
|
|
105
|
+
|
|
106
|
+
params = ", ".join(p.name for p in func.parameters[:8])
|
|
107
|
+
meta_parts = [f"function {func.name}({params})"]
|
|
108
|
+
if func.return_type:
|
|
109
|
+
meta_parts.append(f"-> {func.return_type}")
|
|
110
|
+
if func.decorators:
|
|
111
|
+
meta_parts.append(f"decorators: {', '.join(func.decorators[:3])}")
|
|
112
|
+
|
|
113
|
+
chunks.append(CodeChunk(
|
|
114
|
+
id=f"func:{relative_path}:{func.name}",
|
|
115
|
+
file_path=relative_path,
|
|
116
|
+
chunk_type="function",
|
|
117
|
+
name=func.name,
|
|
118
|
+
text="\n".join(code_lines),
|
|
119
|
+
start_line=func.start_line,
|
|
120
|
+
end_line=func.end_line,
|
|
121
|
+
metadata=" | ".join(meta_parts),
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
# Class-level chunks
|
|
125
|
+
for cls in ast.classes:
|
|
126
|
+
start = max(0, cls.start_line - 1)
|
|
127
|
+
end = min(len(lines), cls.end_line)
|
|
128
|
+
code_lines = lines[start:end]
|
|
129
|
+
if len(code_lines) > max_chunk_lines:
|
|
130
|
+
code_lines = code_lines[:max_chunk_lines]
|
|
131
|
+
code_lines.append(f" # ... ({cls.end_line - cls.start_line - max_chunk_lines} more lines)")
|
|
132
|
+
|
|
133
|
+
meta_parts = [f"class {cls.name}"]
|
|
134
|
+
if cls.bases:
|
|
135
|
+
meta_parts.append(f"extends: {', '.join(cls.bases[:5])}")
|
|
136
|
+
methods = [m.name for m in cls.methods[:10]]
|
|
137
|
+
if methods:
|
|
138
|
+
meta_parts.append(f"methods: {', '.join(methods)}")
|
|
139
|
+
|
|
140
|
+
chunks.append(CodeChunk(
|
|
141
|
+
id=f"cls:{relative_path}:{cls.name}",
|
|
142
|
+
file_path=relative_path,
|
|
143
|
+
chunk_type="class",
|
|
144
|
+
name=cls.name,
|
|
145
|
+
text="\n".join(code_lines),
|
|
146
|
+
start_line=cls.start_line,
|
|
147
|
+
end_line=cls.end_line,
|
|
148
|
+
metadata=" | ".join(meta_parts),
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
# Method-level chunks for large classes
|
|
152
|
+
for method in cls.methods:
|
|
153
|
+
m_start = max(0, method.start_line - 1)
|
|
154
|
+
m_end = min(len(lines), method.end_line)
|
|
155
|
+
m_lines = lines[m_start:m_end]
|
|
156
|
+
if len(m_lines) > max_chunk_lines:
|
|
157
|
+
m_lines = m_lines[:max_chunk_lines]
|
|
158
|
+
m_lines.append(f" # ... ({method.end_line - method.start_line - max_chunk_lines} more lines)")
|
|
159
|
+
|
|
160
|
+
m_params = ", ".join(p.name for p in method.parameters[:8])
|
|
161
|
+
m_meta_parts = [f"method {cls.name}.{method.name}({m_params})"]
|
|
162
|
+
if method.return_type:
|
|
163
|
+
m_meta_parts.append(f"-> {method.return_type}")
|
|
164
|
+
|
|
165
|
+
chunks.append(CodeChunk(
|
|
166
|
+
id=f"method:{relative_path}:{cls.name}.{method.name}",
|
|
167
|
+
file_path=relative_path,
|
|
168
|
+
chunk_type="method",
|
|
169
|
+
name=f"{cls.name}.{method.name}",
|
|
170
|
+
text="\n".join(m_lines),
|
|
171
|
+
start_line=method.start_line,
|
|
172
|
+
end_line=method.end_line,
|
|
173
|
+
metadata=" | ".join(m_meta_parts),
|
|
174
|
+
))
|
|
175
|
+
|
|
176
|
+
return chunks
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def reciprocal_rank_fusion(
|
|
180
|
+
*result_lists: list[tuple[str, float]],
|
|
181
|
+
k: int = 60,
|
|
182
|
+
) -> list[tuple[str, float]]:
|
|
183
|
+
"""Merge multiple ranked result lists using Reciprocal Rank Fusion.
|
|
184
|
+
|
|
185
|
+
Each input is a list of (id, score) tuples sorted by score descending.
|
|
186
|
+
Returns a merged list sorted by fused score descending.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
*result_lists: Variable number of ranked result lists.
|
|
190
|
+
k: RRF constant (default 60, standard from literature).
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Merged list of (id, fused_score) sorted by score descending.
|
|
194
|
+
"""
|
|
195
|
+
fused: dict[str, float] = {}
|
|
196
|
+
|
|
197
|
+
for results in result_lists:
|
|
198
|
+
for rank, (item_id, _score) in enumerate(results):
|
|
199
|
+
fused[item_id] = fused.get(item_id, 0.0) + 1.0 / (k + rank + 1)
|
|
200
|
+
|
|
201
|
+
sorted_results = sorted(fused.items(), key=lambda x: x[1], reverse=True)
|
|
202
|
+
return sorted_results
|