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,332 @@
|
|
|
1
|
+
"""Rule profiling and confidence calibration.
|
|
2
|
+
|
|
3
|
+
Tracks per-rule execution time, match counts, and true/false positive
|
|
4
|
+
feedback to calibrate confidence scores over time.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True)
|
|
20
|
+
class RuleStats:
|
|
21
|
+
"""Per-rule profiling statistics."""
|
|
22
|
+
|
|
23
|
+
rule_id: str
|
|
24
|
+
total_time_ms: float = 0.0
|
|
25
|
+
match_count: int = 0
|
|
26
|
+
files_scanned: int = 0
|
|
27
|
+
true_positives: int = 0
|
|
28
|
+
false_positives: int = 0
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def calibrated_confidence(self) -> float | None:
|
|
32
|
+
"""Compute calibrated confidence from TP/FP feedback.
|
|
33
|
+
|
|
34
|
+
Returns None if insufficient data (<5 samples).
|
|
35
|
+
"""
|
|
36
|
+
total = self.true_positives + self.false_positives
|
|
37
|
+
if total < 5:
|
|
38
|
+
return None
|
|
39
|
+
return self.true_positives / total
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def false_positive_rate(self) -> float | None:
|
|
43
|
+
total = self.true_positives + self.false_positives
|
|
44
|
+
if total == 0:
|
|
45
|
+
return None
|
|
46
|
+
return self.false_positives / total
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RuleProfiler:
|
|
50
|
+
"""Thread-safe rule profiler with per-rule timing and feedback tracking."""
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
self._stats: dict[str, RuleStats] = {}
|
|
54
|
+
self._lock = threading.Lock()
|
|
55
|
+
self._timers: dict[tuple[str, int], float] = {} # (rule_id, thread_id) -> start
|
|
56
|
+
|
|
57
|
+
def _ensure(self, rule_id: str) -> RuleStats:
|
|
58
|
+
if rule_id not in self._stats:
|
|
59
|
+
self._stats[rule_id] = RuleStats(rule_id=rule_id)
|
|
60
|
+
return self._stats[rule_id]
|
|
61
|
+
|
|
62
|
+
def start(self, rule_id: str) -> None:
|
|
63
|
+
"""Start timing a rule execution."""
|
|
64
|
+
key = (rule_id, threading.get_ident())
|
|
65
|
+
with self._lock:
|
|
66
|
+
self._timers[key] = time.monotonic()
|
|
67
|
+
|
|
68
|
+
def stop(self, rule_id: str) -> None:
|
|
69
|
+
"""Stop timing and record elapsed time."""
|
|
70
|
+
key = (rule_id, threading.get_ident())
|
|
71
|
+
with self._lock:
|
|
72
|
+
start = self._timers.pop(key, None)
|
|
73
|
+
if start is None:
|
|
74
|
+
return
|
|
75
|
+
elapsed_ms = (time.monotonic() - start) * 1000
|
|
76
|
+
with self._lock:
|
|
77
|
+
stats = self._ensure(rule_id)
|
|
78
|
+
stats.total_time_ms += elapsed_ms
|
|
79
|
+
|
|
80
|
+
def record_match(self, rule_id: str) -> None:
|
|
81
|
+
"""Record a rule match."""
|
|
82
|
+
with self._lock:
|
|
83
|
+
self._ensure(rule_id).match_count += 1
|
|
84
|
+
|
|
85
|
+
def record_file_scanned(self, rule_id: str) -> None:
|
|
86
|
+
"""Record that a rule was evaluated against a file."""
|
|
87
|
+
with self._lock:
|
|
88
|
+
self._ensure(rule_id).files_scanned += 1
|
|
89
|
+
|
|
90
|
+
def record_feedback(self, rule_id: str, *, is_true_positive: bool) -> None:
|
|
91
|
+
"""Record TP/FP feedback for confidence calibration."""
|
|
92
|
+
with self._lock:
|
|
93
|
+
stats = self._ensure(rule_id)
|
|
94
|
+
if is_true_positive:
|
|
95
|
+
stats.true_positives += 1
|
|
96
|
+
else:
|
|
97
|
+
stats.false_positives += 1
|
|
98
|
+
|
|
99
|
+
def get_stats(self, rule_id: str = "") -> dict[str, RuleStats]:
|
|
100
|
+
"""Get stats for a specific rule or all rules."""
|
|
101
|
+
with self._lock:
|
|
102
|
+
if rule_id:
|
|
103
|
+
s = self._stats.get(rule_id)
|
|
104
|
+
return {rule_id: s} if s else {}
|
|
105
|
+
return dict(self._stats)
|
|
106
|
+
|
|
107
|
+
def reset(self) -> None:
|
|
108
|
+
"""Clear all profiling data."""
|
|
109
|
+
with self._lock:
|
|
110
|
+
self._stats.clear()
|
|
111
|
+
self._timers.clear()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Persistent feedback store
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class FeedbackStore:
|
|
120
|
+
"""Persistent rule-quality state stored in .attocode/rule_feedback.json.
|
|
121
|
+
|
|
122
|
+
Tracks both TP/FP feedback (for confidence calibration) and per-rule
|
|
123
|
+
scan/match counters plus hygiene-applied disable state (consumed by
|
|
124
|
+
``rules.hygiene``). One JSON file keeps related metadata together so
|
|
125
|
+
auto-prune decisions persist across sessions.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(self, project_dir: str) -> None:
|
|
129
|
+
self._path = Path(project_dir) / ".attocode" / "rule_feedback.json"
|
|
130
|
+
self._data: dict[str, dict[str, object]] = {}
|
|
131
|
+
# Single in-process lock guards both the in-memory map and the
|
|
132
|
+
# read-modify-write JSON file. Cross-process safety would need a
|
|
133
|
+
# ``filelock``; in-process MCP/HTTP concurrency is the immediate
|
|
134
|
+
# need here (record_session() fires on every analyze() call).
|
|
135
|
+
self._lock = threading.Lock()
|
|
136
|
+
self._load()
|
|
137
|
+
|
|
138
|
+
def _load(self) -> None:
|
|
139
|
+
if self._path.is_file():
|
|
140
|
+
try:
|
|
141
|
+
self._data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
142
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
143
|
+
logger.warning("Failed to load rule feedback: %s", exc)
|
|
144
|
+
self._data = {}
|
|
145
|
+
|
|
146
|
+
def _save_locked(self) -> None:
|
|
147
|
+
"""Persist to disk. Caller must hold ``self._lock``."""
|
|
148
|
+
try:
|
|
149
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
self._path.write_text(
|
|
151
|
+
json.dumps(self._data, indent=2, sort_keys=True),
|
|
152
|
+
encoding="utf-8",
|
|
153
|
+
)
|
|
154
|
+
except OSError as exc:
|
|
155
|
+
logger.warning("Failed to save rule feedback: %s", exc)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _entry_int(entry: dict[str, object], key: str) -> int:
|
|
159
|
+
value = entry.get(key, 0)
|
|
160
|
+
return int(value) if isinstance(value, (int, float)) else 0
|
|
161
|
+
|
|
162
|
+
def _ensure_locked(self, rule_id: str) -> dict[str, object]:
|
|
163
|
+
"""Caller must hold ``self._lock``."""
|
|
164
|
+
return self._data.setdefault(rule_id, {"tp": 0, "fp": 0})
|
|
165
|
+
|
|
166
|
+
def record(self, rule_id: str, *, is_true_positive: bool) -> None:
|
|
167
|
+
"""Record a TP/FP observation and persist."""
|
|
168
|
+
with self._lock:
|
|
169
|
+
entry = self._ensure_locked(rule_id)
|
|
170
|
+
if is_true_positive:
|
|
171
|
+
entry["tp"] = self._entry_int(entry, "tp") + 1
|
|
172
|
+
else:
|
|
173
|
+
entry["fp"] = self._entry_int(entry, "fp") + 1
|
|
174
|
+
total = self._entry_int(entry, "tp") + self._entry_int(entry, "fp")
|
|
175
|
+
if total >= 5:
|
|
176
|
+
tp = self._entry_int(entry, "tp")
|
|
177
|
+
entry["calibrated"] = round(tp / total, 3)
|
|
178
|
+
self._save_locked()
|
|
179
|
+
|
|
180
|
+
def record_session(
|
|
181
|
+
self,
|
|
182
|
+
rule_ids: list[str],
|
|
183
|
+
matches: dict[str, int],
|
|
184
|
+
*,
|
|
185
|
+
files_scanned: int = 0,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Record one analyze-session's scan + match counters across rules.
|
|
188
|
+
|
|
189
|
+
Each rule that was applied in the session has its ``scans`` counter
|
|
190
|
+
incremented by 1 (one session = one scan, irrespective of file count
|
|
191
|
+
— matching the roadmap's "0 matches across 10+ scanned repos"
|
|
192
|
+
framing). Matches are added cumulatively. ``files_scanned`` is
|
|
193
|
+
recorded as a rolling total per-rule for diagnostics only.
|
|
194
|
+
"""
|
|
195
|
+
if not rule_ids:
|
|
196
|
+
return
|
|
197
|
+
with self._lock:
|
|
198
|
+
for qid in rule_ids:
|
|
199
|
+
entry = self._ensure_locked(qid)
|
|
200
|
+
entry["scans"] = self._entry_int(entry, "scans") + 1
|
|
201
|
+
if files_scanned:
|
|
202
|
+
entry["files_scanned"] = (
|
|
203
|
+
self._entry_int(entry, "files_scanned") + files_scanned
|
|
204
|
+
)
|
|
205
|
+
entry["matches"] = (
|
|
206
|
+
self._entry_int(entry, "matches") + int(matches.get(qid, 0))
|
|
207
|
+
)
|
|
208
|
+
self._save_locked()
|
|
209
|
+
|
|
210
|
+
def set_disabled(self, rule_id: str, reason: str) -> None:
|
|
211
|
+
"""Mark a rule disabled with a reason (e.g. 'dead', 'noisy')."""
|
|
212
|
+
with self._lock:
|
|
213
|
+
entry = self._ensure_locked(rule_id)
|
|
214
|
+
entry["disabled"] = True
|
|
215
|
+
entry["disabled_reason"] = reason
|
|
216
|
+
self._save_locked()
|
|
217
|
+
|
|
218
|
+
def clear_disabled(self, rule_id: str) -> None:
|
|
219
|
+
"""Re-enable a previously hygiene-disabled rule."""
|
|
220
|
+
with self._lock:
|
|
221
|
+
entry = self._data.get(rule_id)
|
|
222
|
+
if entry is None:
|
|
223
|
+
return
|
|
224
|
+
entry["disabled"] = False
|
|
225
|
+
entry["disabled_reason"] = ""
|
|
226
|
+
self._save_locked()
|
|
227
|
+
|
|
228
|
+
def is_disabled(self, rule_id: str) -> bool:
|
|
229
|
+
with self._lock:
|
|
230
|
+
entry = self._data.get(rule_id, {})
|
|
231
|
+
return bool(entry.get("disabled", False))
|
|
232
|
+
|
|
233
|
+
def get_disabled_reason(self, rule_id: str) -> str:
|
|
234
|
+
with self._lock:
|
|
235
|
+
entry = self._data.get(rule_id, {})
|
|
236
|
+
value = entry.get("disabled_reason", "")
|
|
237
|
+
return value if isinstance(value, str) else ""
|
|
238
|
+
|
|
239
|
+
def get_scan_count(self, rule_id: str) -> int:
|
|
240
|
+
with self._lock:
|
|
241
|
+
return self._entry_int(self._data.get(rule_id, {}), "scans")
|
|
242
|
+
|
|
243
|
+
def get_match_count(self, rule_id: str) -> int:
|
|
244
|
+
with self._lock:
|
|
245
|
+
return self._entry_int(self._data.get(rule_id, {}), "matches")
|
|
246
|
+
|
|
247
|
+
def get_calibrated_confidence(self, rule_id: str) -> float | None:
|
|
248
|
+
"""Get calibrated confidence for a rule, or None if insufficient data."""
|
|
249
|
+
with self._lock:
|
|
250
|
+
entry = self._data.get(rule_id, {})
|
|
251
|
+
value = entry.get("calibrated")
|
|
252
|
+
return float(value) if isinstance(value, (int, float)) else None
|
|
253
|
+
|
|
254
|
+
def get_feedback(self, rule_id: str) -> dict[str, int]:
|
|
255
|
+
"""Get raw TP/FP counts for a rule."""
|
|
256
|
+
with self._lock:
|
|
257
|
+
entry = self._data.get(rule_id, {})
|
|
258
|
+
return {
|
|
259
|
+
"tp": self._entry_int(entry, "tp"),
|
|
260
|
+
"fp": self._entry_int(entry, "fp"),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
def all_feedback(self) -> dict[str, dict[str, object]]:
|
|
264
|
+
"""Get all stored data (TP/FP + scan/match + disabled state)."""
|
|
265
|
+
with self._lock:
|
|
266
|
+
return dict(self._data)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# Report formatting
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def format_rule_stats(
|
|
275
|
+
stats: dict[str, RuleStats],
|
|
276
|
+
feedback: dict[str, dict[str, object]] | None = None,
|
|
277
|
+
) -> str:
|
|
278
|
+
"""Format profiling stats as markdown."""
|
|
279
|
+
if not stats:
|
|
280
|
+
return "No profiling data available."
|
|
281
|
+
|
|
282
|
+
lines = ["# Rule Profiling Stats\n"]
|
|
283
|
+
|
|
284
|
+
# Sort by total time descending
|
|
285
|
+
sorted_stats = sorted(stats.values(), key=lambda s: s.total_time_ms, reverse=True)
|
|
286
|
+
|
|
287
|
+
lines.append("| Rule | Time (ms) | Matches | Files | TP | FP | Calibrated |")
|
|
288
|
+
lines.append("|------|-----------|---------|-------|----|----|------------|")
|
|
289
|
+
|
|
290
|
+
for s in sorted_stats:
|
|
291
|
+
fb = (feedback or {}).get(s.rule_id, {})
|
|
292
|
+
tp_raw = fb.get("tp", s.true_positives)
|
|
293
|
+
fp_raw = fb.get("fp", s.false_positives)
|
|
294
|
+
tp = int(tp_raw) if isinstance(tp_raw, (int, float)) else s.true_positives
|
|
295
|
+
fp = int(fp_raw) if isinstance(fp_raw, (int, float)) else s.false_positives
|
|
296
|
+
cal_raw = fb.get("calibrated", s.calibrated_confidence)
|
|
297
|
+
cal: float | None
|
|
298
|
+
if isinstance(cal_raw, (int, float)):
|
|
299
|
+
cal = float(cal_raw)
|
|
300
|
+
elif cal_raw is None:
|
|
301
|
+
cal = s.calibrated_confidence
|
|
302
|
+
else:
|
|
303
|
+
cal = None
|
|
304
|
+
cal_str = f"{cal:.2f}" if cal is not None else "—"
|
|
305
|
+
lines.append(
|
|
306
|
+
f"| `{s.rule_id}` | {s.total_time_ms:.1f} | {s.match_count} | "
|
|
307
|
+
f"{s.files_scanned} | {tp} | {fp} | {cal_str} |"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Summary
|
|
311
|
+
total_time = sum(s.total_time_ms for s in sorted_stats)
|
|
312
|
+
total_matches = sum(s.match_count for s in sorted_stats)
|
|
313
|
+
lines.append(f"\n**Total**: {total_time:.1f}ms across {len(sorted_stats)} rules, {total_matches} matches")
|
|
314
|
+
|
|
315
|
+
# Top 5 slowest
|
|
316
|
+
if len(sorted_stats) > 5:
|
|
317
|
+
lines.append("\n## Slowest Rules")
|
|
318
|
+
for s in sorted_stats[:5]:
|
|
319
|
+
lines.append(f"- `{s.rule_id}`: {s.total_time_ms:.1f}ms")
|
|
320
|
+
|
|
321
|
+
# Highest FP rate
|
|
322
|
+
fp_rules = [
|
|
323
|
+
s for s in sorted_stats
|
|
324
|
+
if s.false_positive_rate is not None and s.false_positive_rate > 0
|
|
325
|
+
]
|
|
326
|
+
if fp_rules:
|
|
327
|
+
fp_rules.sort(key=lambda s: s.false_positive_rate or 0, reverse=True) # type: ignore[arg-type]
|
|
328
|
+
lines.append("\n## Highest False Positive Rates")
|
|
329
|
+
for s in fp_rules[:5]:
|
|
330
|
+
lines.append(f"- `{s.rule_id}`: {s.false_positive_rate:.0%}")
|
|
331
|
+
|
|
332
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Rule registry — stores, indexes, and queries UnifiedRule instances."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
from attocode_intel.rules.model import (
|
|
10
|
+
RuleCategory,
|
|
11
|
+
RuleSeverity,
|
|
12
|
+
RuleSource,
|
|
13
|
+
RuleTier,
|
|
14
|
+
UnifiedRule,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
_SEV_RANK: dict[str, int] = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RuleRegistry:
|
|
23
|
+
"""Thread-safe registry of analysis rules.
|
|
24
|
+
|
|
25
|
+
Supports registration from builtins, language packs, and user plugins.
|
|
26
|
+
Provides filtered queries by language, category, severity, pack, etc.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._lock = threading.Lock()
|
|
31
|
+
self._rules: dict[str, UnifiedRule] = {} # qualified_id -> rule
|
|
32
|
+
self._by_language: dict[str, list[str]] = defaultdict(list)
|
|
33
|
+
self._by_category: dict[RuleCategory, list[str]] = defaultdict(list)
|
|
34
|
+
self._by_pack: dict[str, list[str]] = defaultdict(list)
|
|
35
|
+
self._by_severity: dict[RuleSeverity, list[str]] = defaultdict(list)
|
|
36
|
+
|
|
37
|
+
def register(self, rule: UnifiedRule) -> None:
|
|
38
|
+
"""Register a rule. Overwrites if qualified_id already exists."""
|
|
39
|
+
with self._lock:
|
|
40
|
+
qid = rule.qualified_id
|
|
41
|
+
was_update = qid in self._rules
|
|
42
|
+
if was_update:
|
|
43
|
+
self._unindex(qid)
|
|
44
|
+
self._rules[qid] = rule
|
|
45
|
+
self._index(rule)
|
|
46
|
+
if was_update:
|
|
47
|
+
logger.debug("Updated rule %s", qid)
|
|
48
|
+
else:
|
|
49
|
+
logger.debug("Registered rule %s", qid)
|
|
50
|
+
|
|
51
|
+
def register_many(self, rules: list[UnifiedRule]) -> int:
|
|
52
|
+
"""Register multiple rules. Returns count registered."""
|
|
53
|
+
for rule in rules:
|
|
54
|
+
self.register(rule)
|
|
55
|
+
return len(rules)
|
|
56
|
+
|
|
57
|
+
def get(self, qualified_id: str) -> UnifiedRule | None:
|
|
58
|
+
with self._lock:
|
|
59
|
+
return self._rules.get(qualified_id)
|
|
60
|
+
|
|
61
|
+
def remove(self, qualified_id: str) -> bool:
|
|
62
|
+
"""Remove a rule by qualified ID. Returns True if found."""
|
|
63
|
+
with self._lock:
|
|
64
|
+
if qualified_id not in self._rules:
|
|
65
|
+
return False
|
|
66
|
+
self._unindex(qualified_id)
|
|
67
|
+
del self._rules[qualified_id]
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
def enable(self, qualified_id: str) -> bool:
|
|
71
|
+
with self._lock:
|
|
72
|
+
rule = self._rules.get(qualified_id)
|
|
73
|
+
if rule is None:
|
|
74
|
+
return False
|
|
75
|
+
rule.enabled = True
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
def disable(self, qualified_id: str) -> bool:
|
|
79
|
+
with self._lock:
|
|
80
|
+
rule = self._rules.get(qualified_id)
|
|
81
|
+
if rule is None:
|
|
82
|
+
return False
|
|
83
|
+
rule.enabled = False
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def count(self) -> int:
|
|
88
|
+
with self._lock:
|
|
89
|
+
return len(self._rules)
|
|
90
|
+
|
|
91
|
+
def all_rules(self, *, enabled_only: bool = True) -> list[UnifiedRule]:
|
|
92
|
+
with self._lock:
|
|
93
|
+
rules = list(self._rules.values())
|
|
94
|
+
if enabled_only:
|
|
95
|
+
return [r for r in rules if r.enabled]
|
|
96
|
+
return rules
|
|
97
|
+
|
|
98
|
+
def query(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
language: str = "",
|
|
102
|
+
category: RuleCategory | str = "",
|
|
103
|
+
severity: RuleSeverity | str = "",
|
|
104
|
+
pack: str = "",
|
|
105
|
+
source: RuleSource | str = "",
|
|
106
|
+
tier: RuleTier | str = "",
|
|
107
|
+
tags: list[str] | None = None,
|
|
108
|
+
enabled_only: bool = True,
|
|
109
|
+
min_confidence: float = 0.0,
|
|
110
|
+
) -> list[UnifiedRule]:
|
|
111
|
+
"""Query rules with optional filters. All filters are AND-combined."""
|
|
112
|
+
# Snapshot under lock
|
|
113
|
+
with self._lock:
|
|
114
|
+
if language:
|
|
115
|
+
candidates = set(self._by_language.get(language, []))
|
|
116
|
+
for qid, rule in self._rules.items():
|
|
117
|
+
if not rule.languages:
|
|
118
|
+
candidates.add(qid)
|
|
119
|
+
else:
|
|
120
|
+
candidates = set(self._rules.keys())
|
|
121
|
+
rules_snapshot = {qid: self._rules[qid] for qid in candidates if qid in self._rules}
|
|
122
|
+
|
|
123
|
+
results: list[UnifiedRule] = []
|
|
124
|
+
for qid, rule in rules_snapshot.items():
|
|
125
|
+
if enabled_only and not rule.enabled:
|
|
126
|
+
continue
|
|
127
|
+
if category and rule.category != category:
|
|
128
|
+
continue
|
|
129
|
+
if severity and _SEV_RANK.get(rule.severity, 9) > _SEV_RANK.get(str(severity), 9):
|
|
130
|
+
continue
|
|
131
|
+
if pack and rule.pack != pack:
|
|
132
|
+
continue
|
|
133
|
+
if source and rule.source != source:
|
|
134
|
+
continue
|
|
135
|
+
if tier and rule.tier != tier:
|
|
136
|
+
continue
|
|
137
|
+
if min_confidence > 0.0 and rule.confidence < min_confidence:
|
|
138
|
+
continue
|
|
139
|
+
if tags and not set(tags).issubset(set(rule.tags)):
|
|
140
|
+
continue
|
|
141
|
+
results.append(rule)
|
|
142
|
+
|
|
143
|
+
_severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
144
|
+
results.sort(key=lambda r: (_severity_order.get(r.severity, 9), r.qualified_id))
|
|
145
|
+
return results
|
|
146
|
+
|
|
147
|
+
def languages(self) -> list[str]:
|
|
148
|
+
with self._lock:
|
|
149
|
+
return sorted(self._by_language.keys())
|
|
150
|
+
|
|
151
|
+
def packs(self) -> list[str]:
|
|
152
|
+
with self._lock:
|
|
153
|
+
return sorted(self._by_pack.keys())
|
|
154
|
+
|
|
155
|
+
def categories(self) -> list[tuple[RuleCategory, int]]:
|
|
156
|
+
with self._lock:
|
|
157
|
+
return sorted(
|
|
158
|
+
[(cat, len(ids)) for cat, ids in self._by_category.items()],
|
|
159
|
+
key=lambda x: x[1],
|
|
160
|
+
reverse=True,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def stats(self) -> dict[str, int | dict[str, int]]:
|
|
164
|
+
with self._lock:
|
|
165
|
+
by_sev = {str(sev): len(ids) for sev, ids in self._by_severity.items()}
|
|
166
|
+
by_cat = {str(cat): len(ids) for cat, ids in self._by_category.items()}
|
|
167
|
+
by_src: dict[str, int] = defaultdict(int)
|
|
168
|
+
by_tier_d: dict[str, int] = defaultdict(int)
|
|
169
|
+
enabled = sum(1 for r in self._rules.values() if r.enabled)
|
|
170
|
+
for r in self._rules.values():
|
|
171
|
+
by_src[str(r.source)] += 1
|
|
172
|
+
by_tier_d[str(r.tier)] += 1
|
|
173
|
+
return {
|
|
174
|
+
"total": len(self._rules),
|
|
175
|
+
"enabled": enabled,
|
|
176
|
+
"by_severity": dict(by_sev),
|
|
177
|
+
"by_category": by_cat,
|
|
178
|
+
"by_source": dict(by_src),
|
|
179
|
+
"by_tier": dict(by_tier_d),
|
|
180
|
+
"languages": len(self._by_language),
|
|
181
|
+
"packs": len(self._by_pack),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# --- Internal indexing ---
|
|
185
|
+
|
|
186
|
+
def _index(self, rule: UnifiedRule) -> None:
|
|
187
|
+
qid = rule.qualified_id
|
|
188
|
+
for lang in rule.languages:
|
|
189
|
+
self._by_language[lang].append(qid)
|
|
190
|
+
self._by_category[rule.category].append(qid)
|
|
191
|
+
self._by_severity[rule.severity].append(qid)
|
|
192
|
+
if rule.pack:
|
|
193
|
+
self._by_pack[rule.pack].append(qid)
|
|
194
|
+
|
|
195
|
+
def _unindex(self, qualified_id: str) -> None:
|
|
196
|
+
rule = self._rules.get(qualified_id)
|
|
197
|
+
if rule is None:
|
|
198
|
+
return
|
|
199
|
+
qid = rule.qualified_id
|
|
200
|
+
for lang in rule.languages:
|
|
201
|
+
lst = self._by_language.get(lang, [])
|
|
202
|
+
if qid in lst:
|
|
203
|
+
lst.remove(qid)
|
|
204
|
+
cat_list = self._by_category.get(rule.category, [])
|
|
205
|
+
if qid in cat_list:
|
|
206
|
+
cat_list.remove(qid)
|
|
207
|
+
sev_list = self._by_severity.get(rule.severity, [])
|
|
208
|
+
if qid in sev_list:
|
|
209
|
+
sev_list.remove(qid)
|
|
210
|
+
if rule.pack:
|
|
211
|
+
pack_list = self._by_pack.get(rule.pack, [])
|
|
212
|
+
if qid in pack_list:
|
|
213
|
+
pack_list.remove(qid)
|