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,137 @@
|
|
|
1
|
+
"""Provenance write path for derived code-intel artifacts.
|
|
2
|
+
|
|
3
|
+
Normalizes a ``Provenance`` dataclass (from the shared ``artifacts/``
|
|
4
|
+
package) into a row in the server-side ``provenance`` table.
|
|
5
|
+
|
|
6
|
+
- :func:`write_provenance_rows` takes a list of dicts so batched writes
|
|
7
|
+
(one per embedding chunk in the same upsert) collapse into a single
|
|
8
|
+
``session.add_all`` call.
|
|
9
|
+
- Schema is loose: keys that don't match ``Provenance`` columns flow
|
|
10
|
+
into the ``extra`` JSONB bucket, so callers can tack on contextual
|
|
11
|
+
metadata without a migration.
|
|
12
|
+
- No commit — the caller owns the session lifecycle.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import socket
|
|
19
|
+
import time
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Columns that are first-class on the Provenance ORM (everything else
|
|
29
|
+
# in the input dict goes into ``extra``).
|
|
30
|
+
_TOP_LEVEL_COLS: frozenset[str] = frozenset({
|
|
31
|
+
"action_hash",
|
|
32
|
+
"artifact_type",
|
|
33
|
+
"input_blob_oid",
|
|
34
|
+
"input_tree_oid",
|
|
35
|
+
"indexer_name",
|
|
36
|
+
"indexer_version",
|
|
37
|
+
"config_digest",
|
|
38
|
+
"producer_service",
|
|
39
|
+
"producer_user_id",
|
|
40
|
+
"producer_job_id",
|
|
41
|
+
"producer_host",
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _split_known(data: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
46
|
+
"""Partition a raw dict into (top-level, extra) based on ORM columns."""
|
|
47
|
+
top: dict[str, Any] = {}
|
|
48
|
+
extra: dict[str, Any] = {}
|
|
49
|
+
for k, v in data.items():
|
|
50
|
+
if k in _TOP_LEVEL_COLS:
|
|
51
|
+
top[k] = v
|
|
52
|
+
else:
|
|
53
|
+
extra[k] = v
|
|
54
|
+
return top, extra
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def write_provenance_rows(
|
|
58
|
+
session: AsyncSession,
|
|
59
|
+
rows: list[dict[str, Any]],
|
|
60
|
+
) -> int:
|
|
61
|
+
"""Insert provenance rows for a batch of derived artifacts.
|
|
62
|
+
|
|
63
|
+
Each ``row`` is a dict whose keys are either Provenance ORM column
|
|
64
|
+
names or arbitrary extras (which land in the ``extra`` JSONB bucket).
|
|
65
|
+
Missing required fields are filled from sensible defaults so callers
|
|
66
|
+
can write a provenance row with just an ``action_hash`` +
|
|
67
|
+
``input_blob_oid`` if that's all they know.
|
|
68
|
+
|
|
69
|
+
Returns the number of rows inserted. Does NOT commit — the caller
|
|
70
|
+
is expected to be inside a larger unit of work that commits at its
|
|
71
|
+
own boundary.
|
|
72
|
+
"""
|
|
73
|
+
if not rows:
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
from attocode_intel.db.models import Provenance
|
|
77
|
+
|
|
78
|
+
default_host = socket.gethostname() or "unknown"
|
|
79
|
+
_ = time # keep import alive for future use (e.g. produced_at override)
|
|
80
|
+
|
|
81
|
+
orm_rows: list[Provenance] = []
|
|
82
|
+
for raw in rows:
|
|
83
|
+
top, extra = _split_known(dict(raw))
|
|
84
|
+
orm_rows.append(
|
|
85
|
+
Provenance(
|
|
86
|
+
action_hash=str(top.get("action_hash", "")),
|
|
87
|
+
artifact_type=str(top.get("artifact_type", "unknown")),
|
|
88
|
+
input_blob_oid=str(top.get("input_blob_oid", "")),
|
|
89
|
+
input_tree_oid=top.get("input_tree_oid"),
|
|
90
|
+
indexer_name=str(top.get("indexer_name", "unknown")),
|
|
91
|
+
indexer_version=str(top.get("indexer_version", "0")),
|
|
92
|
+
config_digest=str(top.get("config_digest", "")),
|
|
93
|
+
producer_service=str(
|
|
94
|
+
top.get("producer_service", "attocode-server"),
|
|
95
|
+
),
|
|
96
|
+
producer_user_id=top.get("producer_user_id"),
|
|
97
|
+
producer_job_id=top.get("producer_job_id"),
|
|
98
|
+
producer_host=str(top.get("producer_host") or default_host),
|
|
99
|
+
extra=extra,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
session.add_all(orm_rows)
|
|
104
|
+
return len(orm_rows)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def embedding_provenance_dict(
|
|
108
|
+
*,
|
|
109
|
+
action_hash: str,
|
|
110
|
+
content_sha: str,
|
|
111
|
+
model_name: str,
|
|
112
|
+
model_version: str,
|
|
113
|
+
dimension: int | None,
|
|
114
|
+
indexer_name: str = "attocode-embedding-store",
|
|
115
|
+
indexer_version: str = "1",
|
|
116
|
+
config_digest: str = "",
|
|
117
|
+
**extra: Any,
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
"""Build the dict that gets passed to :func:`write_provenance_rows`
|
|
120
|
+
for an embedding chunk.
|
|
121
|
+
|
|
122
|
+
Factored out so :class:`EmbeddingStore` doesn't have to hand-assemble
|
|
123
|
+
the same dict in every insert path.
|
|
124
|
+
"""
|
|
125
|
+
row: dict[str, Any] = {
|
|
126
|
+
"action_hash": action_hash,
|
|
127
|
+
"artifact_type": "embedding_blob_v1",
|
|
128
|
+
"input_blob_oid": f"sha256:{content_sha}",
|
|
129
|
+
"indexer_name": indexer_name,
|
|
130
|
+
"indexer_version": indexer_version,
|
|
131
|
+
"config_digest": config_digest,
|
|
132
|
+
"model_name": model_name,
|
|
133
|
+
"model_version": model_version,
|
|
134
|
+
"dimension": dimension,
|
|
135
|
+
}
|
|
136
|
+
row.update(extra)
|
|
137
|
+
return row
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""DB-backed security scanning — runs regex patterns on ContentStore content.
|
|
2
|
+
|
|
3
|
+
For remote repos without a local clone, this provides security scanning
|
|
4
|
+
by reading file content from the database and applying the same patterns
|
|
5
|
+
used by the local scanner.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from attocode_intel._internal.integrations.security.matcher import iter_pattern_matches
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Scannable file extensions (same as local scanner)
|
|
22
|
+
_SCANNABLE_EXTENSIONS = {
|
|
23
|
+
".py", ".pyi", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
24
|
+
".rs", ".go", ".java", ".kt", ".rb", ".php", ".swift", ".cs",
|
|
25
|
+
".c", ".cpp", ".h", ".hpp", ".lua", ".sh", ".bash", ".zsh",
|
|
26
|
+
".yaml", ".yml", ".toml", ".json", ".env", ".cfg", ".ini", ".conf",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Max file size to scan (200KB)
|
|
30
|
+
_MAX_SCAN_SIZE = 200 * 1024
|
|
31
|
+
|
|
32
|
+
# Max files to scan per run
|
|
33
|
+
_MAX_SCAN_FILES = 500
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _detect_language(path: str) -> str:
|
|
37
|
+
"""Detect language from file extension."""
|
|
38
|
+
import os
|
|
39
|
+
ext = os.path.splitext(path)[1].lower()
|
|
40
|
+
lang_map = {
|
|
41
|
+
".py": "python", ".pyi": "python",
|
|
42
|
+
".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
|
|
43
|
+
".ts": "typescript", ".tsx": "typescript",
|
|
44
|
+
".go": "go", ".rs": "rust", ".java": "java", ".kt": "kotlin",
|
|
45
|
+
".rb": "ruby", ".php": "php", ".swift": "swift", ".cs": "csharp",
|
|
46
|
+
".c": "c", ".cpp": "cpp", ".h": "c", ".hpp": "cpp",
|
|
47
|
+
}
|
|
48
|
+
return lang_map.get(ext, "")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_scannable(path: str) -> bool:
|
|
52
|
+
"""Check if a file path is scannable by extension or filename."""
|
|
53
|
+
import os
|
|
54
|
+
basename = os.path.basename(path)
|
|
55
|
+
ext = os.path.splitext(path)[1].lower()
|
|
56
|
+
# Handle dotfiles like .env (ext is ".env", basename is ".env")
|
|
57
|
+
if not ext and basename.startswith("."):
|
|
58
|
+
ext = basename.lower()
|
|
59
|
+
return ext in _SCANNABLE_EXTENSIONS
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def db_security_scan(
|
|
63
|
+
session: AsyncSession,
|
|
64
|
+
branch_id: uuid.UUID,
|
|
65
|
+
manifest: dict[str, str],
|
|
66
|
+
*,
|
|
67
|
+
mode: str = "full",
|
|
68
|
+
path_filter: str = "",
|
|
69
|
+
) -> dict:
|
|
70
|
+
"""Run security scan on DB-stored file contents.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
session: Async DB session.
|
|
74
|
+
branch_id: Branch UUID for scoping.
|
|
75
|
+
manifest: Resolved manifest (path → content_sha).
|
|
76
|
+
mode: Scan mode — "quick" (secrets only), "full" (secrets + patterns),
|
|
77
|
+
"secrets", "patterns".
|
|
78
|
+
path_filter: Optional path prefix filter.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dict with findings, total_findings, summary.
|
|
82
|
+
"""
|
|
83
|
+
from attocode_intel.storage.content_store import ContentStore
|
|
84
|
+
|
|
85
|
+
# Import patterns
|
|
86
|
+
from attocode_intel._internal.integrations.security.patterns import (
|
|
87
|
+
ANTI_PATTERNS,
|
|
88
|
+
SECRET_PATTERNS,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
content_store = ContentStore(session)
|
|
92
|
+
|
|
93
|
+
# Select which patterns to use based on mode
|
|
94
|
+
patterns = []
|
|
95
|
+
if mode in ("full", "quick", "secrets"):
|
|
96
|
+
patterns.extend(SECRET_PATTERNS)
|
|
97
|
+
if mode in ("full", "patterns"):
|
|
98
|
+
patterns.extend(ANTI_PATTERNS)
|
|
99
|
+
|
|
100
|
+
# Filter and limit files
|
|
101
|
+
scannable: list[tuple[str, str]] = [] # (path, sha)
|
|
102
|
+
for path, sha in sorted(manifest.items()):
|
|
103
|
+
if path_filter and not path.startswith(path_filter):
|
|
104
|
+
continue
|
|
105
|
+
if not _is_scannable(path):
|
|
106
|
+
continue
|
|
107
|
+
scannable.append((path, sha))
|
|
108
|
+
if len(scannable) >= _MAX_SCAN_FILES:
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
findings: list[dict] = []
|
|
112
|
+
files_scanned = 0
|
|
113
|
+
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
|
114
|
+
|
|
115
|
+
for path, sha in scannable:
|
|
116
|
+
content_bytes = await content_store.get(sha)
|
|
117
|
+
if content_bytes is None:
|
|
118
|
+
continue
|
|
119
|
+
if len(content_bytes) > _MAX_SCAN_SIZE:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
text = content_bytes.decode("utf-8", errors="replace")
|
|
124
|
+
except Exception:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
files_scanned += 1
|
|
128
|
+
file_lang = _detect_language(path)
|
|
129
|
+
|
|
130
|
+
for line_no, line, pat in iter_pattern_matches(text, patterns, file_lang):
|
|
131
|
+
sev = str(pat.severity)
|
|
132
|
+
severity_counts[sev] = severity_counts.get(sev, 0) + 1
|
|
133
|
+
findings.append({
|
|
134
|
+
"pattern": pat.name,
|
|
135
|
+
"severity": sev,
|
|
136
|
+
"category": str(pat.category),
|
|
137
|
+
"cwe_id": pat.cwe_id,
|
|
138
|
+
"file": path,
|
|
139
|
+
"line": line_no,
|
|
140
|
+
"message": pat.message,
|
|
141
|
+
"recommendation": pat.recommendation,
|
|
142
|
+
"snippet": line.strip()[:200],
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
# Compute compliance score
|
|
146
|
+
score = 100 - (
|
|
147
|
+
severity_counts.get("critical", 0) * 20
|
|
148
|
+
+ severity_counts.get("high", 0) * 10
|
|
149
|
+
+ severity_counts.get("medium", 0) * 3
|
|
150
|
+
+ severity_counts.get("low", 0) * 1
|
|
151
|
+
)
|
|
152
|
+
score = max(0, min(100, score))
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"mode": mode,
|
|
156
|
+
"path": path_filter,
|
|
157
|
+
"findings": findings,
|
|
158
|
+
"total_findings": len(findings),
|
|
159
|
+
"summary": {
|
|
160
|
+
"files_scanned": files_scanned,
|
|
161
|
+
"compliance_score": score,
|
|
162
|
+
**severity_counts,
|
|
163
|
+
},
|
|
164
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Symbol storage — content-addressed symbol indexing with branch resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SymbolStore:
|
|
13
|
+
"""Content-addressed symbol storage.
|
|
14
|
+
|
|
15
|
+
Symbols are keyed by content SHA, so identical files share symbol data.
|
|
16
|
+
Branch-aware queries resolve through BranchOverlay.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
20
|
+
self._session = session
|
|
21
|
+
|
|
22
|
+
async def has_symbols(self, content_sha: str) -> bool:
|
|
23
|
+
"""Check if symbols already exist for a content SHA. Used for dedup gating."""
|
|
24
|
+
from sqlalchemy import select
|
|
25
|
+
|
|
26
|
+
from attocode_intel.db.models import Symbol
|
|
27
|
+
|
|
28
|
+
result = await self._session.execute(
|
|
29
|
+
select(Symbol.id).where(Symbol.content_sha == content_sha).limit(1)
|
|
30
|
+
)
|
|
31
|
+
return result.scalar_one_or_none() is not None
|
|
32
|
+
|
|
33
|
+
async def upsert_symbols(
|
|
34
|
+
self,
|
|
35
|
+
content_sha: str,
|
|
36
|
+
symbols: list[dict],
|
|
37
|
+
skip_if_exists: bool = False,
|
|
38
|
+
) -> int:
|
|
39
|
+
"""Insert or update symbols for a content hash.
|
|
40
|
+
|
|
41
|
+
Each symbol dict should have: name, kind, line_start, line_end,
|
|
42
|
+
signature (optional), exported (optional).
|
|
43
|
+
|
|
44
|
+
If skip_if_exists=True, returns 0 immediately when SHA already has symbols.
|
|
45
|
+
This is the content-hash dedup optimization — same content always produces
|
|
46
|
+
the same symbols.
|
|
47
|
+
|
|
48
|
+
Returns count of symbols stored.
|
|
49
|
+
"""
|
|
50
|
+
from attocode_intel.db.models import Symbol
|
|
51
|
+
|
|
52
|
+
if skip_if_exists and await self.has_symbols(content_sha):
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
# Bulk DELETE — O(N) individual ORM deletes are too slow here.
|
|
56
|
+
from sqlalchemy import delete
|
|
57
|
+
await self._session.execute(
|
|
58
|
+
delete(Symbol).where(Symbol.content_sha == content_sha)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Insert new
|
|
62
|
+
for s in symbols:
|
|
63
|
+
sym = Symbol(
|
|
64
|
+
content_sha=content_sha,
|
|
65
|
+
name=s["name"],
|
|
66
|
+
kind=s["kind"],
|
|
67
|
+
line_start=s.get("line_start"),
|
|
68
|
+
line_end=s.get("line_end"),
|
|
69
|
+
signature=s.get("signature"),
|
|
70
|
+
exported=s.get("exported", False),
|
|
71
|
+
metadata_=s.get("metadata", {}),
|
|
72
|
+
)
|
|
73
|
+
self._session.add(sym)
|
|
74
|
+
|
|
75
|
+
await self._session.flush()
|
|
76
|
+
return len(symbols)
|
|
77
|
+
|
|
78
|
+
async def search_symbols(
|
|
79
|
+
self,
|
|
80
|
+
repo_id: uuid.UUID,
|
|
81
|
+
branch_id: uuid.UUID,
|
|
82
|
+
pattern: str,
|
|
83
|
+
limit: int = 50,
|
|
84
|
+
) -> list[dict]:
|
|
85
|
+
"""Search symbols by name pattern within a branch context.
|
|
86
|
+
|
|
87
|
+
Resolves through the branch overlay to find relevant content SHAs.
|
|
88
|
+
"""
|
|
89
|
+
from sqlalchemy import select
|
|
90
|
+
|
|
91
|
+
from attocode_intel.db.models import Symbol
|
|
92
|
+
from attocode_intel.storage.branch_overlay import BranchOverlay
|
|
93
|
+
|
|
94
|
+
overlay = BranchOverlay(self._session)
|
|
95
|
+
manifest = await overlay.resolve_manifest(branch_id)
|
|
96
|
+
content_shas = set(manifest.values())
|
|
97
|
+
|
|
98
|
+
if not content_shas:
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
result = await self._session.execute(
|
|
102
|
+
select(Symbol)
|
|
103
|
+
.where(
|
|
104
|
+
Symbol.content_sha.in_(content_shas),
|
|
105
|
+
Symbol.name.ilike(f"%{pattern}%"),
|
|
106
|
+
)
|
|
107
|
+
.limit(limit)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Build reverse map: sha → path
|
|
111
|
+
sha_to_path = {sha: path for path, sha in manifest.items()}
|
|
112
|
+
|
|
113
|
+
symbols = []
|
|
114
|
+
for sym in result.scalars():
|
|
115
|
+
symbols.append({
|
|
116
|
+
"name": sym.name,
|
|
117
|
+
"kind": sym.kind,
|
|
118
|
+
"file": sha_to_path.get(sym.content_sha, "unknown"),
|
|
119
|
+
"line_start": sym.line_start,
|
|
120
|
+
"line_end": sym.line_end,
|
|
121
|
+
"signature": sym.signature,
|
|
122
|
+
"exported": sym.exported,
|
|
123
|
+
})
|
|
124
|
+
return symbols
|
|
125
|
+
|
|
126
|
+
async def get_symbols_for_branch(
|
|
127
|
+
self,
|
|
128
|
+
branch_id: uuid.UUID,
|
|
129
|
+
) -> list[dict]:
|
|
130
|
+
"""Get all symbols for a branch, resolved through overlay."""
|
|
131
|
+
from sqlalchemy import select
|
|
132
|
+
|
|
133
|
+
from attocode_intel.db.models import Symbol
|
|
134
|
+
from attocode_intel.storage.branch_overlay import BranchOverlay
|
|
135
|
+
|
|
136
|
+
overlay = BranchOverlay(self._session)
|
|
137
|
+
manifest = await overlay.resolve_manifest(branch_id)
|
|
138
|
+
content_shas = set(manifest.values())
|
|
139
|
+
|
|
140
|
+
if not content_shas:
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
sha_to_path = {sha: path for path, sha in manifest.items()}
|
|
144
|
+
|
|
145
|
+
result = await self._session.execute(
|
|
146
|
+
select(Symbol).where(Symbol.content_sha.in_(content_shas))
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
symbols = []
|
|
150
|
+
for sym in result.scalars():
|
|
151
|
+
symbols.append({
|
|
152
|
+
"name": sym.name,
|
|
153
|
+
"kind": sym.kind,
|
|
154
|
+
"file": sha_to_path.get(sym.content_sha, "unknown"),
|
|
155
|
+
"line_start": sym.line_start,
|
|
156
|
+
"line_end": sym.line_end,
|
|
157
|
+
"signature": sym.signature,
|
|
158
|
+
"exported": sym.exported,
|
|
159
|
+
})
|
|
160
|
+
return symbols
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Syntax-local evidence groups; this does not resolve data flow or earlier exits."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
LANGUAGES = {'.py': 'python', '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
8
|
+
'.ts': 'typescript', '.tsx': 'tsx', '.jsx': 'javascript'}
|
|
9
|
+
CONTROLS = {'if_statement', 'elif_clause', 'else_clause', 'for_statement', 'for_in_statement',
|
|
10
|
+
'while_statement', 'with_statement', 'try_statement', 'except_clause',
|
|
11
|
+
'catch_clause', 'finally_clause', 'switch_statement', 'switch_case', 'switch_default'}
|
|
12
|
+
BLOCKS = {'block', 'statement_block', 'switch_body'}
|
|
13
|
+
LEAVES = {'identifier', 'property_identifier', 'string', 'integer', 'float', 'number',
|
|
14
|
+
'true', 'false', 'none', 'null'}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def evidence_terms(value):
|
|
18
|
+
from attocode_intel.focused_evidence import terms
|
|
19
|
+
return terms(value) | set(re.findall(r'--[\w-]*|(?<!\w)\d+(?!\w)', value))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def structural_excerpts(source, definition, hint, preview_end):
|
|
23
|
+
"""Return None when structural context is unavailable, otherwise ranked groups.
|
|
24
|
+
|
|
25
|
+
Each list member owns its context so the response budget drops it atomically.
|
|
26
|
+
Context ranges describe enclosing syntax, not complete executable reachability.
|
|
27
|
+
"""
|
|
28
|
+
language = LANGUAGES.get(Path(definition.file_path).suffix)
|
|
29
|
+
if not language:
|
|
30
|
+
return None
|
|
31
|
+
from attocode_intel._internal.integrations.context.ts_parser import _get_parser
|
|
32
|
+
parser = _get_parser(language)
|
|
33
|
+
if parser is None:
|
|
34
|
+
return None
|
|
35
|
+
raw = '\n'.join(source).encode()
|
|
36
|
+
tree = parser.parse(raw) # No awaits between parse and traversal of this local tree.
|
|
37
|
+
if tree.root_node.has_error:
|
|
38
|
+
return None
|
|
39
|
+
from attocode_intel.focused_evidence import task_terms
|
|
40
|
+
positive, _ = task_terms(hint)
|
|
41
|
+
query = positive | {term for term in evidence_terms(hint)
|
|
42
|
+
if term.startswith('--') or term.isdigit()}
|
|
43
|
+
first, last = definition.start_line, min(definition.end_line, len(source))
|
|
44
|
+
|
|
45
|
+
def bounds(node):
|
|
46
|
+
return node.start_point.row + 1, node.end_point.row + bool(node.end_point.column)
|
|
47
|
+
|
|
48
|
+
def header(node):
|
|
49
|
+
bodies = [c for c in node.children if c.type in BLOCKS]
|
|
50
|
+
for field in ('consequence', 'body'):
|
|
51
|
+
child = node.child_by_field_name(field)
|
|
52
|
+
if child is not None:
|
|
53
|
+
bodies.append(child)
|
|
54
|
+
a, b = bounds(node)
|
|
55
|
+
if bodies:
|
|
56
|
+
body = min(bodies, key=lambda c: c.start_byte)
|
|
57
|
+
# Keep a same-line statement verbatim; do not invent sliced source lines.
|
|
58
|
+
b = max(a, body.start_point.row + (body.type == 'statement_block'))
|
|
59
|
+
return a, b
|
|
60
|
+
|
|
61
|
+
def controls(node):
|
|
62
|
+
ranges = []
|
|
63
|
+
child, parent = node, node.parent
|
|
64
|
+
while parent is not None and bounds(parent)[0] >= first:
|
|
65
|
+
if parent.type in CONTROLS:
|
|
66
|
+
ranges.append(header(parent))
|
|
67
|
+
# Python elif/else are sibling clauses, so include preceding guards.
|
|
68
|
+
for sibling in parent.named_children:
|
|
69
|
+
if sibling.start_byte >= child.start_byte:
|
|
70
|
+
break
|
|
71
|
+
if sibling.type == 'elif_clause':
|
|
72
|
+
ranges.append(header(sibling))
|
|
73
|
+
child, parent = parent, parent.parent
|
|
74
|
+
return ranges
|
|
75
|
+
|
|
76
|
+
def tokens(node, interval=None):
|
|
77
|
+
if interval and (bounds(node)[1] < interval[0] or bounds(node)[0] > interval[1]):
|
|
78
|
+
return set()
|
|
79
|
+
if node.type in {'comment', 'comment_block'}:
|
|
80
|
+
return set()
|
|
81
|
+
# A bare Python string statement is documentation, not an executable literal.
|
|
82
|
+
if node.type == 'expression_statement' and node.named_children and node.named_children[0].type == 'string':
|
|
83
|
+
return set()
|
|
84
|
+
if node.type in LEAVES:
|
|
85
|
+
return evidence_terms(raw[node.start_byte:node.end_byte].decode())
|
|
86
|
+
result = set()
|
|
87
|
+
for child in node.named_children:
|
|
88
|
+
result.update(tokens(child, interval))
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
candidates = []
|
|
92
|
+
stack = [tree.root_node]
|
|
93
|
+
while stack:
|
|
94
|
+
node = stack.pop()
|
|
95
|
+
a, b = bounds(node)
|
|
96
|
+
if b < first or a > last:
|
|
97
|
+
continue
|
|
98
|
+
stack.extend(reversed(node.named_children))
|
|
99
|
+
is_control = node.type in CONTROLS
|
|
100
|
+
is_statement = node.type.endswith('_statement') or node.type in {'lexical_declaration', 'variable_declaration'}
|
|
101
|
+
if not (is_control or is_statement) or a < first or b > last:
|
|
102
|
+
continue
|
|
103
|
+
# Compound statements contribute a header or a small complete branch.
|
|
104
|
+
if is_control:
|
|
105
|
+
effect = (a, b) if b - a < 18 and node.type in {'if_statement', 'elif_clause', 'else_clause'} else header(node)
|
|
106
|
+
else:
|
|
107
|
+
if any(c.type in BLOCKS for c in node.named_children):
|
|
108
|
+
continue
|
|
109
|
+
effect = (a, b)
|
|
110
|
+
if effect[1] <= preview_end:
|
|
111
|
+
continue
|
|
112
|
+
hits = tokens(node, effect) & query
|
|
113
|
+
if not hits:
|
|
114
|
+
continue
|
|
115
|
+
intervals = sorted(set([effect, *controls(node)]))
|
|
116
|
+
merged = []
|
|
117
|
+
for begin, end in intervals:
|
|
118
|
+
if merged and (begin <= merged[-1][1] + 1 or end - merged[-1][0] < 12):
|
|
119
|
+
merged[-1] = (merged[-1][0], max(end, merged[-1][1]))
|
|
120
|
+
else:
|
|
121
|
+
merged.append((begin, end))
|
|
122
|
+
# Prefer one small contiguous excerpt. Large gaps stay explicitly linked.
|
|
123
|
+
if merged[-1][1] - merged[0][0] < 12:
|
|
124
|
+
merged = [(merged[0][0], merged[-1][1])]
|
|
125
|
+
main = next(r for r in merged if r[0] <= effect[0] and r[1] >= effect[1])
|
|
126
|
+
context = [r for r in merged if r != main]
|
|
127
|
+
candidates.append((len(hits) + int(effect == (a, b)), hits, main, context))
|
|
128
|
+
|
|
129
|
+
def excerpt(interval):
|
|
130
|
+
a, b = interval
|
|
131
|
+
return {'file_path': definition.file_path, 'start_line': a, 'end_line': b,
|
|
132
|
+
'text': '\n'.join(source[a-1:b])}
|
|
133
|
+
|
|
134
|
+
selected, covered, occupied = [], set(), set()
|
|
135
|
+
while candidates and len(selected) < 3:
|
|
136
|
+
score, hits, main, context = max(candidates, key=lambda r: (r[0] + 4 * len(r[1] - covered), -r[2][0]))
|
|
137
|
+
selected.append({**excerpt(main), 'matched_terms': sorted(hits),
|
|
138
|
+
'context_ranges': [excerpt(r) for r in context],
|
|
139
|
+
'context_relation': 'enclosing branch and preceding alternative guards',
|
|
140
|
+
'context_incomplete': False})
|
|
141
|
+
covered.update(hits)
|
|
142
|
+
occupied.update(range(main[0], main[1] + 1))
|
|
143
|
+
candidates = [r for r in candidates if not occupied.intersection(range(r[2][0], r[2][1]+1))]
|
|
144
|
+
return selected
|