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,21 @@
|
|
|
1
|
+
"""Validate remotely supplied repository locations before storing them."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
from fastapi import HTTPException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_remote_source(clone_url=None, local_path=None):
|
|
11
|
+
from attocode_intel.api.deps import get_config
|
|
12
|
+
|
|
13
|
+
if not get_config().is_service_mode:
|
|
14
|
+
return
|
|
15
|
+
if local_path:
|
|
16
|
+
raise HTTPException(422, "Remote clients cannot select server filesystem paths. Connect a Git URL or use local MCP.")
|
|
17
|
+
if clone_url:
|
|
18
|
+
parsed = urlsplit(clone_url)
|
|
19
|
+
scp_url = re.fullmatch(r"[\w.-]+@[\w.-]+:[^\s]+", clone_url)
|
|
20
|
+
if not scp_url and not (parsed.scheme in {"https", "http", "ssh"} and parsed.hostname):
|
|
21
|
+
raise HTTPException(422, "Repository URL must use HTTP(S) or SSH; local paths and file URLs are not accepted.")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API route modules."""
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Server-side retrieval pin computation.
|
|
2
|
+
|
|
3
|
+
HTTP search routes return ``pin_id`` + ``manifest_hash`` so clients can
|
|
4
|
+
track the state they queried against (matching the local stdio MCP
|
|
5
|
+
``_stamp_pin``). Per-store manifest hashes are computed via cheap
|
|
6
|
+
``SELECT COUNT + MAX(updated_at)`` probes; pins are ephemeral and not
|
|
7
|
+
persisted in a pins table.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import uuid
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from attocode_intel.artifacts import RetrievalPin, compute_store_hash
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def compute_server_manifest_hashes(
|
|
25
|
+
session: AsyncSession,
|
|
26
|
+
*,
|
|
27
|
+
repo_id: uuid.UUID | None = None,
|
|
28
|
+
branch_id: uuid.UUID | None = None,
|
|
29
|
+
) -> dict[str, str]:
|
|
30
|
+
"""Compute cheap per-store content hashes for the server-side stores.
|
|
31
|
+
|
|
32
|
+
Returns a ``{store_name: sha256hex}`` dict that matches the shape
|
|
33
|
+
of :func:`attocode_intel.tools.pin_tools._compute_current_manifest_hashes`
|
|
34
|
+
(the local-side equivalent). A store's hash changes whenever its
|
|
35
|
+
row count or latest ``created_at`` / ``updated_at`` timestamp moves.
|
|
36
|
+
|
|
37
|
+
``repo_id`` / ``branch_id`` may scope the hash; the current
|
|
38
|
+
implementation uses a global view — the returned hash is
|
|
39
|
+
informational, not a cryptographic commitment to the repo's exact
|
|
40
|
+
state.
|
|
41
|
+
"""
|
|
42
|
+
from sqlalchemy import text
|
|
43
|
+
|
|
44
|
+
results: dict[str, str] = {}
|
|
45
|
+
|
|
46
|
+
# Each store is probed via a single cheap aggregate query. Failures
|
|
47
|
+
# on any individual store fall through to ``absent`` so a missing
|
|
48
|
+
# table (e.g. on an older schema) doesn't break the whole response.
|
|
49
|
+
async def _probe(name: str, sql: str, params: dict) -> None:
|
|
50
|
+
try:
|
|
51
|
+
row = (await session.execute(text(sql), params)).fetchone()
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
logger.debug("server pin: %s probe failed: %s", name, exc)
|
|
54
|
+
results[name] = "absent"
|
|
55
|
+
return
|
|
56
|
+
if row is None:
|
|
57
|
+
results[name] = "absent"
|
|
58
|
+
return
|
|
59
|
+
row_count = int(row[0] or 0)
|
|
60
|
+
# Postgres MAX() on timestamptz returns a datetime; coerce to
|
|
61
|
+
# float epoch for the hash so it matches the local format.
|
|
62
|
+
ts = row[1]
|
|
63
|
+
max_updated_at: float | None = None
|
|
64
|
+
if ts is not None:
|
|
65
|
+
try:
|
|
66
|
+
max_updated_at = ts.timestamp()
|
|
67
|
+
except AttributeError:
|
|
68
|
+
max_updated_at = float(ts)
|
|
69
|
+
results[name] = compute_store_hash(
|
|
70
|
+
schema_version="1",
|
|
71
|
+
row_count=row_count,
|
|
72
|
+
max_updated_at=max_updated_at,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
repo_clause = "" if repo_id is None else " (scoped)"
|
|
76
|
+
branch_params: dict = {}
|
|
77
|
+
if branch_id is not None:
|
|
78
|
+
branch_params["branch_id"] = str(branch_id)
|
|
79
|
+
|
|
80
|
+
# file_contents — global; repo scoping would require a join via
|
|
81
|
+
# branch_files which is overkill here.
|
|
82
|
+
await _probe(
|
|
83
|
+
"file_contents",
|
|
84
|
+
"SELECT COUNT(*), MAX(created_at) FROM file_contents",
|
|
85
|
+
{},
|
|
86
|
+
)
|
|
87
|
+
await _probe(
|
|
88
|
+
"symbols",
|
|
89
|
+
"SELECT COUNT(*), MAX(created_at) FROM symbols",
|
|
90
|
+
{},
|
|
91
|
+
)
|
|
92
|
+
await _probe(
|
|
93
|
+
"embeddings",
|
|
94
|
+
"SELECT COUNT(*), MAX(created_at) FROM embeddings",
|
|
95
|
+
{},
|
|
96
|
+
)
|
|
97
|
+
await _probe(
|
|
98
|
+
"dependencies",
|
|
99
|
+
"SELECT COUNT(*), NULL::timestamptz FROM dependencies",
|
|
100
|
+
{},
|
|
101
|
+
)
|
|
102
|
+
# branch_files scoped per branch if we have one, else global row count.
|
|
103
|
+
if branch_id is not None:
|
|
104
|
+
await _probe(
|
|
105
|
+
"branch_files",
|
|
106
|
+
"SELECT COUNT(*), NULL::timestamptz FROM branch_files "
|
|
107
|
+
"WHERE branch_id = CAST(:branch_id AS uuid)",
|
|
108
|
+
branch_params,
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
await _probe(
|
|
112
|
+
"branch_files",
|
|
113
|
+
"SELECT COUNT(*), NULL::timestamptz FROM branch_files",
|
|
114
|
+
{},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
logger.debug(
|
|
118
|
+
"server pin%s: computed %d store hashes",
|
|
119
|
+
repo_clause, len(results),
|
|
120
|
+
)
|
|
121
|
+
return results
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def build_retrieval_pin(
|
|
125
|
+
session: AsyncSession,
|
|
126
|
+
*,
|
|
127
|
+
repo_id: uuid.UUID | None = None,
|
|
128
|
+
branch_id: uuid.UUID | None = None,
|
|
129
|
+
) -> RetrievalPin:
|
|
130
|
+
"""Build a fresh :class:`RetrievalPin` for the current server state.
|
|
131
|
+
|
|
132
|
+
Server pins are ephemeral (not persisted in a pins table). The
|
|
133
|
+
``pin_id`` is deterministic off the manifest hash so repeat calls
|
|
134
|
+
on an unchanged state return an identical id, letting clients
|
|
135
|
+
cheap-compare across requests.
|
|
136
|
+
"""
|
|
137
|
+
hashes = await compute_server_manifest_hashes(
|
|
138
|
+
session, repo_id=repo_id, branch_id=branch_id,
|
|
139
|
+
)
|
|
140
|
+
pin = RetrievalPin.create(
|
|
141
|
+
manifest_hashes=hashes,
|
|
142
|
+
branch_id=str(branch_id) if branch_id else None,
|
|
143
|
+
ttl_seconds=0,
|
|
144
|
+
)
|
|
145
|
+
# Deterministic id — matches the local ``_stamp_pin`` format
|
|
146
|
+
# (``pin_<hex20>``) so clients don't need to distinguish server
|
|
147
|
+
# vs. local pins by format.
|
|
148
|
+
deterministic_id = f"pin_{pin.manifest_hash[:20]}"
|
|
149
|
+
return RetrievalPin(
|
|
150
|
+
pin_id=deterministic_id,
|
|
151
|
+
schema_version=pin.schema_version,
|
|
152
|
+
manifest_hashes=pin.manifest_hashes,
|
|
153
|
+
manifest_hash=pin.manifest_hash,
|
|
154
|
+
overlay_id=pin.overlay_id,
|
|
155
|
+
branch_id=pin.branch_id,
|
|
156
|
+
created_at=pin.created_at,
|
|
157
|
+
expires_at=pin.expires_at,
|
|
158
|
+
)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Activity feed / audit log endpoints (service mode only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from sqlalchemy import func, select
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
from attocode_intel.api.auth import resolve_auth
|
|
14
|
+
from attocode_intel.api.auth.context import AuthContext
|
|
15
|
+
from attocode_intel.api.deps import get_db_session
|
|
16
|
+
|
|
17
|
+
router = APIRouter(tags=["activity"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# --- Response models ---
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ActivityEventResponse(BaseModel):
|
|
24
|
+
id: str
|
|
25
|
+
event_type: str
|
|
26
|
+
org_id: str
|
|
27
|
+
repo_id: str | None = None
|
|
28
|
+
user_id: str | None = None
|
|
29
|
+
detail: dict = {}
|
|
30
|
+
created_at: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ActivityFeedResponse(BaseModel):
|
|
34
|
+
events: list[ActivityEventResponse]
|
|
35
|
+
total: int
|
|
36
|
+
limit: int
|
|
37
|
+
offset: int
|
|
38
|
+
has_more: bool
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# --- Endpoints ---
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@router.get("/api/v2/orgs/{org_id}/activity", response_model=ActivityFeedResponse)
|
|
45
|
+
async def get_org_activity(
|
|
46
|
+
org_id: uuid.UUID,
|
|
47
|
+
limit: int = Query(50, ge=1, le=100),
|
|
48
|
+
offset: int = Query(0, ge=0),
|
|
49
|
+
event_type: str = Query("", description="Filter by event type"),
|
|
50
|
+
since: str = Query("", description="ISO datetime to filter events after"),
|
|
51
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
52
|
+
session: AsyncSession = Depends(get_db_session),
|
|
53
|
+
) -> ActivityFeedResponse:
|
|
54
|
+
"""Get organization activity feed (paginated)."""
|
|
55
|
+
from attocode_intel.db.models import AuditEvent
|
|
56
|
+
|
|
57
|
+
# Build query
|
|
58
|
+
query = select(AuditEvent).where(AuditEvent.org_id == org_id)
|
|
59
|
+
|
|
60
|
+
if event_type:
|
|
61
|
+
query = query.where(AuditEvent.event_type == event_type)
|
|
62
|
+
if since:
|
|
63
|
+
try:
|
|
64
|
+
since_dt = datetime.fromisoformat(since)
|
|
65
|
+
query = query.where(AuditEvent.created_at >= since_dt)
|
|
66
|
+
except ValueError:
|
|
67
|
+
raise HTTPException(status_code=400, detail="Invalid 'since' datetime format")
|
|
68
|
+
|
|
69
|
+
# Count total
|
|
70
|
+
count_result = await session.execute(
|
|
71
|
+
select(func.count()).select_from(query.subquery())
|
|
72
|
+
)
|
|
73
|
+
total = count_result.scalar() or 0
|
|
74
|
+
|
|
75
|
+
# Fetch page
|
|
76
|
+
result = await session.execute(
|
|
77
|
+
query.order_by(AuditEvent.created_at.desc())
|
|
78
|
+
.offset(offset).limit(limit)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
events = []
|
|
82
|
+
for event in result.scalars():
|
|
83
|
+
events.append(ActivityEventResponse(
|
|
84
|
+
id=str(event.id),
|
|
85
|
+
event_type=event.event_type,
|
|
86
|
+
org_id=str(event.org_id),
|
|
87
|
+
repo_id=str(event.repo_id) if event.repo_id else None,
|
|
88
|
+
user_id=str(event.user_id) if event.user_id else None,
|
|
89
|
+
detail=event.detail or {},
|
|
90
|
+
created_at=event.created_at.isoformat(),
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
return ActivityFeedResponse(
|
|
94
|
+
events=events, total=total, limit=limit, offset=offset,
|
|
95
|
+
has_more=(offset + limit < total),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@router.get("/api/v2/repos/{repo_id}/activity", response_model=ActivityFeedResponse)
|
|
100
|
+
async def get_repo_activity(
|
|
101
|
+
repo_id: uuid.UUID,
|
|
102
|
+
limit: int = Query(50, ge=1, le=100),
|
|
103
|
+
offset: int = Query(0, ge=0),
|
|
104
|
+
event_type: str = Query("", description="Filter by event type"),
|
|
105
|
+
since: str = Query("", description="ISO datetime to filter events after"),
|
|
106
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
107
|
+
session: AsyncSession = Depends(get_db_session),
|
|
108
|
+
) -> ActivityFeedResponse:
|
|
109
|
+
"""Get repository activity feed (paginated)."""
|
|
110
|
+
from attocode_intel.db.models import AuditEvent
|
|
111
|
+
|
|
112
|
+
query = select(AuditEvent).where(AuditEvent.repo_id == repo_id)
|
|
113
|
+
|
|
114
|
+
if event_type:
|
|
115
|
+
query = query.where(AuditEvent.event_type == event_type)
|
|
116
|
+
if since:
|
|
117
|
+
try:
|
|
118
|
+
since_dt = datetime.fromisoformat(since)
|
|
119
|
+
query = query.where(AuditEvent.created_at >= since_dt)
|
|
120
|
+
except ValueError:
|
|
121
|
+
raise HTTPException(status_code=400, detail="Invalid 'since' datetime format")
|
|
122
|
+
|
|
123
|
+
count_result = await session.execute(
|
|
124
|
+
select(func.count()).select_from(query.subquery())
|
|
125
|
+
)
|
|
126
|
+
total = count_result.scalar() or 0
|
|
127
|
+
|
|
128
|
+
result = await session.execute(
|
|
129
|
+
query.order_by(AuditEvent.created_at.desc())
|
|
130
|
+
.offset(offset).limit(limit)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
events = []
|
|
134
|
+
for event in result.scalars():
|
|
135
|
+
events.append(ActivityEventResponse(
|
|
136
|
+
id=str(event.id),
|
|
137
|
+
event_type=event.event_type,
|
|
138
|
+
org_id=str(event.org_id),
|
|
139
|
+
repo_id=str(event.repo_id) if event.repo_id else None,
|
|
140
|
+
user_id=str(event.user_id) if event.user_id else None,
|
|
141
|
+
detail=event.detail or {},
|
|
142
|
+
created_at=event.created_at.isoformat(),
|
|
143
|
+
))
|
|
144
|
+
|
|
145
|
+
return ActivityFeedResponse(
|
|
146
|
+
events=events, total=total, limit=limit, offset=offset,
|
|
147
|
+
has_more=(offset + limit < total),
|
|
148
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""ADR endpoints — text-oriented parity for MCP architecture-decision tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, Query, Request
|
|
6
|
+
|
|
7
|
+
from attocode_intel.api.auth import verify_auth
|
|
8
|
+
from attocode_intel.api.knowledge_bridge import shared_operation
|
|
9
|
+
from attocode_intel.api.deps import BranchParam, ensure_branch_supported, get_service_or_404
|
|
10
|
+
from attocode_intel.api.models import RecordADRRequest, TextResult, UpdateADRStatusRequest
|
|
11
|
+
|
|
12
|
+
router_v1 = APIRouter(
|
|
13
|
+
prefix="/api/v1/projects/{project_id}/adrs",
|
|
14
|
+
tags=["adrs"],
|
|
15
|
+
dependencies=[Depends(verify_auth)],
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router_v1.post("", response_model=TextResult)
|
|
20
|
+
async def record_adr_v1(
|
|
21
|
+
request: Request,
|
|
22
|
+
project_id: str,
|
|
23
|
+
req: RecordADRRequest,
|
|
24
|
+
branch: BranchParam = "",
|
|
25
|
+
) -> TextResult:
|
|
26
|
+
"""Record a new architecture decision."""
|
|
27
|
+
shared = await shared_operation(request, project_id, "record_adr", req.model_dump())
|
|
28
|
+
if shared is not None:
|
|
29
|
+
return TextResult(result=shared["result"])
|
|
30
|
+
ensure_branch_supported(branch)
|
|
31
|
+
svc = await get_service_or_404(project_id)
|
|
32
|
+
return TextResult(
|
|
33
|
+
result=svc.record_adr(
|
|
34
|
+
title=req.title,
|
|
35
|
+
context=req.context,
|
|
36
|
+
decision=req.decision,
|
|
37
|
+
consequences=req.consequences,
|
|
38
|
+
related_files=req.related_files,
|
|
39
|
+
tags=req.tags,
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@router_v1.get("", response_model=TextResult)
|
|
45
|
+
async def list_adrs_v1(
|
|
46
|
+
request: Request,
|
|
47
|
+
project_id: str,
|
|
48
|
+
branch: BranchParam = "",
|
|
49
|
+
status: str = "",
|
|
50
|
+
tag: str = "",
|
|
51
|
+
search: str = Query("", description="Search title, context, and decision"),
|
|
52
|
+
) -> TextResult:
|
|
53
|
+
"""List ADRs with optional filtering."""
|
|
54
|
+
shared = await shared_operation(
|
|
55
|
+
request, project_id, "list_adrs", {"status": status, "tag": tag, "search": search}
|
|
56
|
+
)
|
|
57
|
+
if shared is not None:
|
|
58
|
+
return TextResult(result=shared["result"])
|
|
59
|
+
ensure_branch_supported(branch)
|
|
60
|
+
svc = await get_service_or_404(project_id)
|
|
61
|
+
return TextResult(result=svc.list_adrs(status=status, tag=tag, search=search))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router_v1.get("/{number}", response_model=TextResult)
|
|
65
|
+
async def get_adr_v1(
|
|
66
|
+
request: Request,
|
|
67
|
+
project_id: str,
|
|
68
|
+
number: int,
|
|
69
|
+
branch: BranchParam = "",
|
|
70
|
+
) -> TextResult:
|
|
71
|
+
"""Fetch a single ADR."""
|
|
72
|
+
shared = await shared_operation(request, project_id, "get_adr", {"number": number})
|
|
73
|
+
if shared is not None:
|
|
74
|
+
return TextResult(result=shared["result"])
|
|
75
|
+
ensure_branch_supported(branch)
|
|
76
|
+
svc = await get_service_or_404(project_id)
|
|
77
|
+
return TextResult(result=svc.get_adr(number))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@router_v1.post("/{number}/status", response_model=TextResult)
|
|
81
|
+
async def update_adr_status_v1(
|
|
82
|
+
request: Request,
|
|
83
|
+
project_id: str,
|
|
84
|
+
number: int,
|
|
85
|
+
req: UpdateADRStatusRequest,
|
|
86
|
+
branch: BranchParam = "",
|
|
87
|
+
) -> TextResult:
|
|
88
|
+
"""Update ADR lifecycle state."""
|
|
89
|
+
shared = await shared_operation(
|
|
90
|
+
request, project_id, "update_adr_status", {"number": number, **req.model_dump()}
|
|
91
|
+
)
|
|
92
|
+
if shared is not None:
|
|
93
|
+
return TextResult(result=shared["result"])
|
|
94
|
+
ensure_branch_supported(branch)
|
|
95
|
+
svc = await get_service_or_404(project_id)
|
|
96
|
+
return TextResult(
|
|
97
|
+
result=svc.update_adr_status(
|
|
98
|
+
number=number,
|
|
99
|
+
status=req.status,
|
|
100
|
+
superseded_by=req.superseded_by,
|
|
101
|
+
)
|
|
102
|
+
)
|