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,322 @@
|
|
|
1
|
+
"""Garbage-collection endpoints for code-intel storage.
|
|
2
|
+
|
|
3
|
+
Phase 3a wires the existing :func:`ContentStore.gc_unreferenced` and
|
|
4
|
+
:func:`EmbeddingStore.gc_orphaned` functions up to HTTP. Those helpers
|
|
5
|
+
have existed since Phase 1 but were never exposed — Phase 2a described
|
|
6
|
+
this as gap G9 ("GC is code without a UI"). Now there's a UI.
|
|
7
|
+
|
|
8
|
+
The semantics match the local-mode ``gc_preview`` / ``gc_run`` pair:
|
|
9
|
+
|
|
10
|
+
- ``dry_run=true`` (the default) reports what would be removed without
|
|
11
|
+
mutating anything.
|
|
12
|
+
- ``dry_run=false`` actually applies the GC. Requires the admin role.
|
|
13
|
+
|
|
14
|
+
Phase 3b will add retention policies that schedule GC automatically.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import uuid
|
|
21
|
+
|
|
22
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
23
|
+
from pydantic import BaseModel
|
|
24
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
25
|
+
|
|
26
|
+
from attocode_intel.api.auth import resolve_auth
|
|
27
|
+
from attocode_intel.api.auth.context import AuthContext
|
|
28
|
+
from attocode_intel.api.deps import get_db_session
|
|
29
|
+
from attocode_intel.api.routes.orgs import _require_membership
|
|
30
|
+
from attocode_intel.api.utils import get_repo_by_org as _load_repo
|
|
31
|
+
from attocode_intel.storage.content_store import ContentStore
|
|
32
|
+
from attocode_intel.storage.embedding_store import EmbeddingStore
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
router = APIRouter(prefix="/api/v1/orgs", tags=["gc"])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --- Request / response models ---
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GCRunRequest(BaseModel):
|
|
43
|
+
"""Body for the ``POST .../gc`` endpoint.
|
|
44
|
+
|
|
45
|
+
``types`` selects which stores to GC. Empty list means "all".
|
|
46
|
+
``min_age_minutes`` gates how fresh an entity can be before it's
|
|
47
|
+
eligible — defaults match the underlying ``gc_unreferenced`` +
|
|
48
|
+
``gc_orphaned`` defaults (5min for content, 60min for embeddings).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
dry_run: bool = True
|
|
52
|
+
types: list[str] = []
|
|
53
|
+
min_age_minutes: int = 5
|
|
54
|
+
embedding_min_age_minutes: int = 60
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GCTypeResult(BaseModel):
|
|
58
|
+
kind: str
|
|
59
|
+
removed: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class GCRunResponse(BaseModel):
|
|
63
|
+
repo_id: str
|
|
64
|
+
dry_run: bool
|
|
65
|
+
results: list[GCTypeResult]
|
|
66
|
+
removed_total: int
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GCStatsResponse(BaseModel):
|
|
70
|
+
repo_id: str
|
|
71
|
+
types: dict[str, int]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# --- Helpers ---
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _parse_types(types: list[str]) -> set[str]:
|
|
78
|
+
"""Normalize the ``types`` input, expanding aliases. Empty input
|
|
79
|
+
means every known type."""
|
|
80
|
+
allowed = {"content", "embedding"}
|
|
81
|
+
if not types:
|
|
82
|
+
return set(allowed)
|
|
83
|
+
out: set[str] = set()
|
|
84
|
+
for t in types:
|
|
85
|
+
key = t.strip().lower()
|
|
86
|
+
if key not in allowed:
|
|
87
|
+
raise HTTPException(
|
|
88
|
+
status_code=422,
|
|
89
|
+
detail=f"unknown gc type {key!r}; allowed: {sorted(allowed)}",
|
|
90
|
+
)
|
|
91
|
+
out.add(key)
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# --- Endpoints ---
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@router.post(
|
|
99
|
+
"/{org_id}/repos/{repo_id}/gc",
|
|
100
|
+
response_model=GCRunResponse,
|
|
101
|
+
)
|
|
102
|
+
async def gc_run(
|
|
103
|
+
org_id: uuid.UUID,
|
|
104
|
+
repo_id: uuid.UUID,
|
|
105
|
+
req: GCRunRequest,
|
|
106
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
107
|
+
session: AsyncSession = Depends(get_db_session),
|
|
108
|
+
) -> GCRunResponse:
|
|
109
|
+
"""Run garbage collection on the repo's code-intel storage.
|
|
110
|
+
|
|
111
|
+
``dry_run=true`` is member-readable. Applying GC
|
|
112
|
+
(``dry_run=false``) requires the admin role — destructive ops
|
|
113
|
+
always do.
|
|
114
|
+
"""
|
|
115
|
+
min_role = "member" if req.dry_run else "admin"
|
|
116
|
+
await _require_membership(org_id, auth, session, min_role=min_role)
|
|
117
|
+
await _load_repo(org_id, repo_id, session)
|
|
118
|
+
|
|
119
|
+
selected = _parse_types(req.types)
|
|
120
|
+
content_store = ContentStore(session)
|
|
121
|
+
embedding_store = EmbeddingStore(session)
|
|
122
|
+
|
|
123
|
+
results: list[GCTypeResult] = []
|
|
124
|
+
|
|
125
|
+
if "content" in selected:
|
|
126
|
+
if req.dry_run:
|
|
127
|
+
removed = await _preview_content_gc(
|
|
128
|
+
session,
|
|
129
|
+
min_age_minutes=req.min_age_minutes,
|
|
130
|
+
repo_id=repo_id,
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
removed = await content_store.gc_unreferenced(
|
|
134
|
+
min_age_minutes=req.min_age_minutes,
|
|
135
|
+
repo_id=repo_id,
|
|
136
|
+
)
|
|
137
|
+
results.append(GCTypeResult(kind="content", removed=removed))
|
|
138
|
+
|
|
139
|
+
if "embedding" in selected:
|
|
140
|
+
if req.dry_run:
|
|
141
|
+
removed = await _preview_embedding_gc(
|
|
142
|
+
session,
|
|
143
|
+
min_age_minutes=req.embedding_min_age_minutes,
|
|
144
|
+
repo_id=repo_id,
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
removed = await embedding_store.gc_orphaned(
|
|
148
|
+
min_age_minutes=req.embedding_min_age_minutes,
|
|
149
|
+
repo_id=repo_id,
|
|
150
|
+
)
|
|
151
|
+
results.append(GCTypeResult(kind="embedding", removed=removed))
|
|
152
|
+
|
|
153
|
+
if not req.dry_run:
|
|
154
|
+
from attocode_intel.audit import log_event
|
|
155
|
+
|
|
156
|
+
await log_event(
|
|
157
|
+
session,
|
|
158
|
+
org_id,
|
|
159
|
+
"gc.run",
|
|
160
|
+
repo_id=repo_id,
|
|
161
|
+
user_id=auth.user_id,
|
|
162
|
+
detail={
|
|
163
|
+
"types": sorted(selected),
|
|
164
|
+
"removed": {r.kind: r.removed for r in results},
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
await session.commit()
|
|
168
|
+
else:
|
|
169
|
+
# Roll back any accidental writes the preview may have caused —
|
|
170
|
+
# it shouldn't, but defensive.
|
|
171
|
+
await session.rollback()
|
|
172
|
+
|
|
173
|
+
return GCRunResponse(
|
|
174
|
+
repo_id=str(repo_id),
|
|
175
|
+
dry_run=req.dry_run,
|
|
176
|
+
results=results,
|
|
177
|
+
removed_total=sum(r.removed for r in results),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@router.get(
|
|
182
|
+
"/{org_id}/repos/{repo_id}/gc/stats",
|
|
183
|
+
response_model=GCStatsResponse,
|
|
184
|
+
)
|
|
185
|
+
async def gc_stats(
|
|
186
|
+
org_id: uuid.UUID,
|
|
187
|
+
repo_id: uuid.UUID,
|
|
188
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
189
|
+
session: AsyncSession = Depends(get_db_session),
|
|
190
|
+
) -> GCStatsResponse:
|
|
191
|
+
"""Return per-type counts of entities eligible for GC.
|
|
192
|
+
|
|
193
|
+
This is a non-destructive ``SELECT COUNT(...)`` that gives you the
|
|
194
|
+
same numbers ``gc_run(dry_run=True)`` would produce, without
|
|
195
|
+
needing the body payload.
|
|
196
|
+
"""
|
|
197
|
+
await _require_membership(org_id, auth, session)
|
|
198
|
+
await _load_repo(org_id, repo_id, session)
|
|
199
|
+
|
|
200
|
+
content_count = await _preview_content_gc(
|
|
201
|
+
session, min_age_minutes=5, repo_id=repo_id,
|
|
202
|
+
)
|
|
203
|
+
embedding_count = await _preview_embedding_gc(
|
|
204
|
+
session, min_age_minutes=60, repo_id=repo_id,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return GCStatsResponse(
|
|
208
|
+
repo_id=str(repo_id),
|
|
209
|
+
types={
|
|
210
|
+
"content": content_count,
|
|
211
|
+
"embedding": embedding_count,
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def _preview_content_gc(
|
|
217
|
+
session: AsyncSession,
|
|
218
|
+
*,
|
|
219
|
+
min_age_minutes: int,
|
|
220
|
+
repo_id: uuid.UUID | None = None,
|
|
221
|
+
) -> int:
|
|
222
|
+
"""Count how many ``file_contents`` rows are GC-eligible.
|
|
223
|
+
|
|
224
|
+
Mirrors :func:`ContentStore.gc_unreferenced` but with a
|
|
225
|
+
``SELECT COUNT(*)`` that does not mutate the session. When
|
|
226
|
+
``repo_id`` is set, the count is the same repo-scoped number the
|
|
227
|
+
apply path would delete.
|
|
228
|
+
"""
|
|
229
|
+
from sqlalchemy import text
|
|
230
|
+
|
|
231
|
+
if repo_id is None:
|
|
232
|
+
result = await session.execute(
|
|
233
|
+
text(
|
|
234
|
+
"""
|
|
235
|
+
SELECT COUNT(*) FROM file_contents
|
|
236
|
+
WHERE created_at < NOW() - make_interval(mins => :age)
|
|
237
|
+
AND sha256 NOT IN (
|
|
238
|
+
SELECT DISTINCT content_sha FROM branch_files WHERE content_sha IS NOT NULL
|
|
239
|
+
UNION
|
|
240
|
+
SELECT DISTINCT content_sha FROM symbols
|
|
241
|
+
UNION
|
|
242
|
+
SELECT DISTINCT source_sha FROM dependencies
|
|
243
|
+
UNION
|
|
244
|
+
SELECT DISTINCT target_sha FROM dependencies
|
|
245
|
+
UNION
|
|
246
|
+
SELECT DISTINCT content_sha FROM embeddings
|
|
247
|
+
)
|
|
248
|
+
"""
|
|
249
|
+
).bindparams(age=min_age_minutes)
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
result = await session.execute(
|
|
253
|
+
text(
|
|
254
|
+
"""
|
|
255
|
+
SELECT COUNT(*) FROM file_contents
|
|
256
|
+
WHERE created_at < NOW() - make_interval(mins => :age)
|
|
257
|
+
AND sha256 IN (
|
|
258
|
+
SELECT DISTINCT bf.content_sha
|
|
259
|
+
FROM branch_files bf
|
|
260
|
+
JOIN branches b ON b.id = bf.branch_id
|
|
261
|
+
WHERE b.repo_id = :repo_id AND bf.content_sha IS NOT NULL
|
|
262
|
+
)
|
|
263
|
+
AND sha256 NOT IN (
|
|
264
|
+
SELECT DISTINCT bf.content_sha
|
|
265
|
+
FROM branch_files bf
|
|
266
|
+
WHERE bf.content_sha IS NOT NULL
|
|
267
|
+
UNION
|
|
268
|
+
SELECT DISTINCT content_sha FROM symbols
|
|
269
|
+
UNION
|
|
270
|
+
SELECT DISTINCT source_sha FROM dependencies
|
|
271
|
+
UNION
|
|
272
|
+
SELECT DISTINCT target_sha FROM dependencies
|
|
273
|
+
UNION
|
|
274
|
+
SELECT DISTINCT content_sha FROM embeddings
|
|
275
|
+
)
|
|
276
|
+
"""
|
|
277
|
+
).bindparams(age=min_age_minutes, repo_id=str(repo_id))
|
|
278
|
+
)
|
|
279
|
+
return int(result.scalar_one() or 0)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
async def _preview_embedding_gc(
|
|
283
|
+
session: AsyncSession,
|
|
284
|
+
*,
|
|
285
|
+
min_age_minutes: int,
|
|
286
|
+
repo_id: uuid.UUID | None = None,
|
|
287
|
+
) -> int:
|
|
288
|
+
"""Count embeddings that would be removed by
|
|
289
|
+
:func:`EmbeddingStore.gc_orphaned`."""
|
|
290
|
+
from sqlalchemy import text
|
|
291
|
+
|
|
292
|
+
if repo_id is None:
|
|
293
|
+
result = await session.execute(
|
|
294
|
+
text(
|
|
295
|
+
"""
|
|
296
|
+
SELECT COUNT(*) FROM embeddings
|
|
297
|
+
WHERE created_at < NOW() - make_interval(mins => :age)
|
|
298
|
+
AND content_sha NOT IN (
|
|
299
|
+
SELECT DISTINCT content_sha FROM branch_files WHERE content_sha IS NOT NULL
|
|
300
|
+
)
|
|
301
|
+
"""
|
|
302
|
+
).bindparams(age=min_age_minutes)
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
result = await session.execute(
|
|
306
|
+
text(
|
|
307
|
+
"""
|
|
308
|
+
SELECT COUNT(*) FROM embeddings
|
|
309
|
+
WHERE created_at < NOW() - make_interval(mins => :age)
|
|
310
|
+
AND content_sha IN (
|
|
311
|
+
SELECT DISTINCT bf.content_sha
|
|
312
|
+
FROM branch_files bf
|
|
313
|
+
JOIN branches b ON b.id = bf.branch_id
|
|
314
|
+
WHERE b.repo_id = :repo_id AND bf.content_sha IS NOT NULL
|
|
315
|
+
)
|
|
316
|
+
AND content_sha NOT IN (
|
|
317
|
+
SELECT DISTINCT content_sha FROM branch_files WHERE content_sha IS NOT NULL
|
|
318
|
+
)
|
|
319
|
+
"""
|
|
320
|
+
).bindparams(age=min_age_minutes, repo_id=str(repo_id))
|
|
321
|
+
)
|
|
322
|
+
return int(result.scalar_one() or 0)
|