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,269 @@
|
|
|
1
|
+
"""Repository-scoped durable knowledge shared by MCP and HTTP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String, Text, Uuid, cast, or_, select
|
|
12
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
13
|
+
|
|
14
|
+
from attocode_intel.db.base import Base
|
|
15
|
+
|
|
16
|
+
KNOWLEDGE_TOOLS = frozenset(
|
|
17
|
+
{
|
|
18
|
+
"recall",
|
|
19
|
+
"record_learning",
|
|
20
|
+
"list_learnings",
|
|
21
|
+
"learning_feedback",
|
|
22
|
+
"record_adr",
|
|
23
|
+
"list_adrs",
|
|
24
|
+
"get_adr",
|
|
25
|
+
"update_adr_status",
|
|
26
|
+
"update_learning",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class KnowledgeEntry(Base):
|
|
32
|
+
__tablename__ = "knowledge_entries"
|
|
33
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
34
|
+
repo_id: Mapped[uuid.UUID] = mapped_column(
|
|
35
|
+
Uuid, ForeignKey("repositories.id", ondelete="CASCADE"), index=True
|
|
36
|
+
)
|
|
37
|
+
author_id: Mapped[str] = mapped_column(String(64), default="")
|
|
38
|
+
kind: Mapped[str] = mapped_column(String(16), default="learning")
|
|
39
|
+
status: Mapped[str] = mapped_column(String(16), default="active")
|
|
40
|
+
scope: Mapped[str] = mapped_column(Text, default="")
|
|
41
|
+
payload: Mapped[dict] = mapped_column(JSON, default=dict)
|
|
42
|
+
revision: Mapped[str] = mapped_column(String(64), default="")
|
|
43
|
+
anchor_hash: Mapped[str] = mapped_column(String(64), default="")
|
|
44
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
45
|
+
DateTime(timezone=True), default=lambda: datetime.now(UTC)
|
|
46
|
+
)
|
|
47
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
48
|
+
DateTime(timezone=True), default=lambda: datetime.now(UTC)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def anchor_hash(root: str, scope: str) -> str:
|
|
53
|
+
if not scope:
|
|
54
|
+
return ""
|
|
55
|
+
base = Path(root).resolve()
|
|
56
|
+
path = (base / scope).resolve()
|
|
57
|
+
if not path.is_relative_to(base):
|
|
58
|
+
raise ValueError("Knowledge scope must remain inside the selected repository")
|
|
59
|
+
if not path.is_file():
|
|
60
|
+
return ""
|
|
61
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PostgresKnowledge:
|
|
65
|
+
"""Session-injected store; every lookup includes the authorized repository ID."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, session, context):
|
|
68
|
+
self.session = session
|
|
69
|
+
self.context = context
|
|
70
|
+
self.repo_id = uuid.UUID(context.workspace)
|
|
71
|
+
|
|
72
|
+
def statement(self):
|
|
73
|
+
return select(KnowledgeEntry).where(KnowledgeEntry.repo_id == self.repo_id)
|
|
74
|
+
|
|
75
|
+
def serialize(self, entry):
|
|
76
|
+
current_hash = anchor_hash(self.context.project_dir, entry.scope)
|
|
77
|
+
anchors = entry.payload.get("file_anchors", {})
|
|
78
|
+
stale = bool(entry.anchor_hash and current_hash != entry.anchor_hash) or any(
|
|
79
|
+
anchor_hash(self.context.project_dir, path) != digest
|
|
80
|
+
for path, digest in anchors.items()
|
|
81
|
+
)
|
|
82
|
+
origin_anchor = entry.payload.get("anchor_blob_oid", "")
|
|
83
|
+
if origin_anchor and entry.scope:
|
|
84
|
+
target = Path(self.context.project_dir) / entry.scope
|
|
85
|
+
if not target.is_file():
|
|
86
|
+
stale = True
|
|
87
|
+
elif origin_anchor.startswith("sha256:"):
|
|
88
|
+
stale = stale or origin_anchor != "sha256:" + current_hash
|
|
89
|
+
elif origin_anchor.startswith("git:"):
|
|
90
|
+
data = target.read_bytes()
|
|
91
|
+
stale = (
|
|
92
|
+
stale
|
|
93
|
+
or origin_anchor
|
|
94
|
+
!= "git:"
|
|
95
|
+
+ hashlib.sha1(b"blob " + str(len(data)).encode() + b"\0" + data).hexdigest()
|
|
96
|
+
)
|
|
97
|
+
return {
|
|
98
|
+
**entry.payload,
|
|
99
|
+
"id": entry.id,
|
|
100
|
+
"number": entry.id,
|
|
101
|
+
"kind": entry.kind,
|
|
102
|
+
"status": entry.status,
|
|
103
|
+
"scope": entry.scope,
|
|
104
|
+
"author_id": entry.author_id,
|
|
105
|
+
"created_at": entry.created_at.isoformat(),
|
|
106
|
+
"updated_at": entry.updated_at.isoformat(),
|
|
107
|
+
"revision": entry.revision,
|
|
108
|
+
"stale": stale,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async def execute(self, operation: str, arguments: dict):
|
|
112
|
+
args = dict(arguments)
|
|
113
|
+
if operation in {"record_learning", "record_adr"}:
|
|
114
|
+
kind = "adr" if operation == "record_adr" else "learning"
|
|
115
|
+
description = args.get("description") if kind == "learning" else args.get("title")
|
|
116
|
+
if not description or not description.strip():
|
|
117
|
+
raise ValueError("A non-empty description or title is required")
|
|
118
|
+
if kind == "learning":
|
|
119
|
+
from attocode_intel._internal.integrations.context.memory_store import VALID_TYPES
|
|
120
|
+
|
|
121
|
+
if args.get("type") not in VALID_TYPES:
|
|
122
|
+
raise ValueError(f"type must be one of {sorted(VALID_TYPES)}")
|
|
123
|
+
confidence = float(args.get("confidence", 0.7))
|
|
124
|
+
if not 0 <= confidence <= 1:
|
|
125
|
+
raise ValueError("confidence must be between zero and one")
|
|
126
|
+
scope = args.pop("scope", "")
|
|
127
|
+
if kind == "adr":
|
|
128
|
+
args["file_anchors"] = {
|
|
129
|
+
path: anchor_hash(self.context.project_dir, path)
|
|
130
|
+
for path in args.get("related_files") or []
|
|
131
|
+
}
|
|
132
|
+
entry = KnowledgeEntry(
|
|
133
|
+
repo_id=self.repo_id,
|
|
134
|
+
kind=kind,
|
|
135
|
+
payload=args,
|
|
136
|
+
scope=scope,
|
|
137
|
+
author_id=str(self.context.identity.user_id or "service"),
|
|
138
|
+
status="proposed" if kind == "adr" else "active",
|
|
139
|
+
revision=self.context.revision,
|
|
140
|
+
anchor_hash=anchor_hash(self.context.project_dir, scope),
|
|
141
|
+
)
|
|
142
|
+
self.session.add(entry)
|
|
143
|
+
await self.session.flush()
|
|
144
|
+
await self.session.refresh(entry)
|
|
145
|
+
return self.serialize(entry)
|
|
146
|
+
if operation in {"learning_feedback", "update_adr_status", "get_adr", "update_learning"}:
|
|
147
|
+
number = args.get("learning_id", args.get("number", args.get("adr_number")))
|
|
148
|
+
stmt = self.statement().where(KnowledgeEntry.id == number)
|
|
149
|
+
if operation != "get_adr":
|
|
150
|
+
stmt = stmt.with_for_update()
|
|
151
|
+
entry = (await self.session.execute(stmt)).scalar_one_or_none()
|
|
152
|
+
if entry is None:
|
|
153
|
+
raise ValueError("Knowledge entry not found in this repository")
|
|
154
|
+
expected = "adr" if operation in {"get_adr", "update_adr_status"} else "learning"
|
|
155
|
+
if entry.kind != expected:
|
|
156
|
+
raise ValueError(f"Entry is not a {expected}")
|
|
157
|
+
if operation == "learning_feedback":
|
|
158
|
+
payload = dict(entry.payload)
|
|
159
|
+
key = "helpful_count" if args.get("helpful") else "unhelpful_count"
|
|
160
|
+
payload[key] = payload.get(key, 0) + 1
|
|
161
|
+
payload["confidence"] = max(
|
|
162
|
+
0.1,
|
|
163
|
+
min(
|
|
164
|
+
1.0,
|
|
165
|
+
payload.get("confidence", 0.7) + (0.05 if args.get("helpful") else -0.1),
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
if payload.get("unhelpful_count", 0) >= 5 and payload["confidence"] < 0.15:
|
|
169
|
+
entry.status = "archived"
|
|
170
|
+
entry.payload = payload
|
|
171
|
+
elif operation == "update_learning":
|
|
172
|
+
if entry.kind != "learning":
|
|
173
|
+
raise ValueError("Entry is not a learning")
|
|
174
|
+
payload = dict(entry.payload)
|
|
175
|
+
for key in ("description", "details", "confidence"):
|
|
176
|
+
if key in args:
|
|
177
|
+
payload[key] = args[key]
|
|
178
|
+
if (
|
|
179
|
+
not payload.get("description", "").strip()
|
|
180
|
+
or not 0 <= payload.get("confidence", 0.7) <= 1
|
|
181
|
+
):
|
|
182
|
+
raise ValueError(
|
|
183
|
+
"A non-empty description and confidence between zero and one are required"
|
|
184
|
+
)
|
|
185
|
+
status = args.get("status", entry.status)
|
|
186
|
+
if status not in {"active", "archived"}:
|
|
187
|
+
raise ValueError("Learning status must be active or archived")
|
|
188
|
+
entry.status, entry.payload = status, payload
|
|
189
|
+
elif operation == "update_adr_status":
|
|
190
|
+
from attocode_intel.tools.adr_tools import _STATUS_TRANSITIONS
|
|
191
|
+
|
|
192
|
+
status = args.get("new_status", args.get("status"))
|
|
193
|
+
if status not in _STATUS_TRANSITIONS.get(entry.status, set()):
|
|
194
|
+
raise ValueError("Invalid ADR status transition")
|
|
195
|
+
replacement = args.get("superseded_by")
|
|
196
|
+
if status == "superseded" and replacement is None:
|
|
197
|
+
raise ValueError("superseded_by is required")
|
|
198
|
+
if replacement is not None:
|
|
199
|
+
target = (
|
|
200
|
+
await self.session.execute(
|
|
201
|
+
self.statement().where(
|
|
202
|
+
KnowledgeEntry.id == replacement, KnowledgeEntry.kind == "adr"
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
).scalar_one_or_none()
|
|
206
|
+
if target is None or target.id == entry.id:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
"Superseding ADR must exist in this repository and differ from this ADR"
|
|
209
|
+
)
|
|
210
|
+
entry.status = status
|
|
211
|
+
entry.payload = {**entry.payload, "superseded_by": args.get("superseded_by")}
|
|
212
|
+
if operation != "get_adr":
|
|
213
|
+
entry.updated_at = datetime.now(UTC)
|
|
214
|
+
await self.session.flush()
|
|
215
|
+
return self.serialize(entry)
|
|
216
|
+
kind = "adr" if operation == "list_adrs" else "learning"
|
|
217
|
+
stmt = self.statement().where(KnowledgeEntry.kind == kind)
|
|
218
|
+
status = args.get("status", "active" if kind == "learning" else "")
|
|
219
|
+
if status:
|
|
220
|
+
stmt = stmt.where(KnowledgeEntry.status == status)
|
|
221
|
+
query = args.get("query", args.get("search", "")).lower().split()
|
|
222
|
+
if query:
|
|
223
|
+
stmt = stmt.where(
|
|
224
|
+
or_(
|
|
225
|
+
*(
|
|
226
|
+
cast(KnowledgeEntry.payload, Text).icontains(term, autoescape=True)
|
|
227
|
+
for term in query[:20]
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
entries = (
|
|
232
|
+
(
|
|
233
|
+
await self.session.execute(
|
|
234
|
+
stmt.order_by(KnowledgeEntry.updated_at.desc()).limit(1000)
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
.scalars()
|
|
238
|
+
.all()
|
|
239
|
+
)
|
|
240
|
+
scope = args.get("scope", "").rstrip("/")
|
|
241
|
+
rows = [
|
|
242
|
+
self.serialize(e)
|
|
243
|
+
for e in entries
|
|
244
|
+
if not scope or not e.scope or e.scope == scope or e.scope.startswith(scope + "/")
|
|
245
|
+
]
|
|
246
|
+
if args.get("type"):
|
|
247
|
+
rows = [r for r in rows if r.get("type") == args["type"]]
|
|
248
|
+
if args.get("tag"):
|
|
249
|
+
rows = [r for r in rows if args["tag"] in r.get("tags", [])]
|
|
250
|
+
if query:
|
|
251
|
+
rows.sort(
|
|
252
|
+
key=lambda r: (
|
|
253
|
+
sum(term in json.dumps(r).lower() for term in query),
|
|
254
|
+
not r["stale"],
|
|
255
|
+
r.get("confidence", 0.7),
|
|
256
|
+
),
|
|
257
|
+
reverse=True,
|
|
258
|
+
)
|
|
259
|
+
return rows[: min(int(args.get("max_results", 100)), 1000)]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def execute_knowledge(context, operation, arguments):
|
|
263
|
+
from attocode_intel.db.engine import get_session
|
|
264
|
+
|
|
265
|
+
async for session in get_session():
|
|
266
|
+
result = await PostgresKnowledge(session, context).execute(operation, arguments)
|
|
267
|
+
await session.commit()
|
|
268
|
+
return result
|
|
269
|
+
raise RuntimeError("Knowledge database unavailable")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Structured adapter over the existing local SQLite stores."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
LEARNING_TOOLS = {
|
|
9
|
+
"recall",
|
|
10
|
+
"record_learning",
|
|
11
|
+
"list_learnings",
|
|
12
|
+
"learning_feedback",
|
|
13
|
+
"update_learning",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def execute_local_knowledge(context, operation, arguments):
|
|
18
|
+
args = dict(arguments)
|
|
19
|
+
store = context.service._get_memory_store()
|
|
20
|
+
|
|
21
|
+
def rows(status="active", kind=None):
|
|
22
|
+
result = store.list_all(status=status, type=kind)
|
|
23
|
+
for row in result:
|
|
24
|
+
row.update(status=status, author_id="local", revision="working-tree", stale=False)
|
|
25
|
+
anchor, scope = row.get("anchor_blob_oid", ""), row.get("scope", "")
|
|
26
|
+
if anchor and scope:
|
|
27
|
+
path = Path(context.project_dir) / scope
|
|
28
|
+
if not path.is_file():
|
|
29
|
+
row["stale"] = True
|
|
30
|
+
else:
|
|
31
|
+
data = path.read_bytes()
|
|
32
|
+
current = (
|
|
33
|
+
"git:"
|
|
34
|
+
+ hashlib.sha1(
|
|
35
|
+
b"blob " + str(len(data)).encode() + b"\0" + data
|
|
36
|
+
).hexdigest()
|
|
37
|
+
if anchor.startswith("git:")
|
|
38
|
+
else "sha256:" + hashlib.sha256(data).hexdigest()
|
|
39
|
+
)
|
|
40
|
+
row["stale"] = current != anchor
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
if operation == "record_learning":
|
|
44
|
+
if not args["description"].strip():
|
|
45
|
+
raise ValueError("A non-empty description is required")
|
|
46
|
+
if not 0 <= args.get("confidence", 0.7) <= 1:
|
|
47
|
+
raise ValueError("confidence must be between zero and one")
|
|
48
|
+
path = (Path(context.project_dir) / args.get("scope", "")).resolve()
|
|
49
|
+
if not path.is_relative_to(Path(context.project_dir)):
|
|
50
|
+
raise ValueError("Knowledge scope must remain inside the selected repository")
|
|
51
|
+
if path.is_file() and not args.get("anchor_blob_oid"):
|
|
52
|
+
args["anchor_blob_oid"] = "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest()
|
|
53
|
+
number = store.add(**args)
|
|
54
|
+
return next(row for row in rows() if row["id"] == number)
|
|
55
|
+
if operation in {"update_learning", "learning_feedback"}:
|
|
56
|
+
number = args.pop("learning_id")
|
|
57
|
+
existing = next((r for r in rows() + rows("archived") if r["id"] == number), None)
|
|
58
|
+
if existing is None:
|
|
59
|
+
raise ValueError("Learning not found")
|
|
60
|
+
if operation == "update_learning":
|
|
61
|
+
if "description" in args and not args["description"].strip():
|
|
62
|
+
raise ValueError("A non-empty description is required")
|
|
63
|
+
store.update(number, **args)
|
|
64
|
+
else:
|
|
65
|
+
store.record_feedback(number, args["helpful"])
|
|
66
|
+
return next(r for r in rows() + rows("archived") if r["id"] == number)
|
|
67
|
+
if operation == "recall":
|
|
68
|
+
matched = store.recall(**args)
|
|
69
|
+
by_id = {row["id"]: row for row in rows()}
|
|
70
|
+
return [by_id[r["id"]] for r in matched if r["id"] in by_id]
|
|
71
|
+
result = rows(args.get("status", "active"), args.get("type") or None)
|
|
72
|
+
scope = args.get("scope", "").rstrip("/")
|
|
73
|
+
return [
|
|
74
|
+
r
|
|
75
|
+
for r in result
|
|
76
|
+
if not scope or not r["scope"] or r["scope"] == scope or r["scope"].startswith(scope + "/")
|
|
77
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[alembic]
|
|
2
|
+
script_location = %(here)s
|
|
3
|
+
sqlalchemy.url = postgresql+asyncpg://localhost/code_intel
|
|
4
|
+
|
|
5
|
+
[loggers]
|
|
6
|
+
keys = root,sqlalchemy,alembic
|
|
7
|
+
|
|
8
|
+
[handlers]
|
|
9
|
+
keys = console
|
|
10
|
+
|
|
11
|
+
[formatters]
|
|
12
|
+
keys = generic
|
|
13
|
+
|
|
14
|
+
[logger_root]
|
|
15
|
+
level = WARN
|
|
16
|
+
handlers = console
|
|
17
|
+
|
|
18
|
+
[logger_sqlalchemy]
|
|
19
|
+
level = WARN
|
|
20
|
+
handlers =
|
|
21
|
+
qualname = sqlalchemy.engine
|
|
22
|
+
|
|
23
|
+
[logger_alembic]
|
|
24
|
+
level = INFO
|
|
25
|
+
handlers =
|
|
26
|
+
qualname = alembic
|
|
27
|
+
|
|
28
|
+
[handler_console]
|
|
29
|
+
class = StreamHandler
|
|
30
|
+
args = (sys.stderr,)
|
|
31
|
+
level = NOTSET
|
|
32
|
+
formatter = generic
|
|
33
|
+
|
|
34
|
+
[formatter_generic]
|
|
35
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
36
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Alembic async migration environment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
from logging.config import fileConfig
|
|
8
|
+
|
|
9
|
+
from alembic import context
|
|
10
|
+
from sqlalchemy import pool
|
|
11
|
+
from sqlalchemy.ext.asyncio import async_engine_from_config
|
|
12
|
+
|
|
13
|
+
from attocode_intel.db.base import Base
|
|
14
|
+
|
|
15
|
+
# Import all models so metadata is populated
|
|
16
|
+
import attocode_intel.db.models # noqa: F401
|
|
17
|
+
import attocode_intel.knowledge # noqa: F401
|
|
18
|
+
|
|
19
|
+
config = context.config
|
|
20
|
+
|
|
21
|
+
if config.config_file_name is not None:
|
|
22
|
+
fileConfig(config.config_file_name)
|
|
23
|
+
|
|
24
|
+
target_metadata = Base.metadata
|
|
25
|
+
|
|
26
|
+
# Allow DATABASE_URL env override
|
|
27
|
+
database_url = os.environ.get("DATABASE_URL", "")
|
|
28
|
+
if database_url:
|
|
29
|
+
config.set_main_option("sqlalchemy.url", database_url)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_migrations_offline() -> None:
|
|
33
|
+
"""Run migrations in 'offline' mode — generates SQL script."""
|
|
34
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
35
|
+
context.configure(
|
|
36
|
+
url=url,
|
|
37
|
+
target_metadata=target_metadata,
|
|
38
|
+
literal_binds=True,
|
|
39
|
+
dialect_opts={"paramstyle": "named"},
|
|
40
|
+
)
|
|
41
|
+
with context.begin_transaction():
|
|
42
|
+
context.run_migrations()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def do_run_migrations(connection) -> None:
|
|
46
|
+
context.configure(connection=connection, target_metadata=target_metadata)
|
|
47
|
+
with context.begin_transaction():
|
|
48
|
+
context.run_migrations()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def run_async_migrations() -> None:
|
|
52
|
+
"""Run migrations in 'online' mode with async engine."""
|
|
53
|
+
connectable = async_engine_from_config(
|
|
54
|
+
config.get_section(config.config_ini_section, {}),
|
|
55
|
+
prefix="sqlalchemy.",
|
|
56
|
+
poolclass=pool.NullPool,
|
|
57
|
+
)
|
|
58
|
+
async with connectable.connect() as connection:
|
|
59
|
+
await connection.run_sync(do_run_migrations)
|
|
60
|
+
await connectable.dispose()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_migrations_online() -> None:
|
|
64
|
+
"""Entry point for online migrations."""
|
|
65
|
+
asyncio.run(run_async_migrations())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if context.is_offline_mode():
|
|
69
|
+
run_migrations_offline()
|
|
70
|
+
else:
|
|
71
|
+
run_migrations_online()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
import sqlalchemy as sa
|
|
13
|
+
${imports if imports else ""}
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = ${repr(up_revision)}
|
|
17
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
${upgrades if upgrades else "pass"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade() -> None:
|
|
27
|
+
${downgrades if downgrades else "pass"}
|