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,63 @@
|
|
|
1
|
+
"""Shared community detection helpers.
|
|
2
|
+
|
|
3
|
+
Used by both the MCP tool (analysis_tools.py) and the HTTP service (service.py).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections import deque
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def louvain_communities(
|
|
12
|
+
all_files: set[str],
|
|
13
|
+
adj: dict[str, set[str]],
|
|
14
|
+
weights: dict[tuple[str, str], float],
|
|
15
|
+
) -> tuple[list[set[str]], float]:
|
|
16
|
+
"""Run Louvain community detection using networkx.
|
|
17
|
+
|
|
18
|
+
Returns ``([], 0.0)`` for an empty graph (no edges).
|
|
19
|
+
Raises ``ImportError`` if networkx is not installed.
|
|
20
|
+
"""
|
|
21
|
+
import networkx as nx
|
|
22
|
+
from networkx.algorithms.community import louvain_communities as _louvain
|
|
23
|
+
from networkx.algorithms.community import modularity
|
|
24
|
+
|
|
25
|
+
G = nx.Graph() # noqa: N806
|
|
26
|
+
G.add_nodes_from(all_files)
|
|
27
|
+
for src, neighbors in adj.items():
|
|
28
|
+
for tgt in neighbors:
|
|
29
|
+
if src < tgt: # avoid duplicate edges
|
|
30
|
+
w = weights.get((src, tgt), weights.get((tgt, src), 1.0))
|
|
31
|
+
G.add_edge(src, tgt, weight=w)
|
|
32
|
+
|
|
33
|
+
if G.number_of_edges() == 0:
|
|
34
|
+
return [{f} for f in all_files], 0.0
|
|
35
|
+
|
|
36
|
+
communities = [set(c) for c in _louvain(G, weight="weight", seed=42)]
|
|
37
|
+
mod = modularity(G, communities, weight="weight")
|
|
38
|
+
return communities, mod
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def bfs_connected_components(
|
|
42
|
+
all_files: set[str],
|
|
43
|
+
adj: dict[str, set[str]],
|
|
44
|
+
) -> tuple[list[set[str]], float]:
|
|
45
|
+
"""Fallback: connected components via BFS. Modularity = 0."""
|
|
46
|
+
visited: set[str] = set()
|
|
47
|
+
communities: list[set[str]] = []
|
|
48
|
+
for start in all_files:
|
|
49
|
+
if start in visited:
|
|
50
|
+
continue
|
|
51
|
+
component: set[str] = set()
|
|
52
|
+
bfs_queue: deque[str] = deque([start])
|
|
53
|
+
while bfs_queue:
|
|
54
|
+
node = bfs_queue.popleft()
|
|
55
|
+
if node in visited:
|
|
56
|
+
continue
|
|
57
|
+
visited.add(node)
|
|
58
|
+
component.add(node)
|
|
59
|
+
for neighbor in adj.get(node, set()):
|
|
60
|
+
if neighbor not in visited:
|
|
61
|
+
bfs_queue.append(neighbor)
|
|
62
|
+
communities.append(component)
|
|
63
|
+
return communities, 0.0
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Where a finding's confidence comes from.
|
|
2
|
+
|
|
3
|
+
Most confidences in this package are literals somebody picked: the eight in
|
|
4
|
+
``bug_finder._PATTERNS``, the ``0.6`` on synthesized rules, the ``0.7``/``0.8``
|
|
5
|
+
in ``dead_code_tools``. Two scorers can replace them, chosen with
|
|
6
|
+
``ATTOCODE_FLAG_CONFIDENCE``: jev and llm. Historical experiments and their
|
|
7
|
+
limitations live in ``eval/rule_accuracy/demo``; their results are not
|
|
8
|
+
general accuracy guarantees.
|
|
9
|
+
|
|
10
|
+
``ATTOCODE_FLAG_CONFIDENCE_MODE`` picks what happens with the estimate:
|
|
11
|
+
``shadow`` logs it beside the constant and changes nothing, ``live`` uses it.
|
|
12
|
+
Both default to off/shadow, so nothing here runs unless asked.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import logging
|
|
19
|
+
import math
|
|
20
|
+
from contextvars import ContextVar
|
|
21
|
+
from copy import deepcopy
|
|
22
|
+
from typing import TYPE_CHECKING, Any
|
|
23
|
+
|
|
24
|
+
from attocode_intel.confidence import jev as jev_scorer
|
|
25
|
+
from attocode_intel.confidence import llm as llm_scorer
|
|
26
|
+
from attocode_intel.confidence import settings
|
|
27
|
+
from attocode_intel.confidence.redact import redact
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from collections.abc import Sequence
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
__all__ = ["MAX_CALLS", "last_report", "last_run", "mode", "redact", "rule_threshold", "score", "scorer_name", "summary"]
|
|
35
|
+
|
|
36
|
+
# ponytail: per-call cap. A corpus file yields a handful of findings; a wide
|
|
37
|
+
# diff can yield hundreds, and each one is a network round trip.
|
|
38
|
+
MAX_CALLS = 25
|
|
39
|
+
|
|
40
|
+
_SCORERS = {"jev": jev_scorer, "llm": llm_scorer}
|
|
41
|
+
|
|
42
|
+
_last: ContextVar[tuple[int, int]] = ContextVar("confidence_counts", default=(0, 0))
|
|
43
|
+
_report: ContextVar[dict | None] = ContextVar("confidence_report", default=None)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def last_run() -> tuple[int, int]:
|
|
47
|
+
"""(estimated, considered) from the most recent score() call."""
|
|
48
|
+
return _last.get()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def last_report() -> dict:
|
|
52
|
+
"""A detached request-local audit, including suppressed and unscored candidates."""
|
|
53
|
+
return deepcopy(_report.get() or {})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def summary() -> str:
|
|
57
|
+
report = _report.get() or {}
|
|
58
|
+
if not report or report["scorer"] == "off":
|
|
59
|
+
return ""
|
|
60
|
+
return (
|
|
61
|
+
f"Confidence: {report['scorer']} ({report['mode']}); "
|
|
62
|
+
f"{report['estimated']}/{report['total']} scored, "
|
|
63
|
+
f"{report['fallback']} fallback, {report['skipped']} skipped by cap. "
|
|
64
|
+
"Fallback and skipped findings retain their baseline confidence."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def rule_threshold(min_confidence: float) -> float:
|
|
69
|
+
"""Live estimates can promote rules below their old constant threshold."""
|
|
70
|
+
return 0.0 if settings.selected() != "off" and mode() == "live" else min_confidence
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def scorer_name() -> str:
|
|
74
|
+
"""Which scorer is selected: "off", "jev" or "llm"."""
|
|
75
|
+
name = settings.selected()
|
|
76
|
+
if name == "jev":
|
|
77
|
+
try:
|
|
78
|
+
if not jev_scorer.available():
|
|
79
|
+
return "off"
|
|
80
|
+
except (ValueError, OSError):
|
|
81
|
+
return "off"
|
|
82
|
+
return name if name in _SCORERS else "off"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def mode() -> str:
|
|
86
|
+
"""What to do with an estimate: "shadow" (log only) or "live" (use it)."""
|
|
87
|
+
value = settings.mode()
|
|
88
|
+
return "live" if value == "live" else "shadow"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def score(findings: Sequence[Any], *, min_confidence: float) -> tuple[int, int]:
|
|
92
|
+
"""Estimate p(true positive) per finding and log it beside the constant.
|
|
93
|
+
|
|
94
|
+
In ``live`` mode the estimate replaces ``finding.confidence``. Findings the
|
|
95
|
+
scorer has no opinion on keep their constant. Never raises: a scorer outage
|
|
96
|
+
must not cost a caller its findings.
|
|
97
|
+
|
|
98
|
+
Returns (estimated, considered) so a caller can tell a real score from a
|
|
99
|
+
scorer that ran but never committed to anything.
|
|
100
|
+
"""
|
|
101
|
+
requested = settings.selected()
|
|
102
|
+
name = scorer_name()
|
|
103
|
+
items = list(findings)
|
|
104
|
+
batch = items[:MAX_CALLS] if name != "off" else []
|
|
105
|
+
estimates: list = []
|
|
106
|
+
if batch:
|
|
107
|
+
try:
|
|
108
|
+
estimates = list(_SCORERS[name].estimate(batch, min_confidence=min_confidence))
|
|
109
|
+
except Exception:
|
|
110
|
+
logger.warning("confidence: %s scorer failed, keeping constants", name)
|
|
111
|
+
live = mode() == "live"
|
|
112
|
+
estimated = 0
|
|
113
|
+
rows = []
|
|
114
|
+
for index, finding in enumerate(items):
|
|
115
|
+
baseline = finding.confidence
|
|
116
|
+
estimate = estimates[index] if index < len(batch) and index < len(estimates) else None
|
|
117
|
+
if isinstance(estimate, bool) or not isinstance(estimate, int | float) or not 0 <= estimate <= 1 or not math.isfinite(estimate):
|
|
118
|
+
estimate = None
|
|
119
|
+
if estimate is not None:
|
|
120
|
+
estimated += 1
|
|
121
|
+
if live:
|
|
122
|
+
finding.confidence = estimate
|
|
123
|
+
status = ("off" if requested == "off" else "unavailable" if name == "off"
|
|
124
|
+
else "skipped_cap" if index >= len(batch)
|
|
125
|
+
else "estimated" if estimate is not None else "fallback")
|
|
126
|
+
identity = f"{finding.file}:{finding.line}:{getattr(finding, 'rule_id', '')}"
|
|
127
|
+
rows.append({
|
|
128
|
+
"id": hashlib.sha256(identity.encode()).hexdigest()[:20],
|
|
129
|
+
"file": finding.file, "line": finding.line,
|
|
130
|
+
"rule": getattr(finding, "rule_id", ""),
|
|
131
|
+
"baseline": baseline, "estimate": estimate, "effective": finding.confidence,
|
|
132
|
+
"status": status, "kept": finding.confidence >= min_confidence,
|
|
133
|
+
})
|
|
134
|
+
report = {
|
|
135
|
+
"scorer": requested, "mode": mode(), "total": len(items), "estimated": estimated,
|
|
136
|
+
"considered": len(batch), "skipped": sum(r["status"] == "skipped_cap" for r in rows),
|
|
137
|
+
"fallback": sum(r["status"] in {"fallback", "unavailable"} for r in rows),
|
|
138
|
+
"findings": rows,
|
|
139
|
+
}
|
|
140
|
+
_report.set(report)
|
|
141
|
+
_last.set((estimated, len(batch)))
|
|
142
|
+
if requested != "off":
|
|
143
|
+
logger.info("%s", summary())
|
|
144
|
+
return last_run()
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Jev scorer — a calibrated probability in place of a hardcoded constant.
|
|
2
|
+
|
|
3
|
+
Jev (TypeSafe System One) answers a yes/no question with a probability.
|
|
4
|
+
Historical comparisons and their limitations live in eval/rule_accuracy/demo.
|
|
5
|
+
|
|
6
|
+
It needs no client library. The backend is a public HTTP endpoint and one POST
|
|
7
|
+
is the whole protocol. Supported backends are OpenRouter, TypeSafe, and a
|
|
8
|
+
local server, with credentials selected for the configured backend.
|
|
9
|
+
|
|
10
|
+
If the standalone `jev` CLI happens to be importable, its decide() is used
|
|
11
|
+
outside workspace-scoped requests. It also appends a row to
|
|
12
|
+
~/.jev/decisions.jsonl, which is what `jev label` and `jev report` read.
|
|
13
|
+
|
|
14
|
+
An unkeyed or unreachable backend yields no estimate, and the caller keeps its
|
|
15
|
+
constant.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import ipaddress
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import math
|
|
24
|
+
import os
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.request
|
|
27
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
28
|
+
from typing import TYPE_CHECKING, Any
|
|
29
|
+
from urllib.parse import urlsplit
|
|
30
|
+
|
|
31
|
+
from attocode_intel.confidence import settings
|
|
32
|
+
from attocode_intel.confidence.redact import redact
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Sequence
|
|
36
|
+
|
|
37
|
+
try: # optional: only for the decisions.jsonl log the jev CLI reads
|
|
38
|
+
import jev
|
|
39
|
+
|
|
40
|
+
_HAS_JEV = True
|
|
41
|
+
except ImportError:
|
|
42
|
+
jev = None
|
|
43
|
+
_HAS_JEV = False
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
_env_loaded = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _load_env() -> None:
|
|
51
|
+
"""Populate keys once, from wherever this machine keeps them.
|
|
52
|
+
|
|
53
|
+
The jev CLI keeps its key in ~/.jev/env; the project keeps one in .env.
|
|
54
|
+
Neither is on a hook's or a subprocess's environment by default.
|
|
55
|
+
"""
|
|
56
|
+
global _env_loaded
|
|
57
|
+
if settings.project_dir():
|
|
58
|
+
return # Scoped requests read .env without mutating process credentials.
|
|
59
|
+
if _env_loaded:
|
|
60
|
+
return
|
|
61
|
+
_env_loaded = True
|
|
62
|
+
if _HAS_JEV:
|
|
63
|
+
jev.load_env()
|
|
64
|
+
try:
|
|
65
|
+
from dotenv import load_dotenv
|
|
66
|
+
except ImportError: # pragma: no cover - declared dependency
|
|
67
|
+
return
|
|
68
|
+
load_dotenv()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# backend -> (url, model, key env var). Mirrors the jev CLI so a row logged by
|
|
72
|
+
# either route is directly comparable.
|
|
73
|
+
_BACKENDS = {
|
|
74
|
+
"openrouter": (
|
|
75
|
+
"https://openrouter.ai/api/alpha/decisions",
|
|
76
|
+
"typesafe/jev-1.13",
|
|
77
|
+
"OPENROUTER_API_KEY",
|
|
78
|
+
),
|
|
79
|
+
"typesafe": ("https://api.typesafe.ai/v1/systemone", "jev-1.13.0", "TYPESAFE_API_KEY"),
|
|
80
|
+
"local": (None, "jev-latest", "OPENJEV_API_KEY"),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Measured on the rule corpus: near-linear to 16 workers (9.8x), flat after.
|
|
84
|
+
WORKERS = 16
|
|
85
|
+
|
|
86
|
+
QUESTION = "Is this rule match a true positive at this location?"
|
|
87
|
+
CRITERIA = {
|
|
88
|
+
"true": "The problem the rule describes is really present in this code.",
|
|
89
|
+
"false": "Test code, a comment, a string literal, a dead branch, or the case "
|
|
90
|
+
"is already handled nearby.",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def available() -> bool:
|
|
95
|
+
"""True when a backend can be reached: a key, or a local server."""
|
|
96
|
+
_load_env()
|
|
97
|
+
chosen = backend()
|
|
98
|
+
return bool(chosen == "local" and (
|
|
99
|
+
settings.environment().get("JEV_BASE_URL")
|
|
100
|
+
or settings.environment().get("JEV_BACKEND") == "local"
|
|
101
|
+
or settings.local_settings().get("backend") == "local"
|
|
102
|
+
or settings.environment().get("ATTOCODE_LOCAL_ONLY", "").lower() in {"1", "true", "yes", "on"}
|
|
103
|
+
) or settings.environment().get(_BACKENDS[chosen][2])
|
|
104
|
+
or (_HAS_JEV and not settings.project_dir()))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _loopback(url: str) -> bool:
|
|
108
|
+
# IP literals only, as in the SystemOne ranker: a hostname can re-resolve.
|
|
109
|
+
try:
|
|
110
|
+
return ipaddress.ip_address(urlsplit(url).hostname or "").is_loopback
|
|
111
|
+
except ValueError:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
116
|
+
def redirect_request(self, *args: Any, **kwargs: Any) -> None:
|
|
117
|
+
return None # urllib would resend the key to the new URL; the 3xx becomes an error
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _open(request: urllib.request.Request, timeout: float) -> Any:
|
|
121
|
+
"""Send without redirects, and without an ambient proxy for a loopback server."""
|
|
122
|
+
handlers: list[urllib.request.BaseHandler] = [_NoRedirect()]
|
|
123
|
+
if _loopback(request.full_url):
|
|
124
|
+
handlers.append(urllib.request.ProxyHandler({}))
|
|
125
|
+
return urllib.request.build_opener(*handlers).open(request, timeout=timeout)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _decide(site: str, state: dict[str, Any], questions: dict[str, Any],
|
|
129
|
+
incumbent: str, chosen: str) -> dict[str, Any]:
|
|
130
|
+
"""One decision. Uses the jev CLI when present, else a direct POST."""
|
|
131
|
+
if _HAS_JEV and not settings.project_dir():
|
|
132
|
+
result: dict[str, Any] = jev.decide(
|
|
133
|
+
site, state, questions, incumbent=incumbent, backend=chosen,
|
|
134
|
+
)
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
env = settings.environment()
|
|
138
|
+
url, model, keyvar = _BACKENDS[chosen]
|
|
139
|
+
if url is None:
|
|
140
|
+
# 127.0.0.1, not localhost: localhost resolves to ::1 first.
|
|
141
|
+
base = env.get("JEV_BASE_URL", "http://127.0.0.1:8000").rstrip("/")
|
|
142
|
+
if not _loopback(base) and (
|
|
143
|
+
"JEV_BASE_URL" not in os.environ # a repository .env may not pick the host
|
|
144
|
+
or env.get("ATTOCODE_LOCAL_ONLY", "").lower() in {"1", "true", "yes", "on"}
|
|
145
|
+
):
|
|
146
|
+
raise ValueError("a local Jev server must use a loopback IP address")
|
|
147
|
+
url = base + "/v1/systemone"
|
|
148
|
+
headers = {"Content-Type": "application/json"}
|
|
149
|
+
key = env.get(keyvar)
|
|
150
|
+
if key:
|
|
151
|
+
headers["Authorization"] = "Bearer " + key
|
|
152
|
+
body = json.dumps(
|
|
153
|
+
{"model": env.get("JEV_MODEL", model), "state": state, "questions": questions}
|
|
154
|
+
).encode()
|
|
155
|
+
request = urllib.request.Request(url, data=body, headers=headers) # noqa: S310
|
|
156
|
+
timeout = float(env.get("JEV_TIMEOUT", "20"))
|
|
157
|
+
with _open(request, timeout) as response:
|
|
158
|
+
parsed: dict[str, Any] = json.loads(response.read())
|
|
159
|
+
return {"answers": parsed.get("answers"), "error": None}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def backend() -> str:
|
|
163
|
+
"""Pick a Jev backend: local when offline or unkeyed, else the configured one."""
|
|
164
|
+
_load_env()
|
|
165
|
+
env = settings.environment()
|
|
166
|
+
if env.get("ATTOCODE_LOCAL_ONLY", "").lower() in {"1", "true", "yes", "on"}:
|
|
167
|
+
return "local"
|
|
168
|
+
explicit = env.get("JEV_BACKEND") or settings.local_settings().get("backend")
|
|
169
|
+
if explicit:
|
|
170
|
+
if explicit not in _BACKENDS:
|
|
171
|
+
raise ValueError("Unsupported Jev backend")
|
|
172
|
+
return explicit
|
|
173
|
+
if env.get("OPENROUTER_API_KEY"):
|
|
174
|
+
return "openrouter"
|
|
175
|
+
if env.get("TYPESAFE_API_KEY"):
|
|
176
|
+
return "typesafe"
|
|
177
|
+
return "local"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _state(finding: Any) -> dict[str, Any]:
|
|
181
|
+
"""The evidence Jev sees. Redacted: this leaves the machine."""
|
|
182
|
+
code = "\n".join([
|
|
183
|
+
*getattr(finding, "context_before", []),
|
|
184
|
+
getattr(finding, "code_snippet", ""),
|
|
185
|
+
*getattr(finding, "context_after", []),
|
|
186
|
+
])
|
|
187
|
+
return {
|
|
188
|
+
"file": redact(finding.file),
|
|
189
|
+
"line": finding.line,
|
|
190
|
+
"rule": redact(getattr(finding, "rule_id", "")),
|
|
191
|
+
"message": redact(getattr(finding, "description", ""))[:1000],
|
|
192
|
+
"code": redact(code)[:3000],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def estimate(findings: Sequence[Any], *, min_confidence: float) -> list[float | None]:
|
|
197
|
+
"""Return p(true positive) per finding, or None where Jev gave no answer.
|
|
198
|
+
|
|
199
|
+
The incumbent recorded with each row is the constant's own verdict at
|
|
200
|
+
``min_confidence``, which makes every logged row a paired comparison.
|
|
201
|
+
"""
|
|
202
|
+
if not findings:
|
|
203
|
+
return []
|
|
204
|
+
|
|
205
|
+
_load_env()
|
|
206
|
+
questions = {"p": {"type": "noul", "instructions": QUESTION, "criteria": CRITERIA}}
|
|
207
|
+
chosen = backend()
|
|
208
|
+
|
|
209
|
+
def ask(finding: Any) -> float | None:
|
|
210
|
+
try:
|
|
211
|
+
row = _decide(
|
|
212
|
+
"attocode_rule_fp",
|
|
213
|
+
_state(finding),
|
|
214
|
+
questions,
|
|
215
|
+
"yes" if finding.confidence >= min_confidence else "no",
|
|
216
|
+
chosen,
|
|
217
|
+
)
|
|
218
|
+
except Exception: # thread plus network; a finding is not worth an outage
|
|
219
|
+
logger.debug("jev: decide failed for %s:%s", finding.file, finding.line)
|
|
220
|
+
return None
|
|
221
|
+
try:
|
|
222
|
+
if row.get("error"):
|
|
223
|
+
return None
|
|
224
|
+
value = ((row.get("answers") or {}).get("p") or {}).get("noul")
|
|
225
|
+
if isinstance(value, bool) or not isinstance(value, int | float):
|
|
226
|
+
return None
|
|
227
|
+
return float(value) if math.isfinite(value) and 0 <= value <= 1 else None
|
|
228
|
+
except (AttributeError, TypeError, ValueError, OverflowError):
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
with ThreadPoolExecutor(max_workers=WORKERS) as pool:
|
|
232
|
+
from contextvars import copy_context
|
|
233
|
+
futures = [pool.submit(copy_context().run, ask, finding) for finding in findings]
|
|
234
|
+
return [future.result() for future in futures]
|