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,992 @@
|
|
|
1
|
+
"""Rule-based analysis tools for the code-intel MCP server.
|
|
2
|
+
|
|
3
|
+
Tools:
|
|
4
|
+
Core: analyze, list_rules, list_packs, install_pack, register_rule
|
|
5
|
+
Testing: test_rules
|
|
6
|
+
CI: ci_scan
|
|
7
|
+
Feedback: rule_stats, rule_feedback
|
|
8
|
+
Community: search_community_packs, install_community_pack, validate_pack_tool
|
|
9
|
+
Import: import_rules
|
|
10
|
+
|
|
11
|
+
These expose the pluggable rule engine to the connected coding agent,
|
|
12
|
+
providing rich, pre-filtered, context-laden findings that enable the
|
|
13
|
+
agent to do LLMCheck-quality validation natively.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import threading
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from attocode_intel._shared import _get_project_dir, mcp
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from attocode_intel.rules.registry import RuleRegistry
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Lazy singleton for the rule registry
|
|
31
|
+
_registry: RuleRegistry | None = None
|
|
32
|
+
_registry_loaded = False
|
|
33
|
+
_registry_lock = threading.Lock()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_registry() -> RuleRegistry:
|
|
37
|
+
"""Get or create the global rule registry (lazy singleton)."""
|
|
38
|
+
from attocode_intel.request_context import current_request, scoped_store
|
|
39
|
+
if current_request.get() is not None:
|
|
40
|
+
return scoped_store("rules", _build_scoped_registry)
|
|
41
|
+
global _registry, _registry_loaded
|
|
42
|
+
# Read-only fast path (safe: both reads are atomic in CPython;
|
|
43
|
+
# the lock below re-checks to avoid races with install_pack resets)
|
|
44
|
+
reg = _registry
|
|
45
|
+
if reg is not None and _registry_loaded:
|
|
46
|
+
return reg
|
|
47
|
+
with _registry_lock:
|
|
48
|
+
if _registry is not None and _registry_loaded:
|
|
49
|
+
return _registry
|
|
50
|
+
|
|
51
|
+
from attocode_intel.rules.registry import RuleRegistry
|
|
52
|
+
from attocode_intel.rules.loader import load_builtin_rules, load_user_rules
|
|
53
|
+
from attocode_intel.rules.packs.pack_loader import load_all_packs
|
|
54
|
+
from attocode_intel.rules.plugins.plugin_loader import load_all_plugins
|
|
55
|
+
from attocode_intel.rules.hygiene import apply_persistent_disable
|
|
56
|
+
from attocode_intel.rules.profiling import FeedbackStore
|
|
57
|
+
|
|
58
|
+
reg = RuleRegistry()
|
|
59
|
+
project_dir = _get_project_dir()
|
|
60
|
+
|
|
61
|
+
# Load builtins
|
|
62
|
+
builtins = load_builtin_rules()
|
|
63
|
+
reg.register_many(builtins)
|
|
64
|
+
|
|
65
|
+
# Load language packs (builtin + user packs from .attocode/packs/)
|
|
66
|
+
pack_manifests, pack_rules = load_all_packs(project_dir)
|
|
67
|
+
reg.register_many(pack_rules)
|
|
68
|
+
|
|
69
|
+
# Load user plugins (from .attocode/plugins/)
|
|
70
|
+
plugin_count = 0
|
|
71
|
+
if project_dir:
|
|
72
|
+
plugin_manifests, plugin_rules = load_all_plugins(project_dir)
|
|
73
|
+
reg.register_many(plugin_rules)
|
|
74
|
+
plugin_count = len(plugin_rules)
|
|
75
|
+
|
|
76
|
+
# Load user rules (from .attocode/rules/)
|
|
77
|
+
user_count = 0
|
|
78
|
+
if project_dir:
|
|
79
|
+
user_rules = load_user_rules(project_dir)
|
|
80
|
+
reg.register_many(user_rules)
|
|
81
|
+
user_count = len(user_rules)
|
|
82
|
+
|
|
83
|
+
# Re-apply previously persisted hygiene auto-disable decisions so
|
|
84
|
+
# dead/noisy rules pruned in earlier sessions stay disabled.
|
|
85
|
+
hygiene_disabled = 0
|
|
86
|
+
if project_dir:
|
|
87
|
+
hygiene_disabled = apply_persistent_disable(reg, FeedbackStore(project_dir))
|
|
88
|
+
|
|
89
|
+
# Assign pointer LAST — fully populated before visible
|
|
90
|
+
_registry = reg
|
|
91
|
+
_registry_loaded = True
|
|
92
|
+
|
|
93
|
+
logger.info(
|
|
94
|
+
"Rule registry: %d rules (%d builtin, %d from %d packs, %d plugins, %d user); "
|
|
95
|
+
"%d hygiene-disabled",
|
|
96
|
+
reg.count,
|
|
97
|
+
len(builtins),
|
|
98
|
+
len(pack_rules),
|
|
99
|
+
len(pack_manifests),
|
|
100
|
+
plugin_count,
|
|
101
|
+
user_count,
|
|
102
|
+
hygiene_disabled,
|
|
103
|
+
)
|
|
104
|
+
return _registry
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _collect_files(
|
|
108
|
+
files: list[str] | None,
|
|
109
|
+
path: str,
|
|
110
|
+
project_dir: str,
|
|
111
|
+
) -> list[str]:
|
|
112
|
+
"""Resolve file list from explicit files or path glob.
|
|
113
|
+
|
|
114
|
+
All resolved paths are checked for containment within project_dir
|
|
115
|
+
to prevent path traversal attacks via the HTTP API.
|
|
116
|
+
"""
|
|
117
|
+
root = os.path.realpath(project_dir)
|
|
118
|
+
|
|
119
|
+
def _is_within_project(abs_path: str) -> bool:
|
|
120
|
+
real = os.path.realpath(abs_path)
|
|
121
|
+
return real == root or real.startswith(root + os.sep)
|
|
122
|
+
|
|
123
|
+
if files:
|
|
124
|
+
result = []
|
|
125
|
+
for f in files:
|
|
126
|
+
abs_f = os.path.realpath(
|
|
127
|
+
f if os.path.isabs(f) else os.path.join(project_dir, f)
|
|
128
|
+
)
|
|
129
|
+
if not _is_within_project(abs_f):
|
|
130
|
+
logger.warning("Skipping path outside project root: %s", f)
|
|
131
|
+
continue
|
|
132
|
+
if os.path.isfile(abs_f):
|
|
133
|
+
result.append(abs_f)
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
# Walk path (or whole project)
|
|
137
|
+
scan_dir = os.path.realpath(
|
|
138
|
+
os.path.join(project_dir, path) if path else project_dir
|
|
139
|
+
)
|
|
140
|
+
if not _is_within_project(scan_dir):
|
|
141
|
+
logger.warning("Scan path escapes project root: %s", path)
|
|
142
|
+
return []
|
|
143
|
+
if not os.path.isdir(scan_dir):
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
from attocode_intel.rules.executor import _EXT_LANG
|
|
147
|
+
|
|
148
|
+
_SKIP_DIRS = frozenset({
|
|
149
|
+
".git", "node_modules", "__pycache__", ".venv", "venv",
|
|
150
|
+
".tox", "dist", "build", ".next", ".nuxt", ".attocode",
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
result = []
|
|
154
|
+
for dirpath, dirnames, filenames in os.walk(scan_dir):
|
|
155
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
|
156
|
+
for fname in filenames:
|
|
157
|
+
ext = os.path.splitext(fname)[1].lower()
|
|
158
|
+
if ext in _EXT_LANG:
|
|
159
|
+
result.append(os.path.join(dirpath, fname))
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _analyze_impl(
|
|
164
|
+
files: list[str] | None = None,
|
|
165
|
+
path: str = "",
|
|
166
|
+
language: str = "",
|
|
167
|
+
category: str = "",
|
|
168
|
+
severity: str = "",
|
|
169
|
+
pack: str = "",
|
|
170
|
+
min_confidence: float = 0.5,
|
|
171
|
+
max_findings: int = 50,
|
|
172
|
+
project_dir: str = "",
|
|
173
|
+
) -> str:
|
|
174
|
+
"""Internal implementation — accepts explicit project_dir for service layer."""
|
|
175
|
+
if not project_dir:
|
|
176
|
+
project_dir = _get_project_dir()
|
|
177
|
+
reg = _get_registry()
|
|
178
|
+
|
|
179
|
+
# Query applicable rules
|
|
180
|
+
from attocode_intel import confidence
|
|
181
|
+
from attocode_intel.confidence.settings import workspace
|
|
182
|
+
|
|
183
|
+
with workspace(project_dir):
|
|
184
|
+
threshold = confidence.rule_threshold(min_confidence)
|
|
185
|
+
rules = reg.query(
|
|
186
|
+
language=language,
|
|
187
|
+
category=category,
|
|
188
|
+
severity=severity,
|
|
189
|
+
pack=pack,
|
|
190
|
+
min_confidence=threshold,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if not rules:
|
|
194
|
+
return "No rules match the given filters."
|
|
195
|
+
|
|
196
|
+
# Collect files
|
|
197
|
+
file_list = _collect_files(files, path, project_dir)
|
|
198
|
+
if not file_list:
|
|
199
|
+
return "No scannable source files found."
|
|
200
|
+
|
|
201
|
+
# Execute
|
|
202
|
+
from attocode_intel.rules.executor import execute_rules
|
|
203
|
+
from attocode_intel.rules.enricher import enrich_findings
|
|
204
|
+
from attocode_intel.rules.filters.pipeline import run_pipeline
|
|
205
|
+
from attocode_intel.rules.formatter import format_findings
|
|
206
|
+
|
|
207
|
+
findings = execute_rules(file_list, rules, project_dir=project_dir)
|
|
208
|
+
|
|
209
|
+
# Persist per-rule scan/match counters so the hygiene tool can
|
|
210
|
+
# identify dead rules across sessions. Counts pre-filter findings —
|
|
211
|
+
# post-filter would under-count dead rules whose only matches got
|
|
212
|
+
# dropped by the test-file/confidence pipeline.
|
|
213
|
+
if project_dir:
|
|
214
|
+
from attocode_intel.rules.profiling import FeedbackStore
|
|
215
|
+
|
|
216
|
+
match_counts: dict[str, int] = {}
|
|
217
|
+
for f in findings:
|
|
218
|
+
match_counts[f.rule_id] = match_counts.get(f.rule_id, 0) + 1
|
|
219
|
+
FeedbackStore(project_dir).record_session(
|
|
220
|
+
rule_ids=[r.qualified_id for r in rules],
|
|
221
|
+
matches=match_counts,
|
|
222
|
+
files_scanned=len(file_list),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Pre-filter pipeline (dedup, test-file adjustment, confidence threshold)
|
|
226
|
+
findings = run_pipeline(findings, min_confidence=min_confidence, project_dir=project_dir)
|
|
227
|
+
|
|
228
|
+
# Enrich with context
|
|
229
|
+
enrich_findings(findings, project_dir=project_dir)
|
|
230
|
+
|
|
231
|
+
# Format for agent
|
|
232
|
+
text = format_findings(findings, max_findings=max_findings)
|
|
233
|
+
return text + ("\n\n" + confidence.summary() if confidence.summary() else "")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@mcp.tool()
|
|
237
|
+
def analyze(
|
|
238
|
+
files: list[str] | None = None,
|
|
239
|
+
path: str = "",
|
|
240
|
+
language: str = "",
|
|
241
|
+
category: str = "",
|
|
242
|
+
severity: str = "",
|
|
243
|
+
pack: str = "",
|
|
244
|
+
min_confidence: float = 0.5,
|
|
245
|
+
max_findings: int = 50,
|
|
246
|
+
) -> str:
|
|
247
|
+
"""Run rule-based analysis on source files with rich context for reasoning.
|
|
248
|
+
|
|
249
|
+
Returns structured findings with: code context (10 lines before/after),
|
|
250
|
+
antipattern explanations, few-shot examples, suggested fixes, confidence
|
|
251
|
+
scores, and CWE references. Designed to give you everything needed to
|
|
252
|
+
triage, fix, and explain issues.
|
|
253
|
+
|
|
254
|
+
Language-specific rules require installing a pack first via install_pack().
|
|
255
|
+
Builtin security rules (secrets, CWE patterns) are always active.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
files: Specific file paths to analyze (relative or absolute).
|
|
259
|
+
If empty, scans all source files under path.
|
|
260
|
+
path: Directory to scan (relative to project root). Default: entire project.
|
|
261
|
+
language: Filter rules to this language (e.g. "python", "go").
|
|
262
|
+
category: Filter by category: correctness, suspicious, complexity,
|
|
263
|
+
performance, style, security, deprecated.
|
|
264
|
+
severity: Filter by minimum severity: critical, high, medium, low, info.
|
|
265
|
+
pack: Filter rules from a specific language pack.
|
|
266
|
+
min_confidence: Minimum confidence threshold (0.0-1.0). Default 0.5.
|
|
267
|
+
max_findings: Maximum findings to return. Default 50.
|
|
268
|
+
"""
|
|
269
|
+
return _analyze_impl(
|
|
270
|
+
files=files, path=path, language=language, category=category,
|
|
271
|
+
severity=severity, pack=pack, min_confidence=min_confidence,
|
|
272
|
+
max_findings=max_findings,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@mcp.tool()
|
|
277
|
+
def list_rules(
|
|
278
|
+
language: str = "",
|
|
279
|
+
category: str = "",
|
|
280
|
+
severity: str = "",
|
|
281
|
+
pack: str = "",
|
|
282
|
+
verbose: bool = False,
|
|
283
|
+
) -> str:
|
|
284
|
+
"""List available analysis rules with optional filters.
|
|
285
|
+
|
|
286
|
+
Shows all registered rules grouped by category, with severity tags.
|
|
287
|
+
Use verbose=True to include rule descriptions.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
language: Filter to rules for this language.
|
|
291
|
+
category: Filter by category (correctness, security, performance, etc.).
|
|
292
|
+
severity: Filter by severity level.
|
|
293
|
+
pack: Filter to rules from a specific language pack.
|
|
294
|
+
verbose: Include rule descriptions in output.
|
|
295
|
+
"""
|
|
296
|
+
reg = _get_registry()
|
|
297
|
+
rules = reg.query(
|
|
298
|
+
language=language,
|
|
299
|
+
category=category,
|
|
300
|
+
severity=severity,
|
|
301
|
+
pack=pack,
|
|
302
|
+
enabled_only=False,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
from attocode_intel.rules.formatter import format_rules_list
|
|
306
|
+
output = format_rules_list(rules, verbose=verbose)
|
|
307
|
+
|
|
308
|
+
# Append stats
|
|
309
|
+
stats = reg.stats()
|
|
310
|
+
output += f"\n---\nTotal: {stats['total']} rules | "
|
|
311
|
+
output += f"Enabled: {stats['enabled']} | "
|
|
312
|
+
output += f"Languages: {stats['languages']} | "
|
|
313
|
+
output += f"Packs: {stats['packs']}"
|
|
314
|
+
|
|
315
|
+
return output
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@mcp.tool()
|
|
319
|
+
def list_packs() -> str:
|
|
320
|
+
"""List installed and available example language packs.
|
|
321
|
+
|
|
322
|
+
Shows installed packs (active, from .attocode/packs/) and available
|
|
323
|
+
example packs that can be installed with install_pack().
|
|
324
|
+
|
|
325
|
+
Language packs provide language-specific rules for performance
|
|
326
|
+
antipatterns, correctness bugs, security issues, and idiomatic style.
|
|
327
|
+
They are NOT auto-loaded — install the ones relevant to your project.
|
|
328
|
+
"""
|
|
329
|
+
from attocode_intel.rules.packs.pack_loader import list_example_packs
|
|
330
|
+
from attocode_intel.rules.formatter import format_packs_list
|
|
331
|
+
|
|
332
|
+
reg = _get_registry()
|
|
333
|
+
pack_names = reg.packs()
|
|
334
|
+
|
|
335
|
+
lines: list[str] = []
|
|
336
|
+
|
|
337
|
+
# Installed packs
|
|
338
|
+
if pack_names:
|
|
339
|
+
packs = []
|
|
340
|
+
for name in pack_names:
|
|
341
|
+
pack_rules = reg.query(pack=name, enabled_only=False)
|
|
342
|
+
packs.append({
|
|
343
|
+
"name": name,
|
|
344
|
+
"languages": sorted(set(
|
|
345
|
+
lang for r in pack_rules for lang in r.languages
|
|
346
|
+
)),
|
|
347
|
+
"rules_count": len(pack_rules),
|
|
348
|
+
"description": "",
|
|
349
|
+
})
|
|
350
|
+
lines.append(format_packs_list(packs))
|
|
351
|
+
else:
|
|
352
|
+
lines.append("## Installed Packs: none\n")
|
|
353
|
+
lines.append("No packs installed. Use `install_pack(name)` to activate one.\n")
|
|
354
|
+
|
|
355
|
+
# Available examples
|
|
356
|
+
examples = list_example_packs()
|
|
357
|
+
if examples:
|
|
358
|
+
lines.append("## Available Example Packs\n")
|
|
359
|
+
lines.append("These ship with attocode but are NOT auto-loaded.")
|
|
360
|
+
lines.append("Install with `install_pack(name)` to activate.\n")
|
|
361
|
+
for ex in examples:
|
|
362
|
+
langs = ", ".join(ex.languages)
|
|
363
|
+
lines.append(f"- **{ex.name}** ({langs}) — {ex.description}")
|
|
364
|
+
lines.append("")
|
|
365
|
+
|
|
366
|
+
return "\n".join(lines)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@mcp.tool()
|
|
370
|
+
def install_pack(name: str) -> str:
|
|
371
|
+
"""Install an example language pack into this project.
|
|
372
|
+
|
|
373
|
+
Copies the pack from attocode's shipped examples into
|
|
374
|
+
.attocode/packs/<name>/ where it becomes active. You can then
|
|
375
|
+
customize the rules in .attocode/packs/<name>/rules/*.yaml.
|
|
376
|
+
|
|
377
|
+
Available packs: go, python, typescript, rust, java.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
name: Pack name to install (e.g. "go", "python").
|
|
381
|
+
"""
|
|
382
|
+
from attocode_intel.rules.packs.pack_loader import install_pack as _install
|
|
383
|
+
|
|
384
|
+
project_dir = _get_project_dir()
|
|
385
|
+
result = _install(name, project_dir)
|
|
386
|
+
|
|
387
|
+
# Reload registry to pick up new pack
|
|
388
|
+
global _registry, _registry_loaded
|
|
389
|
+
with _registry_lock:
|
|
390
|
+
_registry = None
|
|
391
|
+
_registry_loaded = False
|
|
392
|
+
|
|
393
|
+
return result
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@mcp.tool()
|
|
397
|
+
def register_rule(yaml_content: str) -> str:
|
|
398
|
+
"""Register a custom analysis rule at runtime from YAML.
|
|
399
|
+
|
|
400
|
+
The rule is added to the registry immediately and will be used
|
|
401
|
+
by subsequent analyze() calls. Rules persist for the session.
|
|
402
|
+
|
|
403
|
+
For permanent rules, add YAML files to .attocode/rules/.
|
|
404
|
+
|
|
405
|
+
Format:
|
|
406
|
+
id: my-rule-name
|
|
407
|
+
pattern: "regex_pattern"
|
|
408
|
+
message: "What was detected"
|
|
409
|
+
severity: high
|
|
410
|
+
category: performance
|
|
411
|
+
languages: [python]
|
|
412
|
+
recommendation: "How to fix"
|
|
413
|
+
explanation: "Why this matters"
|
|
414
|
+
examples:
|
|
415
|
+
- bad: "slow_code()"
|
|
416
|
+
good: "fast_code()"
|
|
417
|
+
explanation: "Why good is better"
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
yaml_content: YAML rule definition (single rule or list).
|
|
421
|
+
"""
|
|
422
|
+
try:
|
|
423
|
+
import yaml # type: ignore[import-untyped]
|
|
424
|
+
except ImportError:
|
|
425
|
+
return "Error: PyYAML not installed. Run: pip install pyyaml"
|
|
426
|
+
|
|
427
|
+
from attocode_intel.rules.loader import _parse_yaml_rule
|
|
428
|
+
from attocode_intel.rules.model import RuleSource
|
|
429
|
+
|
|
430
|
+
data = yaml.safe_load(yaml_content)
|
|
431
|
+
if data is None:
|
|
432
|
+
return "Error: Empty YAML content."
|
|
433
|
+
|
|
434
|
+
items = [data] if isinstance(data, dict) else data if isinstance(data, list) else []
|
|
435
|
+
if not items:
|
|
436
|
+
return "Error: YAML must be a dict (single rule) or list (multiple rules)."
|
|
437
|
+
|
|
438
|
+
reg = _get_registry()
|
|
439
|
+
registered = 0
|
|
440
|
+
errors: list[str] = []
|
|
441
|
+
|
|
442
|
+
for i, item in enumerate(items):
|
|
443
|
+
if not isinstance(item, dict):
|
|
444
|
+
errors.append(f"Rule at index {i}: expected dict, got {type(item).__name__}")
|
|
445
|
+
continue
|
|
446
|
+
rule = _parse_yaml_rule(item, source=RuleSource.USER, origin=f"runtime[{i}]")
|
|
447
|
+
if rule is None:
|
|
448
|
+
errors.append(f"Rule at index {i}: invalid (check id, pattern, message, severity)")
|
|
449
|
+
else:
|
|
450
|
+
reg.register(rule)
|
|
451
|
+
registered += 1
|
|
452
|
+
|
|
453
|
+
parts = [f"Registered {registered} rule(s)."]
|
|
454
|
+
if errors:
|
|
455
|
+
parts.append("Errors:\n" + "\n".join(f" - {e}" for e in errors))
|
|
456
|
+
parts.append(f"Registry now has {reg.count} rules total.")
|
|
457
|
+
|
|
458
|
+
return "\n".join(parts)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@mcp.tool()
|
|
462
|
+
def test_rules(
|
|
463
|
+
fixtures_dir: str = "",
|
|
464
|
+
rule_id: str = "",
|
|
465
|
+
) -> str:
|
|
466
|
+
"""Run rule tests against fixture files with expect/ok/todoruleid annotations.
|
|
467
|
+
|
|
468
|
+
Fixture files should contain inline annotations:
|
|
469
|
+
# expect: rule-id — rule MUST fire on this line
|
|
470
|
+
# ok: rule-id — rule must NOT fire on this line (false-positive guard)
|
|
471
|
+
# todoruleid: rule-id — rule SHOULD fire but is known-missing
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
fixtures_dir: Directory containing annotated test files.
|
|
475
|
+
Default: .attocode/test_fixtures/ or tests/fixtures/rule_violations/.
|
|
476
|
+
rule_id: Optional — only test this specific rule.
|
|
477
|
+
"""
|
|
478
|
+
from attocode_intel.rules.testing import (
|
|
479
|
+
RuleTestRunner,
|
|
480
|
+
format_test_report,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
reg = _get_registry()
|
|
484
|
+
project_dir = _get_project_dir()
|
|
485
|
+
|
|
486
|
+
# Resolve fixtures directory
|
|
487
|
+
if not fixtures_dir:
|
|
488
|
+
import os
|
|
489
|
+
for candidate in [
|
|
490
|
+
os.path.join(project_dir, ".attocode", "test_fixtures"),
|
|
491
|
+
os.path.join(project_dir, "tests", "fixtures", "rule_violations"),
|
|
492
|
+
]:
|
|
493
|
+
if os.path.isdir(candidate):
|
|
494
|
+
fixtures_dir = candidate
|
|
495
|
+
break
|
|
496
|
+
if not fixtures_dir:
|
|
497
|
+
return "No test fixtures directory found. Provide fixtures_dir or create .attocode/test_fixtures/."
|
|
498
|
+
|
|
499
|
+
if rule_id:
|
|
500
|
+
rule = reg.get(rule_id)
|
|
501
|
+
if rule is None:
|
|
502
|
+
return f"Rule '{rule_id}' not found in registry."
|
|
503
|
+
rules = [rule]
|
|
504
|
+
else:
|
|
505
|
+
rules = reg.all_rules(enabled_only=False)
|
|
506
|
+
|
|
507
|
+
runner = RuleTestRunner(rules, project_dir=project_dir)
|
|
508
|
+
suite = runner.run_test_suite(fixtures_dir)
|
|
509
|
+
|
|
510
|
+
if not suite.file_results:
|
|
511
|
+
return f"No annotated test files found in {fixtures_dir}."
|
|
512
|
+
|
|
513
|
+
return format_test_report(suite)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
@mcp.tool()
|
|
517
|
+
def ci_scan(
|
|
518
|
+
path: str = "",
|
|
519
|
+
language: str = "",
|
|
520
|
+
category: str = "",
|
|
521
|
+
fail_on: str = "high",
|
|
522
|
+
diff_only: bool = False,
|
|
523
|
+
output_format: str = "summary",
|
|
524
|
+
) -> str:
|
|
525
|
+
"""Run CI-style rule scan with exit-code semantics and SARIF output.
|
|
526
|
+
|
|
527
|
+
Designed for CI/CD pipelines: scans source files, applies rules,
|
|
528
|
+
and reports findings with pass/fail status. Supports diff-only mode
|
|
529
|
+
to scan only changed lines since a baseline git ref.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
path: Directory to scan (relative to project root). Default: entire project.
|
|
533
|
+
language: Filter rules to this language.
|
|
534
|
+
category: Filter by category (correctness, security, etc.).
|
|
535
|
+
fail_on: Minimum severity to trigger failure: critical, high, medium, low.
|
|
536
|
+
diff_only: Only report findings on lines changed since baseline.
|
|
537
|
+
output_format: Output format — "summary" (human-readable),
|
|
538
|
+
"sarif" (SARIF JSON), "annotations" (GitHub Actions format).
|
|
539
|
+
"""
|
|
540
|
+
from attocode_intel.rules.ci import (
|
|
541
|
+
CIConfig, CIRunner, format_ci_summary, format_github_annotations,
|
|
542
|
+
)
|
|
543
|
+
from attocode_intel.rules.sarif import findings_to_sarif, sarif_to_json
|
|
544
|
+
from attocode_intel.rules.model import RuleSeverity
|
|
545
|
+
|
|
546
|
+
project_dir = _get_project_dir()
|
|
547
|
+
|
|
548
|
+
config = CIConfig()
|
|
549
|
+
sev_str = fail_on.lower()
|
|
550
|
+
if sev_str in {s.value for s in RuleSeverity}:
|
|
551
|
+
config.fail_on = RuleSeverity(sev_str)
|
|
552
|
+
|
|
553
|
+
runner = CIRunner(project_dir, config=config)
|
|
554
|
+
result = runner.run(path=path, language=language, category=category, diff_only=diff_only)
|
|
555
|
+
|
|
556
|
+
if output_format == "sarif":
|
|
557
|
+
sarif = findings_to_sarif(result.findings)
|
|
558
|
+
return sarif_to_json(sarif)
|
|
559
|
+
elif output_format == "annotations":
|
|
560
|
+
annotations = format_github_annotations(result.findings)
|
|
561
|
+
status = "PASS" if result.passed else "FAIL"
|
|
562
|
+
return f"Status: {status}\n{annotations}"
|
|
563
|
+
else:
|
|
564
|
+
return format_ci_summary(result)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@mcp.tool()
|
|
568
|
+
def rule_stats(rule_id: str = "") -> str:
|
|
569
|
+
"""Show profiling stats and confidence calibration for rules.
|
|
570
|
+
|
|
571
|
+
Shows execution time, match count, true/false positive feedback,
|
|
572
|
+
and calibrated confidence from persistent feedback data.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
rule_id: Specific rule ID, or empty for all rules with feedback.
|
|
576
|
+
"""
|
|
577
|
+
from attocode_intel.rules.profiling import (
|
|
578
|
+
FeedbackStore, RuleStats, format_rule_stats,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
project_dir = _get_project_dir()
|
|
582
|
+
store = FeedbackStore(project_dir)
|
|
583
|
+
feedback = store.all_feedback()
|
|
584
|
+
|
|
585
|
+
if not feedback:
|
|
586
|
+
return "No rule feedback recorded yet. Use rule_feedback() to record TP/FP observations."
|
|
587
|
+
|
|
588
|
+
def _as_int(value: object) -> int:
|
|
589
|
+
return int(value) if isinstance(value, (int, float)) else 0
|
|
590
|
+
|
|
591
|
+
# Build stats from feedback data
|
|
592
|
+
stats: dict[str, RuleStats] = {}
|
|
593
|
+
for rid, fb in feedback.items():
|
|
594
|
+
if rule_id and rid != rule_id:
|
|
595
|
+
continue
|
|
596
|
+
s = RuleStats(
|
|
597
|
+
rule_id=rid,
|
|
598
|
+
true_positives=_as_int(fb.get("tp", 0)),
|
|
599
|
+
false_positives=_as_int(fb.get("fp", 0)),
|
|
600
|
+
)
|
|
601
|
+
stats[rid] = s
|
|
602
|
+
|
|
603
|
+
if rule_id and not stats:
|
|
604
|
+
return f"No feedback for rule '{rule_id}'."
|
|
605
|
+
|
|
606
|
+
return format_rule_stats(stats, feedback)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@mcp.tool()
|
|
610
|
+
def rule_feedback(
|
|
611
|
+
rule_id: str,
|
|
612
|
+
is_true_positive: bool,
|
|
613
|
+
finding_line: int = 0,
|
|
614
|
+
) -> str:
|
|
615
|
+
"""Record whether a finding was a true or false positive.
|
|
616
|
+
|
|
617
|
+
This feedback is used to calibrate rule confidence scores over time.
|
|
618
|
+
After 5+ observations, a calibrated confidence replaces the rule's
|
|
619
|
+
default confidence for more accurate triage.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
rule_id: The rule that produced the finding.
|
|
623
|
+
is_true_positive: True if the finding was a real issue, False if it was a false positive.
|
|
624
|
+
finding_line: Optional line number for context (not used in calibration).
|
|
625
|
+
"""
|
|
626
|
+
from attocode_intel.rules.profiling import FeedbackStore
|
|
627
|
+
|
|
628
|
+
project_dir = _get_project_dir()
|
|
629
|
+
store = FeedbackStore(project_dir)
|
|
630
|
+
store.record(rule_id, is_true_positive=is_true_positive)
|
|
631
|
+
|
|
632
|
+
fb = store.get_feedback(rule_id)
|
|
633
|
+
tp, fp = fb.get("tp", 0), fb.get("fp", 0)
|
|
634
|
+
total = tp + fp
|
|
635
|
+
cal = store.get_calibrated_confidence(rule_id)
|
|
636
|
+
|
|
637
|
+
parts = [f"Recorded {'TP' if is_true_positive else 'FP'} for `{rule_id}`."]
|
|
638
|
+
parts.append(f"Total: {tp} TP, {fp} FP ({total} observations)")
|
|
639
|
+
if cal is not None:
|
|
640
|
+
parts.append(f"Calibrated confidence: {cal:.1%}")
|
|
641
|
+
else:
|
|
642
|
+
remaining = 5 - total
|
|
643
|
+
parts.append(f"Need {remaining} more observation(s) for calibration.")
|
|
644
|
+
|
|
645
|
+
return "\n".join(parts)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@mcp.tool()
|
|
649
|
+
def evolve_rules(
|
|
650
|
+
target_rule_ids: list[str],
|
|
651
|
+
fixtures_dir: str = "",
|
|
652
|
+
generations: int = 20,
|
|
653
|
+
population_size: int = 12,
|
|
654
|
+
plateau_gens: int = 5,
|
|
655
|
+
) -> str:
|
|
656
|
+
"""Evolutionary optimisation of one or more rules against a labelled corpus.
|
|
657
|
+
|
|
658
|
+
Uses tournament selection (k=3) + 10% elitism + per-generation
|
|
659
|
+
mutation/crossover. Fitness is precision + recall + speed bonus.
|
|
660
|
+
Stops on ``max_generations`` or when the best composite plateaus
|
|
661
|
+
for ``plateau_gens`` consecutive generations.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
target_rule_ids: Qualified rule IDs to seed the population with.
|
|
665
|
+
Each must be in the active registry.
|
|
666
|
+
fixtures_dir: Directory of inline-annotated source files (same
|
|
667
|
+
format as ``test_rules``). Defaults to
|
|
668
|
+
``.attocode/test_fixtures/`` if empty.
|
|
669
|
+
generations: Maximum generations to run.
|
|
670
|
+
population_size: Total population per generation (incl. elites).
|
|
671
|
+
plateau_gens: Stop early if best composite is flat across this
|
|
672
|
+
many recent generations.
|
|
673
|
+
"""
|
|
674
|
+
from attocode_intel.rules.evolution import EvolutionState, evolve
|
|
675
|
+
|
|
676
|
+
if not target_rule_ids:
|
|
677
|
+
return "Error: provide at least one target_rule_id."
|
|
678
|
+
|
|
679
|
+
project_dir = _get_project_dir()
|
|
680
|
+
if not fixtures_dir:
|
|
681
|
+
candidate = os.path.join(project_dir or ".", ".attocode", "test_fixtures")
|
|
682
|
+
if os.path.isdir(candidate):
|
|
683
|
+
fixtures_dir = candidate
|
|
684
|
+
else:
|
|
685
|
+
return (
|
|
686
|
+
"Error: fixtures_dir not provided and "
|
|
687
|
+
".attocode/test_fixtures/ does not exist."
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
registry = _get_registry()
|
|
691
|
+
seeds: list = []
|
|
692
|
+
missing: list[str] = []
|
|
693
|
+
for qid in target_rule_ids:
|
|
694
|
+
rule = registry.get(qid)
|
|
695
|
+
if rule is None:
|
|
696
|
+
missing.append(qid)
|
|
697
|
+
else:
|
|
698
|
+
seeds.append(rule)
|
|
699
|
+
if missing:
|
|
700
|
+
return f"Error: rule(s) not found: {missing}"
|
|
701
|
+
if not seeds:
|
|
702
|
+
return "Error: no usable seed rules."
|
|
703
|
+
|
|
704
|
+
log_path = (
|
|
705
|
+
os.path.join(project_dir, ".attocode", "evolution_log.jsonl")
|
|
706
|
+
if project_dir else ""
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
state: EvolutionState = evolve(
|
|
710
|
+
seeds,
|
|
711
|
+
fixtures_dir=fixtures_dir,
|
|
712
|
+
project_dir=project_dir,
|
|
713
|
+
max_generations=generations,
|
|
714
|
+
population_size=population_size,
|
|
715
|
+
plateau_gens=plateau_gens,
|
|
716
|
+
log_path=log_path,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
best = state.best_rule
|
|
720
|
+
fit = state.best_fitness
|
|
721
|
+
parts = [
|
|
722
|
+
f"Evolution complete: {state.generations_run} generation(s), "
|
|
723
|
+
f"stopped on {state.stopped_reason}.",
|
|
724
|
+
"",
|
|
725
|
+
f"Best rule: {best.qualified_id}",
|
|
726
|
+
f" pattern: {best.pattern.pattern if best.pattern else '(none)'}",
|
|
727
|
+
f" severity: {best.severity}, confidence: {best.confidence}",
|
|
728
|
+
f" precision: {fit.precision:.3f}, recall: {fit.recall:.3f}, "
|
|
729
|
+
f"f1: {fit.f1:.3f}, speed: {fit.speed_ms:.1f}ms",
|
|
730
|
+
f" composite: {fit.composite:.3f}",
|
|
731
|
+
"",
|
|
732
|
+
f"History (composite per generation): {state.history}",
|
|
733
|
+
]
|
|
734
|
+
if log_path:
|
|
735
|
+
parts.append(f"\nFull audit log: {log_path}")
|
|
736
|
+
return "\n".join(parts)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
@mcp.tool()
|
|
740
|
+
def synthesize_rule(
|
|
741
|
+
positive_samples: list[str],
|
|
742
|
+
negative_samples: list[str] | None = None,
|
|
743
|
+
language: str = "",
|
|
744
|
+
rule_id: str = "synth-rule",
|
|
745
|
+
description: str = "",
|
|
746
|
+
mode: str = "auto",
|
|
747
|
+
) -> str:
|
|
748
|
+
"""Generate a candidate rule from positive / negative code samples.
|
|
749
|
+
|
|
750
|
+
Tries deterministic regex synthesis first (longest common substring +
|
|
751
|
+
word boundaries, validated against negatives). Falls back to an
|
|
752
|
+
LLM-assisted synthesis when no clean anchor exists and ``mode`` is
|
|
753
|
+
``"auto"`` or ``"llm"``. The returned YAML can be pasted into a pack
|
|
754
|
+
file or registered at runtime via ``register_rule``.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
positive_samples: Lines of code the rule MUST match (one per entry).
|
|
758
|
+
negative_samples: Lines of code the rule MUST NOT match. Optional.
|
|
759
|
+
language: Target language tag (e.g. "python", "go").
|
|
760
|
+
rule_id: Suggested ID for the synthesized rule.
|
|
761
|
+
description: What the rule should detect — used by the LLM prompt
|
|
762
|
+
and as the rule's ``message`` field.
|
|
763
|
+
mode: ``"auto"`` (default), ``"regex"``, or ``"llm"``.
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
YAML rule body when synthesis succeeded, otherwise a diagnostic
|
|
767
|
+
report explaining what was tried.
|
|
768
|
+
"""
|
|
769
|
+
from attocode_intel.rules.synthesis import (
|
|
770
|
+
render_rule_yaml,
|
|
771
|
+
synthesize_rule as _synth,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
result = _synth(
|
|
775
|
+
positives=positive_samples,
|
|
776
|
+
negatives=list(negative_samples or []),
|
|
777
|
+
language=language,
|
|
778
|
+
rule_id=rule_id,
|
|
779
|
+
description=description,
|
|
780
|
+
mode=mode,
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
if result.rule is None:
|
|
784
|
+
diag_lines = "\n - ".join(result.diagnostics) if result.diagnostics else "(no diagnostics)"
|
|
785
|
+
return (
|
|
786
|
+
f"Synthesis failed (mode={mode}, method={result.method}):\n - {diag_lines}"
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
yaml_body = render_rule_yaml(result.rule)
|
|
790
|
+
diag_lines = "\n# - ".join(result.diagnostics)
|
|
791
|
+
return f"# synthesised via {result.method}\n# - {diag_lines}\n{yaml_body}"
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@mcp.tool()
|
|
795
|
+
def rule_hygiene(
|
|
796
|
+
apply: bool = False,
|
|
797
|
+
min_scans: int = 10,
|
|
798
|
+
min_samples: int = 10,
|
|
799
|
+
fp_threshold: float = 0.5,
|
|
800
|
+
drift_threshold: float = 0.2,
|
|
801
|
+
) -> str:
|
|
802
|
+
"""Report (and optionally auto-disable) dead, noisy, and drifting rules.
|
|
803
|
+
|
|
804
|
+
Categories:
|
|
805
|
+
dead — rule applied across `min_scans` analyze sessions with 0 matches.
|
|
806
|
+
noisy — at least `min_samples` TP/FP observations and FP rate > `fp_threshold`.
|
|
807
|
+
drift — calibrated confidence diverges from the rule's declared
|
|
808
|
+
confidence by at least `drift_threshold`. Reported only.
|
|
809
|
+
|
|
810
|
+
With `apply=True`, dead and noisy rules are disabled in the in-memory
|
|
811
|
+
registry and the decision persists in `.attocode/rule_feedback.json` so
|
|
812
|
+
they stay disabled in future sessions until manually re-enabled.
|
|
813
|
+
|
|
814
|
+
Args:
|
|
815
|
+
apply: When True, disables dead and noisy rules.
|
|
816
|
+
min_scans: Sessions a rule must have run before it can be marked dead.
|
|
817
|
+
min_samples: TP/FP observations required before a rule can be marked noisy.
|
|
818
|
+
fp_threshold: FP rate above which a rule is noisy.
|
|
819
|
+
drift_threshold: Confidence delta above which drift is reported.
|
|
820
|
+
"""
|
|
821
|
+
from attocode_intel.rules.hygiene import (
|
|
822
|
+
apply_hygiene,
|
|
823
|
+
compute_hygiene,
|
|
824
|
+
format_hygiene_report,
|
|
825
|
+
)
|
|
826
|
+
from attocode_intel.rules.profiling import FeedbackStore
|
|
827
|
+
|
|
828
|
+
project_dir = _get_project_dir()
|
|
829
|
+
if not project_dir:
|
|
830
|
+
return "Error: No project directory detected. Open a project first."
|
|
831
|
+
|
|
832
|
+
store = FeedbackStore(project_dir)
|
|
833
|
+
registry = _get_registry()
|
|
834
|
+
report = compute_hygiene(
|
|
835
|
+
store,
|
|
836
|
+
registry,
|
|
837
|
+
min_scans=min_scans,
|
|
838
|
+
min_samples=min_samples,
|
|
839
|
+
fp_threshold=fp_threshold,
|
|
840
|
+
drift_threshold=drift_threshold,
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
output = format_hygiene_report(report)
|
|
844
|
+
if apply and report.total_actionable:
|
|
845
|
+
disabled = apply_hygiene(report, registry, store)
|
|
846
|
+
output += f"\n\nDisabled {disabled} rule(s) (dead + noisy)."
|
|
847
|
+
return output
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
@mcp.tool()
|
|
851
|
+
def search_community_packs(
|
|
852
|
+
query: str = "",
|
|
853
|
+
language: str = "",
|
|
854
|
+
) -> str:
|
|
855
|
+
"""Search the community rule pack registry.
|
|
856
|
+
|
|
857
|
+
Finds rule packs shared by the community, filterable by keyword
|
|
858
|
+
and language. Install found packs with install_community_pack().
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
query: Search by name, description, or tags.
|
|
862
|
+
language: Filter to packs supporting this language.
|
|
863
|
+
"""
|
|
864
|
+
from attocode_intel.rules.marketplace import search_packs, format_registry_search
|
|
865
|
+
|
|
866
|
+
entries = search_packs(query=query, language=language)
|
|
867
|
+
return format_registry_search(entries)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
@mcp.tool()
|
|
871
|
+
def install_community_pack(
|
|
872
|
+
name: str,
|
|
873
|
+
url: str = "",
|
|
874
|
+
) -> str:
|
|
875
|
+
"""Install a community rule pack from a Git repository.
|
|
876
|
+
|
|
877
|
+
Downloads and validates the pack, then installs to .attocode/packs/.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
name: Pack name (used as directory name).
|
|
881
|
+
url: Git clone URL. If empty, looks up from community registry.
|
|
882
|
+
"""
|
|
883
|
+
from attocode_intel.rules.marketplace import (
|
|
884
|
+
install_remote_pack,
|
|
885
|
+
search_packs,
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
project_dir = _get_project_dir()
|
|
889
|
+
if not project_dir:
|
|
890
|
+
return "Error: No project directory detected. Open a project first."
|
|
891
|
+
|
|
892
|
+
if not url:
|
|
893
|
+
entries = search_packs(query=name)
|
|
894
|
+
matching = [e for e in entries if e.name == name]
|
|
895
|
+
if matching:
|
|
896
|
+
url = matching[0].url
|
|
897
|
+
else:
|
|
898
|
+
return f"Pack '{name}' not found in registry. Provide a URL directly."
|
|
899
|
+
|
|
900
|
+
result = install_remote_pack(url, project_dir, pack_name=name)
|
|
901
|
+
|
|
902
|
+
# Reload registry
|
|
903
|
+
global _registry, _registry_loaded
|
|
904
|
+
with _registry_lock:
|
|
905
|
+
_registry = None
|
|
906
|
+
_registry_loaded = False
|
|
907
|
+
|
|
908
|
+
return result
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
@mcp.tool()
|
|
912
|
+
def validate_pack_tool(pack_path: str = "") -> str:
|
|
913
|
+
"""Validate a rule pack for correctness before publishing.
|
|
914
|
+
|
|
915
|
+
Checks manifest, rule syntax, regex compilation, and inline tests.
|
|
916
|
+
|
|
917
|
+
Args:
|
|
918
|
+
pack_path: Path to pack directory. Default: validates all installed packs.
|
|
919
|
+
"""
|
|
920
|
+
from attocode_intel.rules.marketplace import validate_pack, prepare_pack_for_publish
|
|
921
|
+
|
|
922
|
+
project_dir = _get_project_dir()
|
|
923
|
+
if not project_dir and not pack_path:
|
|
924
|
+
return "Error: No project directory detected. Provide pack_path or open a project."
|
|
925
|
+
|
|
926
|
+
if pack_path:
|
|
927
|
+
import os
|
|
928
|
+
if not os.path.isabs(pack_path):
|
|
929
|
+
pack_path = os.path.join(project_dir, pack_path)
|
|
930
|
+
return prepare_pack_for_publish(pack_path)
|
|
931
|
+
|
|
932
|
+
import os
|
|
933
|
+
packs_dir = os.path.join(project_dir, ".attocode", "packs")
|
|
934
|
+
if not os.path.isdir(packs_dir):
|
|
935
|
+
return "No packs installed (.attocode/packs/ not found)."
|
|
936
|
+
|
|
937
|
+
results: list[str] = []
|
|
938
|
+
for entry in sorted(os.listdir(packs_dir)):
|
|
939
|
+
pack_dir = os.path.join(packs_dir, entry)
|
|
940
|
+
if os.path.isdir(pack_dir):
|
|
941
|
+
errors = validate_pack(pack_dir)
|
|
942
|
+
status = "PASS" if not errors else "FAIL"
|
|
943
|
+
results.append(f"**{entry}**: {status}")
|
|
944
|
+
if errors:
|
|
945
|
+
for e in errors:
|
|
946
|
+
results.append(f" - {e}")
|
|
947
|
+
|
|
948
|
+
return "\n".join(results) if results else "No packs found."
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
@mcp.tool()
|
|
952
|
+
def import_rules(
|
|
953
|
+
source_format: str,
|
|
954
|
+
content: str,
|
|
955
|
+
) -> str:
|
|
956
|
+
"""Import rules from another tool's format into attocode YAML.
|
|
957
|
+
|
|
958
|
+
Converts rules from Semgrep or other formats to attocode YAML.
|
|
959
|
+
Output can be saved to .attocode/rules/ or registered via register_rule().
|
|
960
|
+
|
|
961
|
+
Supported formats: semgrep
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
source_format: Source format — "semgrep".
|
|
965
|
+
content: YAML content in the source format.
|
|
966
|
+
"""
|
|
967
|
+
if source_format.lower() == "semgrep":
|
|
968
|
+
from attocode_intel.rules.importers.semgrep import convert_semgrep_to_yaml
|
|
969
|
+
return convert_semgrep_to_yaml(content)
|
|
970
|
+
else:
|
|
971
|
+
return f"Unsupported format '{source_format}'. Supported: semgrep"
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _build_scoped_registry(project_dir):
|
|
975
|
+
from attocode_intel.rules.registry import RuleRegistry
|
|
976
|
+
from attocode_intel.rules.loader import load_builtin_rules, load_user_rules
|
|
977
|
+
from attocode_intel.rules.packs.pack_loader import load_all_packs
|
|
978
|
+
from attocode_intel.rules.hygiene import apply_persistent_disable
|
|
979
|
+
from attocode_intel.rules.profiling import FeedbackStore
|
|
980
|
+
registry = RuleRegistry()
|
|
981
|
+
registry.register_many(load_builtin_rules())
|
|
982
|
+
_, rules = load_all_packs(project_dir)
|
|
983
|
+
registry.register_many(rules)
|
|
984
|
+
registry.register_many(load_user_rules(project_dir))
|
|
985
|
+
from attocode_intel.request_context import current_request
|
|
986
|
+
request = current_request.get()
|
|
987
|
+
if request and request.source == "local":
|
|
988
|
+
from attocode_intel.rules.plugins.plugin_loader import load_all_plugins
|
|
989
|
+
_, plugin_rules = load_all_plugins(project_dir)
|
|
990
|
+
registry.register_many(plugin_rules)
|
|
991
|
+
apply_persistent_disable(registry, FeedbackStore(project_dir))
|
|
992
|
+
return registry
|