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,375 @@
|
|
|
1
|
+
"""Community rule registry — search, install, validate, and publish packs.
|
|
2
|
+
|
|
3
|
+
Phase 1: GitHub-based registry with a static YAML index.
|
|
4
|
+
Packs are Git repositories or subdirectories that follow the standard
|
|
5
|
+
pack structure (manifest.yaml + rules/*.yaml).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import tempfile
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from attocode_intel.rules.packs.pack_loader import _load_manifest
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Default registry index URL (GitHub raw)
|
|
22
|
+
DEFAULT_REGISTRY_URL = (
|
|
23
|
+
"https://raw.githubusercontent.com/attocode/rules/main/index.yaml"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Registry index
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class RegistryEntry:
|
|
34
|
+
"""A pack listed in the community registry."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
url: str # Git clone URL
|
|
38
|
+
languages: list[str] = field(default_factory=list)
|
|
39
|
+
rules_count: int = 0
|
|
40
|
+
description: str = ""
|
|
41
|
+
author: str = ""
|
|
42
|
+
version: str = "1.0.0"
|
|
43
|
+
tags: list[str] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def fetch_registry_index(
|
|
47
|
+
registry_url: str = DEFAULT_REGISTRY_URL,
|
|
48
|
+
) -> list[RegistryEntry]:
|
|
49
|
+
"""Fetch the community registry index.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of RegistryEntry from the remote index.
|
|
53
|
+
Returns empty list on network errors (graceful degradation).
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
import urllib.request
|
|
57
|
+
import yaml # type: ignore[import-untyped]
|
|
58
|
+
|
|
59
|
+
with urllib.request.urlopen(registry_url, timeout=10) as resp:
|
|
60
|
+
data = yaml.safe_load(resp.read().decode("utf-8"))
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
logger.debug("Failed to fetch registry index: %s", exc)
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
if not isinstance(data, dict) or "packs" not in data:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
entries: list[RegistryEntry] = []
|
|
69
|
+
for item in data["packs"]:
|
|
70
|
+
if not isinstance(item, dict) or "name" not in item:
|
|
71
|
+
continue
|
|
72
|
+
entries.append(RegistryEntry(
|
|
73
|
+
name=str(item["name"]),
|
|
74
|
+
url=str(item.get("url", "")),
|
|
75
|
+
languages=list(item.get("languages", [])),
|
|
76
|
+
rules_count=int(item.get("rules_count", 0)),
|
|
77
|
+
description=str(item.get("description", "")),
|
|
78
|
+
author=str(item.get("author", "")),
|
|
79
|
+
version=str(item.get("version", "1.0.0")),
|
|
80
|
+
tags=list(item.get("tags", [])),
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
return entries
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def search_packs(
|
|
87
|
+
query: str = "",
|
|
88
|
+
*,
|
|
89
|
+
language: str = "",
|
|
90
|
+
registry_url: str = DEFAULT_REGISTRY_URL,
|
|
91
|
+
) -> list[RegistryEntry]:
|
|
92
|
+
"""Search the community registry for packs matching a query.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
query: Free-text search (matches name, description, tags).
|
|
96
|
+
language: Filter to packs supporting this language.
|
|
97
|
+
registry_url: Override registry URL.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Matching registry entries.
|
|
101
|
+
"""
|
|
102
|
+
entries = fetch_registry_index(registry_url)
|
|
103
|
+
if not entries:
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
results: list[RegistryEntry] = []
|
|
107
|
+
query_lower = query.lower()
|
|
108
|
+
|
|
109
|
+
for entry in entries:
|
|
110
|
+
if language and language.lower() not in [l.lower() for l in entry.languages]:
|
|
111
|
+
continue
|
|
112
|
+
if query_lower:
|
|
113
|
+
searchable = f"{entry.name} {entry.description} {' '.join(entry.tags)}".lower()
|
|
114
|
+
if query_lower not in searchable:
|
|
115
|
+
continue
|
|
116
|
+
results.append(entry)
|
|
117
|
+
|
|
118
|
+
return results
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Remote pack installation
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def install_remote_pack(
|
|
127
|
+
pack_url: str,
|
|
128
|
+
project_dir: str,
|
|
129
|
+
*,
|
|
130
|
+
pack_name: str = "",
|
|
131
|
+
) -> str:
|
|
132
|
+
"""Install a pack from a remote Git URL into .attocode/packs/.
|
|
133
|
+
|
|
134
|
+
Clones the repository at depth=1 and validates the pack before installing.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
pack_url: URL to clone or download.
|
|
138
|
+
project_dir: Project root directory.
|
|
139
|
+
pack_name: Override pack name (default: derived from URL).
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Status message.
|
|
143
|
+
"""
|
|
144
|
+
if not pack_name:
|
|
145
|
+
# Derive name from URL: github.com/user/repo -> repo
|
|
146
|
+
pack_name = pack_url.rstrip("/").rsplit("/", 1)[-1]
|
|
147
|
+
pack_name = pack_name.removesuffix(".git")
|
|
148
|
+
|
|
149
|
+
# Sanitize pack_name to prevent path traversal
|
|
150
|
+
pack_name = re.sub(r"[^a-zA-Z0-9_-]", "_", pack_name)
|
|
151
|
+
|
|
152
|
+
dest = Path(project_dir) / ".attocode" / "packs" / pack_name
|
|
153
|
+
if dest.is_dir():
|
|
154
|
+
return f"Pack '{pack_name}' already installed at {dest}. Delete to reinstall."
|
|
155
|
+
|
|
156
|
+
# Try git clone first
|
|
157
|
+
import subprocess
|
|
158
|
+
|
|
159
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
160
|
+
clone_dir = Path(tmpdir) / pack_name
|
|
161
|
+
try:
|
|
162
|
+
subprocess.run(
|
|
163
|
+
["git", "clone", "--depth=1", pack_url, str(clone_dir)],
|
|
164
|
+
capture_output=True, text=True, timeout=60,
|
|
165
|
+
check=True,
|
|
166
|
+
)
|
|
167
|
+
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired) as exc:
|
|
168
|
+
return f"Failed to clone '{pack_url}': {exc}"
|
|
169
|
+
|
|
170
|
+
# Validate before installing
|
|
171
|
+
errors = validate_pack(str(clone_dir))
|
|
172
|
+
if errors:
|
|
173
|
+
return f"Pack validation failed:\n" + "\n".join(f" - {e}" for e in errors)
|
|
174
|
+
|
|
175
|
+
# Copy to destination
|
|
176
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
shutil.copytree(str(clone_dir), str(dest), dirs_exist_ok=True)
|
|
178
|
+
# Remove .git directory
|
|
179
|
+
git_dir = dest / ".git"
|
|
180
|
+
if git_dir.is_dir():
|
|
181
|
+
shutil.rmtree(str(git_dir))
|
|
182
|
+
|
|
183
|
+
manifest = _load_manifest(dest)
|
|
184
|
+
return (
|
|
185
|
+
f"Installed community pack '{pack_name}' to {dest}\n"
|
|
186
|
+
f" Version: {manifest.version if manifest else 'unknown'}\n"
|
|
187
|
+
f" Languages: {', '.join(manifest.languages) if manifest else pack_name}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Pack validation
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def validate_pack(pack_dir: str) -> list[str]:
|
|
197
|
+
"""Validate a pack directory for correctness and completeness.
|
|
198
|
+
|
|
199
|
+
Checks:
|
|
200
|
+
1. manifest.yaml present and parseable
|
|
201
|
+
2. At least one rule file in rules/
|
|
202
|
+
3. All rule IDs are unique
|
|
203
|
+
4. Required fields present (message, severity)
|
|
204
|
+
5. All regex patterns compile
|
|
205
|
+
6. Inline test_cases pass (if present)
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
List of error messages (empty = valid).
|
|
209
|
+
"""
|
|
210
|
+
errors: list[str] = []
|
|
211
|
+
pack_path = Path(pack_dir)
|
|
212
|
+
|
|
213
|
+
# 1. Manifest — check existence first
|
|
214
|
+
manifest_file = pack_path / "manifest.yaml"
|
|
215
|
+
if not manifest_file.is_file():
|
|
216
|
+
errors.append("Missing manifest.yaml (create one with name, version, languages)")
|
|
217
|
+
return errors
|
|
218
|
+
|
|
219
|
+
manifest = _load_manifest(pack_path)
|
|
220
|
+
if manifest is None:
|
|
221
|
+
errors.append("Failed to parse manifest.yaml")
|
|
222
|
+
return errors
|
|
223
|
+
|
|
224
|
+
if not manifest.name:
|
|
225
|
+
errors.append("Manifest missing 'name' field")
|
|
226
|
+
|
|
227
|
+
# 2. Rules directory
|
|
228
|
+
rules_dir = pack_path / "rules"
|
|
229
|
+
if not rules_dir.is_dir():
|
|
230
|
+
errors.append("Missing rules/ directory")
|
|
231
|
+
return errors
|
|
232
|
+
|
|
233
|
+
yaml_files = list(rules_dir.glob("*.yaml"))
|
|
234
|
+
if not yaml_files:
|
|
235
|
+
errors.append("No *.yaml rule files in rules/")
|
|
236
|
+
return errors
|
|
237
|
+
|
|
238
|
+
# 3-6. Load and validate rules
|
|
239
|
+
try:
|
|
240
|
+
import yaml as _yaml # type: ignore[import-untyped]
|
|
241
|
+
except ImportError:
|
|
242
|
+
errors.append("PyYAML not installed — cannot validate rules")
|
|
243
|
+
return errors
|
|
244
|
+
|
|
245
|
+
seen_ids: set[str] = set()
|
|
246
|
+
rule_count = 0
|
|
247
|
+
|
|
248
|
+
for yaml_file in yaml_files:
|
|
249
|
+
try:
|
|
250
|
+
content = yaml_file.read_text(encoding="utf-8")
|
|
251
|
+
data = _yaml.safe_load(content)
|
|
252
|
+
if data is None:
|
|
253
|
+
continue
|
|
254
|
+
items = [data] if isinstance(data, dict) else data if isinstance(data, list) else []
|
|
255
|
+
|
|
256
|
+
for i, item in enumerate(items):
|
|
257
|
+
if not isinstance(item, dict):
|
|
258
|
+
errors.append(f"{yaml_file.name}[{i}]: expected dict, got {type(item).__name__}")
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
rule_id = item.get("id", "")
|
|
262
|
+
if not rule_id:
|
|
263
|
+
errors.append(f"{yaml_file.name}[{i}]: missing 'id' field")
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
# 3. Unique IDs
|
|
267
|
+
if rule_id in seen_ids:
|
|
268
|
+
errors.append(f"{yaml_file.name}[{i}]: duplicate rule ID '{rule_id}'")
|
|
269
|
+
seen_ids.add(rule_id)
|
|
270
|
+
|
|
271
|
+
# 4. Check required fields
|
|
272
|
+
for req in ("pattern", "message", "severity"):
|
|
273
|
+
if req not in item and "patterns" not in item and "pattern-either" not in item:
|
|
274
|
+
if req == "pattern":
|
|
275
|
+
continue # composite patterns don't need top-level pattern
|
|
276
|
+
errors.append(f"{yaml_file.name}[{i}]: missing '{req}' field")
|
|
277
|
+
|
|
278
|
+
# 5. Regex compilation
|
|
279
|
+
pattern_str = item.get("pattern", "")
|
|
280
|
+
if pattern_str:
|
|
281
|
+
try:
|
|
282
|
+
re.compile(pattern_str)
|
|
283
|
+
except re.error as exc:
|
|
284
|
+
errors.append(f"{yaml_file.name}[{i}]: invalid regex '{pattern_str[:40]}': {exc}")
|
|
285
|
+
|
|
286
|
+
# 6. Inline test_cases (per-line matching, consistent with executor)
|
|
287
|
+
test_cases = item.get("test_cases", [])
|
|
288
|
+
if test_cases and pattern_str:
|
|
289
|
+
try:
|
|
290
|
+
compiled = re.compile(pattern_str)
|
|
291
|
+
for j, tc in enumerate(test_cases):
|
|
292
|
+
code = str(tc.get("code", ""))
|
|
293
|
+
should_match = bool(tc.get("should_match", True))
|
|
294
|
+
matched = any(compiled.search(ln) for ln in code.splitlines()) if code else False
|
|
295
|
+
if should_match and not matched:
|
|
296
|
+
errors.append(
|
|
297
|
+
f"{yaml_file.name}[{i}].test_cases[{j}]: "
|
|
298
|
+
f"expected match on '{code[:40]}'"
|
|
299
|
+
)
|
|
300
|
+
elif not should_match and matched:
|
|
301
|
+
errors.append(
|
|
302
|
+
f"{yaml_file.name}[{i}].test_cases[{j}]: "
|
|
303
|
+
f"unexpected match on '{code[:40]}'"
|
|
304
|
+
)
|
|
305
|
+
except re.error:
|
|
306
|
+
pass # already reported above
|
|
307
|
+
|
|
308
|
+
rule_count += 1
|
|
309
|
+
|
|
310
|
+
except Exception as exc:
|
|
311
|
+
errors.append(f"Failed to parse {yaml_file.name}: {exc}")
|
|
312
|
+
|
|
313
|
+
if rule_count == 0:
|
|
314
|
+
errors.append("No valid rules found")
|
|
315
|
+
|
|
316
|
+
return errors
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
# Pack publishing helper
|
|
321
|
+
# ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def prepare_pack_for_publish(pack_dir: str) -> str:
|
|
325
|
+
"""Validate and prepare a pack for community sharing.
|
|
326
|
+
|
|
327
|
+
Returns a status message with validation results and next steps.
|
|
328
|
+
"""
|
|
329
|
+
errors = validate_pack(pack_dir)
|
|
330
|
+
|
|
331
|
+
if errors:
|
|
332
|
+
return (
|
|
333
|
+
"Pack validation FAILED — fix these before publishing:\n"
|
|
334
|
+
+ "\n".join(f" - {e}" for e in errors)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
manifest = _load_manifest(Path(pack_dir))
|
|
338
|
+
from attocode_intel.rules.loader import load_yaml_rules
|
|
339
|
+
|
|
340
|
+
rules_dir = Path(pack_dir) / "rules"
|
|
341
|
+
rules = load_yaml_rules(str(rules_dir))
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
f"Pack '{manifest.name}' is ready for publishing!\n"
|
|
345
|
+
f" Version: {manifest.version}\n"
|
|
346
|
+
f" Languages: {', '.join(manifest.languages)}\n"
|
|
347
|
+
f" Rules: {len(rules)}\n"
|
|
348
|
+
f"\nNext steps:\n"
|
|
349
|
+
f" 1. Push to a public Git repository\n"
|
|
350
|
+
f" 2. Submit a PR to the attocode/rules registry to list your pack\n"
|
|
351
|
+
f" 3. Others can install with: install_community_pack('{manifest.name}')"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
# Formatting
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def format_registry_search(entries: list[RegistryEntry]) -> str:
|
|
361
|
+
"""Format registry search results as markdown."""
|
|
362
|
+
if not entries:
|
|
363
|
+
return "No packs found matching your query."
|
|
364
|
+
|
|
365
|
+
lines = ["## Community Rule Packs\n"]
|
|
366
|
+
for e in entries:
|
|
367
|
+
langs = ", ".join(e.languages) if e.languages else "any"
|
|
368
|
+
tags = " ".join(f"`{t}`" for t in e.tags) if e.tags else ""
|
|
369
|
+
lines.append(
|
|
370
|
+
f"- **{e.name}** v{e.version} ({langs}) — {e.description}\n"
|
|
371
|
+
f" {e.rules_count} rules | by {e.author or 'community'} {tags}\n"
|
|
372
|
+
f" Install: `install_community_pack('{e.name}')`"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Metavariable support for regex-tier rules.
|
|
2
|
+
|
|
3
|
+
Translates Semgrep-style ``$VAR`` placeholders in pattern strings into
|
|
4
|
+
named capture groups, enabling extraction of matched fragments and
|
|
5
|
+
template-based autofix.
|
|
6
|
+
|
|
7
|
+
Metavariable type inference:
|
|
8
|
+
$FUNC, $NAME, $VAR, $IDENT -> identifier (``\\w+``)
|
|
9
|
+
$ARG, $EXPR, $VALUE -> expression within parens (``[^,)]+``)
|
|
10
|
+
$STR -> string literal (``["'][^"']*["']``)
|
|
11
|
+
$NUM -> number (``\\d+(?:\\.\\d+)?``)
|
|
12
|
+
$TYPE -> type annotation (``[\\w.\\[\\]]+``)
|
|
13
|
+
$... -> any (non-capturing ``.*?``)
|
|
14
|
+
All others -> identifier (``\\w+``)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
# Pattern that matches $IDENTIFIER tokens in a rule pattern string
|
|
22
|
+
_METAVAR_RE = re.compile(r"\$(\.\.\.|[A-Z_][A-Z0-9_]*)")
|
|
23
|
+
|
|
24
|
+
# Named groups map — root names (no numeric suffix) to regex fragment
|
|
25
|
+
_TYPE_FRAGMENTS: dict[str, str] = {
|
|
26
|
+
"FUNC": r"\w+",
|
|
27
|
+
"NAME": r"\w+",
|
|
28
|
+
"VAR": r"\w+",
|
|
29
|
+
"IDENT": r"\w+",
|
|
30
|
+
"ARG": r"[^,)]+",
|
|
31
|
+
"EXPR": r".+?",
|
|
32
|
+
"VALUE": r"[^,)]+",
|
|
33
|
+
"STR": r"""["'][^"']*["']""",
|
|
34
|
+
"NUM": r"\d+(?:\.\d+)?",
|
|
35
|
+
"TYPE": r"[\w.\[\]]+",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Default regex for any $X not in _TYPE_FRAGMENTS
|
|
39
|
+
_DEFAULT_FRAGMENT = r"\w+"
|
|
40
|
+
|
|
41
|
+
# Comparison operators for metavariable-comparison constraints
|
|
42
|
+
_CMP_OPS = {
|
|
43
|
+
">": float.__gt__, ">=": float.__ge__,
|
|
44
|
+
"<": float.__lt__, "<=": float.__le__,
|
|
45
|
+
"==": float.__eq__, "!=": float.__ne__,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _infer_fragment(name: str) -> str:
|
|
50
|
+
"""Infer the regex fragment for a metavariable name.
|
|
51
|
+
|
|
52
|
+
Strips trailing digits to find the root type hint:
|
|
53
|
+
``$FUNC2`` -> root ``FUNC`` -> ``\\w+``.
|
|
54
|
+
"""
|
|
55
|
+
root = name.rstrip("0123456789")
|
|
56
|
+
return _TYPE_FRAGMENTS.get(root, _DEFAULT_FRAGMENT)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def has_metavars(pattern_str: str) -> bool:
|
|
60
|
+
"""Return True if *pattern_str* contains any ``$VAR`` metavariable tokens."""
|
|
61
|
+
return bool(_METAVAR_RE.search(pattern_str))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def compile_metavar_pattern(
|
|
65
|
+
pattern_str: str,
|
|
66
|
+
) -> tuple[re.Pattern[str], list[str]]:
|
|
67
|
+
"""Compile a pattern string with ``$VAR`` metavariables into a regex.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
(compiled_regex, metavar_names) where *metavar_names* lists the
|
|
71
|
+
metavariable names in order of appearance (``"..."`` for ellipsis).
|
|
72
|
+
"""
|
|
73
|
+
metavar_names: list[str] = []
|
|
74
|
+
seen: set[str] = set()
|
|
75
|
+
pos = 0
|
|
76
|
+
parts: list[str] = []
|
|
77
|
+
|
|
78
|
+
for m in _METAVAR_RE.finditer(pattern_str):
|
|
79
|
+
# Escape the literal text between the previous match end and this match start
|
|
80
|
+
parts.append(re.escape(pattern_str[pos:m.start()]))
|
|
81
|
+
pos = m.end()
|
|
82
|
+
|
|
83
|
+
token = m.group(1) # e.g. "FUNC", "...", "ARG2"
|
|
84
|
+
|
|
85
|
+
if token == "...":
|
|
86
|
+
parts.append(r".*?")
|
|
87
|
+
metavar_names.append("...")
|
|
88
|
+
elif token in seen:
|
|
89
|
+
# Back-reference — must match the same text as the first occurrence
|
|
90
|
+
parts.append(rf"(?P={token})")
|
|
91
|
+
metavar_names.append(token)
|
|
92
|
+
else:
|
|
93
|
+
fragment = _infer_fragment(token)
|
|
94
|
+
parts.append(rf"(?P<{token}>{fragment})")
|
|
95
|
+
metavar_names.append(token)
|
|
96
|
+
seen.add(token)
|
|
97
|
+
|
|
98
|
+
# Trailing literal after the last metavar
|
|
99
|
+
parts.append(re.escape(pattern_str[pos:]))
|
|
100
|
+
|
|
101
|
+
combined = "".join(parts)
|
|
102
|
+
return re.compile(combined), metavar_names
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def interpolate_message(template: str, captures: dict[str, str]) -> str:
|
|
106
|
+
"""Substitute ``$VAR`` references in a message template with captured values."""
|
|
107
|
+
def _replace(m: re.Match[str]) -> str:
|
|
108
|
+
token = m.group(1)
|
|
109
|
+
if token == "...":
|
|
110
|
+
return "$..."
|
|
111
|
+
return captures.get(token, m.group(0))
|
|
112
|
+
|
|
113
|
+
return _METAVAR_RE.sub(_replace, template)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def apply_metavar_fix(
|
|
117
|
+
search_template: str,
|
|
118
|
+
replace_template: str,
|
|
119
|
+
captures: dict[str, str],
|
|
120
|
+
) -> tuple[str, str]:
|
|
121
|
+
"""Resolve ``$VAR`` references in autofix search/replace templates.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
(concrete_search, concrete_replace) with metavariables substituted.
|
|
125
|
+
"""
|
|
126
|
+
return (
|
|
127
|
+
interpolate_message(search_template, captures),
|
|
128
|
+
interpolate_message(replace_template, captures),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Metavariable constraints (post-match filters)
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def check_metavar_constraints(
|
|
138
|
+
captures: dict[str, str],
|
|
139
|
+
metavar_regex: dict[str, str] | None = None,
|
|
140
|
+
metavar_comparison: dict[str, str] | None = None,
|
|
141
|
+
) -> bool:
|
|
142
|
+
"""Evaluate metavariable constraints against captured values.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
captures: Captured metavar values from a regex match.
|
|
146
|
+
metavar_regex: Map of ``$VAR`` name to regex that the captured
|
|
147
|
+
value must match (e.g. ``{"FUNC": "^(query|execute)$"}``).
|
|
148
|
+
metavar_comparison: Map of ``$VAR`` name to comparison expression
|
|
149
|
+
(e.g. ``{"NUM": "> 1000"}``). Only numeric comparisons supported.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if all constraints pass (or no constraints given).
|
|
153
|
+
"""
|
|
154
|
+
if metavar_regex:
|
|
155
|
+
for var, regex_str in metavar_regex.items():
|
|
156
|
+
name = var.lstrip("$")
|
|
157
|
+
val = captures.get(name, "")
|
|
158
|
+
if not re.search(regex_str, val):
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
if metavar_comparison:
|
|
162
|
+
for var, expr in metavar_comparison.items():
|
|
163
|
+
name = var.lstrip("$")
|
|
164
|
+
val = captures.get(name, "")
|
|
165
|
+
try:
|
|
166
|
+
num = float(val)
|
|
167
|
+
except (ValueError, TypeError):
|
|
168
|
+
return False
|
|
169
|
+
# Parse operator + value from expr like "> 1000" or "<= 5"
|
|
170
|
+
cmp_match = re.match(r"([<>!=]+)\s*(-?\d+(?:\.\d+)?)", expr.strip())
|
|
171
|
+
if not cmp_match:
|
|
172
|
+
return False
|
|
173
|
+
op, threshold_str = cmp_match.group(1), cmp_match.group(2)
|
|
174
|
+
threshold = float(threshold_str)
|
|
175
|
+
op_fn = _CMP_OPS.get(op)
|
|
176
|
+
if op_fn is None:
|
|
177
|
+
return False # unrecognized operator — fail closed
|
|
178
|
+
if not op_fn(num, threshold):
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
return True
|