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,602 @@
|
|
|
1
|
+
"""SQLite-backed learning store for cross-agent memory.
|
|
2
|
+
|
|
3
|
+
Stores project learnings (patterns, conventions, gotchas, workarounds,
|
|
4
|
+
anti-patterns) with FTS5 full-text search and scope-aware recall.
|
|
5
|
+
Follows the same storage conventions as ``graph_store.py``.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
store = MemoryStore("/path/to/project")
|
|
10
|
+
lid = store.add("convention", "Always use dataclass(slots=True)", scope="src/")
|
|
11
|
+
results = store.recall("dataclass patterns", scope="src/models/")
|
|
12
|
+
store.record_feedback(lid, helpful=True)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import contextlib
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import sqlite3
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
VALID_TYPES = {"pattern", "antipattern", "workaround", "convention", "gotcha"}
|
|
27
|
+
VALID_STATUSES = {"active", "archived"}
|
|
28
|
+
|
|
29
|
+
# FTS rank threshold for deduplication.
|
|
30
|
+
# FTS5 rank is negative; closer to 0 = better match.
|
|
31
|
+
# -1.0 is a very generous threshold (catches near-exact duplicates).
|
|
32
|
+
_DEDUP_RANK_THRESHOLD = -1.0
|
|
33
|
+
|
|
34
|
+
# Confidence thresholds
|
|
35
|
+
_CONFIDENCE_FLOOR = 0.1
|
|
36
|
+
_CONFIDENCE_CAP = 1.0
|
|
37
|
+
_AUTO_ARCHIVE_THRESHOLD = 0.15
|
|
38
|
+
_UNHELPFUL_AUTO_ARCHIVE = 5
|
|
39
|
+
|
|
40
|
+
# Confidence adjustments
|
|
41
|
+
_HELPFUL_BOOST = 0.05
|
|
42
|
+
_UNHELPFUL_REDUCE = 0.1
|
|
43
|
+
_RECALL_DECAY = 0.001
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class MemoryStore:
|
|
48
|
+
"""SQLite-backed persistent store for project learnings.
|
|
49
|
+
|
|
50
|
+
Stores learnings with FTS5 search, scope hierarchy, confidence
|
|
51
|
+
tracking, and automatic deduplication.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
project_dir: str
|
|
55
|
+
db_path: str = field(default="", repr=False)
|
|
56
|
+
_conn: sqlite3.Connection | None = field(default=None, repr=False)
|
|
57
|
+
_last_decay_at: float = field(default=0.0, repr=False)
|
|
58
|
+
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
|
+
if not self.db_path:
|
|
61
|
+
store_dir = os.path.join(self.project_dir, ".attocode", "cache")
|
|
62
|
+
os.makedirs(store_dir, exist_ok=True)
|
|
63
|
+
self.db_path = os.path.join(store_dir, "memory.db")
|
|
64
|
+
|
|
65
|
+
self._conn = sqlite3.connect(self.db_path)
|
|
66
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
67
|
+
self._conn.execute("PRAGMA synchronous=NORMAL")
|
|
68
|
+
self._create_tables()
|
|
69
|
+
|
|
70
|
+
def _get_conn(self) -> sqlite3.Connection:
|
|
71
|
+
"""Return the active connection or raise if closed."""
|
|
72
|
+
if self._conn is None:
|
|
73
|
+
raise RuntimeError("MemoryStore connection is closed")
|
|
74
|
+
return self._conn
|
|
75
|
+
|
|
76
|
+
def _create_tables(self) -> None:
|
|
77
|
+
"""Create tables if they don't exist."""
|
|
78
|
+
conn = self._get_conn()
|
|
79
|
+
conn.executescript("""
|
|
80
|
+
CREATE TABLE IF NOT EXISTS learnings (
|
|
81
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
|
+
type TEXT NOT NULL,
|
|
83
|
+
description TEXT NOT NULL,
|
|
84
|
+
details TEXT NOT NULL DEFAULT '',
|
|
85
|
+
scope TEXT NOT NULL DEFAULT '',
|
|
86
|
+
confidence REAL NOT NULL DEFAULT 0.7,
|
|
87
|
+
apply_count INTEGER NOT NULL DEFAULT 0,
|
|
88
|
+
help_count INTEGER NOT NULL DEFAULT 0,
|
|
89
|
+
unhelpful_count INTEGER NOT NULL DEFAULT 0,
|
|
90
|
+
created_at TEXT NOT NULL,
|
|
91
|
+
updated_at TEXT NOT NULL,
|
|
92
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
93
|
+
anchor_blob_oid TEXT NOT NULL DEFAULT ''
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_learnings_scope ON learnings(scope);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_learnings_status ON learnings(status);
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_learnings_type ON learnings(type);
|
|
99
|
+
""")
|
|
100
|
+
# The anchor index references a column that may not exist yet
|
|
101
|
+
# on pre-2c databases — migrate first, then create the index.
|
|
102
|
+
self._migrate_schema()
|
|
103
|
+
|
|
104
|
+
# FTS5 virtual table — created separately (can't be in executescript
|
|
105
|
+
# with IF NOT EXISTS reliably on all SQLite versions)
|
|
106
|
+
with contextlib.suppress(sqlite3.OperationalError):
|
|
107
|
+
conn.execute("""
|
|
108
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS learnings_fts
|
|
109
|
+
USING fts5(description, details, content=learnings, content_rowid=id)
|
|
110
|
+
""")
|
|
111
|
+
|
|
112
|
+
# Triggers to keep FTS in sync
|
|
113
|
+
for trigger_sql in [
|
|
114
|
+
"""CREATE TRIGGER IF NOT EXISTS learnings_ai AFTER INSERT ON learnings BEGIN
|
|
115
|
+
INSERT INTO learnings_fts(rowid, description, details)
|
|
116
|
+
VALUES (new.id, new.description, new.details);
|
|
117
|
+
END""",
|
|
118
|
+
"""CREATE TRIGGER IF NOT EXISTS learnings_ad AFTER DELETE ON learnings BEGIN
|
|
119
|
+
INSERT INTO learnings_fts(learnings_fts, rowid, description, details)
|
|
120
|
+
VALUES ('delete', old.id, old.description, old.details);
|
|
121
|
+
END""",
|
|
122
|
+
"""CREATE TRIGGER IF NOT EXISTS learnings_au AFTER UPDATE ON learnings BEGIN
|
|
123
|
+
INSERT INTO learnings_fts(learnings_fts, rowid, description, details)
|
|
124
|
+
VALUES ('delete', old.id, old.description, old.details);
|
|
125
|
+
INSERT INTO learnings_fts(rowid, description, details)
|
|
126
|
+
VALUES (new.id, new.description, new.details);
|
|
127
|
+
END""",
|
|
128
|
+
]:
|
|
129
|
+
with contextlib.suppress(sqlite3.OperationalError):
|
|
130
|
+
conn.execute(trigger_sql)
|
|
131
|
+
|
|
132
|
+
conn.commit()
|
|
133
|
+
|
|
134
|
+
def _migrate_schema(self) -> None:
|
|
135
|
+
"""Additive in-place migration for pre-anchor learnings databases.
|
|
136
|
+
|
|
137
|
+
Adds the ``anchor_blob_oid`` column if missing so an existing
|
|
138
|
+
Phase 1 / 2a / 2b store keeps working after the Phase 2c
|
|
139
|
+
upgrade. Idempotent.
|
|
140
|
+
"""
|
|
141
|
+
conn = self._get_conn()
|
|
142
|
+
columns = {
|
|
143
|
+
row[1]
|
|
144
|
+
for row in conn.execute("PRAGMA table_info(learnings)").fetchall()
|
|
145
|
+
}
|
|
146
|
+
if "anchor_blob_oid" not in columns:
|
|
147
|
+
conn.execute(
|
|
148
|
+
"ALTER TABLE learnings ADD COLUMN anchor_blob_oid "
|
|
149
|
+
"TEXT NOT NULL DEFAULT ''"
|
|
150
|
+
)
|
|
151
|
+
logger.info("memory_store: migrated learnings table (+ anchor_blob_oid)")
|
|
152
|
+
# Ensure the new partial index is present regardless of whether
|
|
153
|
+
# we just added the column or it was already there.
|
|
154
|
+
with contextlib.suppress(sqlite3.OperationalError):
|
|
155
|
+
conn.execute(
|
|
156
|
+
"CREATE INDEX IF NOT EXISTS idx_learnings_anchor "
|
|
157
|
+
"ON learnings(anchor_blob_oid) WHERE anchor_blob_oid != ''"
|
|
158
|
+
)
|
|
159
|
+
conn.commit()
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
# Write operations
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def add(
|
|
166
|
+
self,
|
|
167
|
+
type: str, # noqa: A002
|
|
168
|
+
description: str,
|
|
169
|
+
details: str = "",
|
|
170
|
+
scope: str = "",
|
|
171
|
+
confidence: float = 0.7,
|
|
172
|
+
anchor_blob_oid: str = "",
|
|
173
|
+
) -> int:
|
|
174
|
+
"""Record a new learning. Returns learning ID.
|
|
175
|
+
|
|
176
|
+
Deduplicates via FTS similarity — if a highly similar learning
|
|
177
|
+
exists with the same type and scope, updates its confidence instead.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
anchor_blob_oid: Optional content-addressed anchor for the
|
|
181
|
+
file this learning references (e.g. ``"git:abc123"``).
|
|
182
|
+
When set, ``orphan_scan`` can check git reachability to
|
|
183
|
+
detect learnings whose referenced content has been removed
|
|
184
|
+
from the repo. Pass an empty string to fall back to the
|
|
185
|
+
path-based ``scope`` heuristic.
|
|
186
|
+
"""
|
|
187
|
+
conn = self._get_conn()
|
|
188
|
+
|
|
189
|
+
if type not in VALID_TYPES:
|
|
190
|
+
raise ValueError(f"Invalid type '{type}'. Must be one of: {VALID_TYPES}")
|
|
191
|
+
|
|
192
|
+
confidence = max(_CONFIDENCE_FLOOR, min(_CONFIDENCE_CAP, confidence))
|
|
193
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
194
|
+
|
|
195
|
+
# Deduplication: check FTS for similar existing learning
|
|
196
|
+
existing_id = self._find_duplicate(type, description, scope)
|
|
197
|
+
if existing_id is not None:
|
|
198
|
+
# Boost confidence of existing learning. Also backfill the
|
|
199
|
+
# anchor_blob_oid if the new call has one and the existing
|
|
200
|
+
# row doesn't — a learning gains provenance on re-record.
|
|
201
|
+
if anchor_blob_oid:
|
|
202
|
+
conn.execute(
|
|
203
|
+
"""UPDATE learnings
|
|
204
|
+
SET confidence = MIN(?, confidence + 0.05),
|
|
205
|
+
updated_at = ?,
|
|
206
|
+
anchor_blob_oid = CASE
|
|
207
|
+
WHEN anchor_blob_oid = '' THEN ?
|
|
208
|
+
ELSE anchor_blob_oid
|
|
209
|
+
END
|
|
210
|
+
WHERE id = ?""",
|
|
211
|
+
(_CONFIDENCE_CAP, now, anchor_blob_oid, existing_id),
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
conn.execute(
|
|
215
|
+
"""UPDATE learnings
|
|
216
|
+
SET confidence = MIN(?, confidence + 0.05),
|
|
217
|
+
updated_at = ?
|
|
218
|
+
WHERE id = ?""",
|
|
219
|
+
(_CONFIDENCE_CAP, now, existing_id),
|
|
220
|
+
)
|
|
221
|
+
conn.commit()
|
|
222
|
+
return existing_id
|
|
223
|
+
|
|
224
|
+
cursor = conn.execute(
|
|
225
|
+
"""INSERT INTO learnings
|
|
226
|
+
(type, description, details, scope, confidence,
|
|
227
|
+
anchor_blob_oid, created_at, updated_at)
|
|
228
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
229
|
+
(type, description, details, scope, confidence,
|
|
230
|
+
anchor_blob_oid, now, now),
|
|
231
|
+
)
|
|
232
|
+
conn.commit()
|
|
233
|
+
return cursor.lastrowid # type: ignore[return-value]
|
|
234
|
+
|
|
235
|
+
def _find_duplicate(self, type: str, description: str, scope: str) -> int | None: # noqa: A002
|
|
236
|
+
"""Check if a similar learning already exists."""
|
|
237
|
+
conn = self._get_conn()
|
|
238
|
+
try:
|
|
239
|
+
cursor = conn.execute(
|
|
240
|
+
"""SELECT l.id, rank
|
|
241
|
+
FROM learnings_fts f
|
|
242
|
+
JOIN learnings l ON l.id = f.rowid
|
|
243
|
+
WHERE learnings_fts MATCH ?
|
|
244
|
+
AND l.type = ?
|
|
245
|
+
AND l.scope = ?
|
|
246
|
+
AND l.status = 'active'
|
|
247
|
+
ORDER BY rank DESC
|
|
248
|
+
LIMIT 1""",
|
|
249
|
+
(self._escape_fts_query(description), type, scope),
|
|
250
|
+
)
|
|
251
|
+
row = cursor.fetchone()
|
|
252
|
+
if row and row[1] > _DEDUP_RANK_THRESHOLD:
|
|
253
|
+
return row[0]
|
|
254
|
+
except sqlite3.OperationalError:
|
|
255
|
+
pass # FTS match syntax issue — skip dedup
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
def record_feedback(self, learning_id: int, helpful: bool) -> None:
|
|
259
|
+
"""Record feedback on a learning.
|
|
260
|
+
|
|
261
|
+
Helpful: boost confidence by 0.05 (cap 1.0).
|
|
262
|
+
Unhelpful: reduce confidence by 0.1 (floor 0.1).
|
|
263
|
+
Auto-archive if unhelpful_count >= 5 or confidence < 0.15.
|
|
264
|
+
"""
|
|
265
|
+
conn = self._get_conn()
|
|
266
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
267
|
+
|
|
268
|
+
if helpful:
|
|
269
|
+
conn.execute(
|
|
270
|
+
"""UPDATE learnings
|
|
271
|
+
SET help_count = help_count + 1,
|
|
272
|
+
confidence = MIN(?, confidence + ?),
|
|
273
|
+
updated_at = ?
|
|
274
|
+
WHERE id = ?""",
|
|
275
|
+
(_CONFIDENCE_CAP, _HELPFUL_BOOST, now, learning_id),
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
conn.execute(
|
|
279
|
+
"""UPDATE learnings
|
|
280
|
+
SET unhelpful_count = unhelpful_count + 1,
|
|
281
|
+
confidence = MAX(?, confidence - ?),
|
|
282
|
+
updated_at = ?
|
|
283
|
+
WHERE id = ?""",
|
|
284
|
+
(_CONFIDENCE_FLOOR, _UNHELPFUL_REDUCE, now, learning_id),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Auto-archive check
|
|
288
|
+
conn.execute(
|
|
289
|
+
"""UPDATE learnings
|
|
290
|
+
SET status = 'archived', updated_at = ?
|
|
291
|
+
WHERE id = ?
|
|
292
|
+
AND (unhelpful_count >= ? OR confidence < ?)""",
|
|
293
|
+
(now, learning_id, _UNHELPFUL_AUTO_ARCHIVE, _AUTO_ARCHIVE_THRESHOLD),
|
|
294
|
+
)
|
|
295
|
+
conn.commit()
|
|
296
|
+
|
|
297
|
+
def record_applied(self, learning_id: int) -> None:
|
|
298
|
+
"""Increment apply_count (called when learning injected into context)."""
|
|
299
|
+
conn = self._get_conn()
|
|
300
|
+
conn.execute(
|
|
301
|
+
"UPDATE learnings SET apply_count = apply_count + 1 WHERE id = ?",
|
|
302
|
+
(learning_id,),
|
|
303
|
+
)
|
|
304
|
+
conn.commit()
|
|
305
|
+
|
|
306
|
+
def update(self, learning_id: int, **kwargs: str | float) -> None:
|
|
307
|
+
"""Update fields on a learning."""
|
|
308
|
+
conn = self._get_conn()
|
|
309
|
+
allowed = {"type", "description", "details", "scope", "confidence", "status"}
|
|
310
|
+
updates = {k: v for k, v in kwargs.items() if k in allowed}
|
|
311
|
+
if not updates:
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
if "type" in updates and updates["type"] not in VALID_TYPES:
|
|
315
|
+
raise ValueError(f"Invalid type '{updates['type']}'. Must be one of: {VALID_TYPES}")
|
|
316
|
+
if "status" in updates and updates["status"] not in VALID_STATUSES:
|
|
317
|
+
raise ValueError(
|
|
318
|
+
f"Invalid status '{updates['status']}'. Must be one of: {VALID_STATUSES}"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
322
|
+
updates["updated_at"] = now
|
|
323
|
+
|
|
324
|
+
set_clause = ", ".join(f"{k} = ?" for k in updates)
|
|
325
|
+
values = list(updates.values()) + [learning_id]
|
|
326
|
+
conn.execute(
|
|
327
|
+
f"UPDATE learnings SET {set_clause} WHERE id = ?",
|
|
328
|
+
values,
|
|
329
|
+
)
|
|
330
|
+
conn.commit()
|
|
331
|
+
|
|
332
|
+
# ------------------------------------------------------------------
|
|
333
|
+
# Read operations
|
|
334
|
+
# ------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
def recall(
|
|
337
|
+
self,
|
|
338
|
+
query: str,
|
|
339
|
+
scope: str = "",
|
|
340
|
+
max_results: int = 10,
|
|
341
|
+
) -> list[dict]:
|
|
342
|
+
"""Retrieve relevant learnings by FTS + scope filtering.
|
|
343
|
+
|
|
344
|
+
Scope hierarchy: exact match > parent dir > global.
|
|
345
|
+
Results ranked by: FTS relevance x scope_proximity x confidence.
|
|
346
|
+
"""
|
|
347
|
+
conn = self._get_conn()
|
|
348
|
+
|
|
349
|
+
# Apply slow confidence decay to all active learnings
|
|
350
|
+
self._apply_decay()
|
|
351
|
+
|
|
352
|
+
# Build scope candidates (exact → parents → global)
|
|
353
|
+
scope_candidates = self._build_scope_hierarchy(scope)
|
|
354
|
+
|
|
355
|
+
results: list[dict] = []
|
|
356
|
+
seen_ids: set[int] = set()
|
|
357
|
+
|
|
358
|
+
# First: FTS-matched results
|
|
359
|
+
if query.strip():
|
|
360
|
+
try:
|
|
361
|
+
cursor = conn.execute(
|
|
362
|
+
"""SELECT l.id, l.type, l.description, l.details, l.scope,
|
|
363
|
+
l.confidence, l.apply_count, l.help_count,
|
|
364
|
+
l.unhelpful_count, l.created_at, l.updated_at,
|
|
365
|
+
l.anchor_blob_oid, rank
|
|
366
|
+
FROM learnings_fts f
|
|
367
|
+
JOIN learnings l ON l.id = f.rowid
|
|
368
|
+
WHERE learnings_fts MATCH ?
|
|
369
|
+
AND l.status = 'active'
|
|
370
|
+
ORDER BY rank DESC
|
|
371
|
+
LIMIT ?""",
|
|
372
|
+
(self._escape_fts_query(query), max_results * 3),
|
|
373
|
+
)
|
|
374
|
+
for row in cursor:
|
|
375
|
+
entry = self._row_to_dict(row[:12])
|
|
376
|
+
entry["_rank"] = row[12]
|
|
377
|
+
results.append(entry)
|
|
378
|
+
seen_ids.add(entry["id"])
|
|
379
|
+
except sqlite3.OperationalError:
|
|
380
|
+
pass # FTS syntax error — fall back to scope-only
|
|
381
|
+
|
|
382
|
+
# Second: scope-matched results (for when FTS doesn't match but scope does)
|
|
383
|
+
if len(results) < max_results and scope_candidates:
|
|
384
|
+
placeholders = ",".join("?" * len(scope_candidates))
|
|
385
|
+
cursor = conn.execute(
|
|
386
|
+
f"""SELECT id, type, description, details, scope,
|
|
387
|
+
confidence, apply_count, help_count,
|
|
388
|
+
unhelpful_count, created_at, updated_at,
|
|
389
|
+
anchor_blob_oid
|
|
390
|
+
FROM learnings
|
|
391
|
+
WHERE status = 'active'
|
|
392
|
+
AND scope IN ({placeholders})
|
|
393
|
+
ORDER BY confidence DESC
|
|
394
|
+
LIMIT ?""",
|
|
395
|
+
[*scope_candidates, max_results * 2],
|
|
396
|
+
)
|
|
397
|
+
for row in cursor:
|
|
398
|
+
entry = self._row_to_dict(row)
|
|
399
|
+
if entry["id"] not in seen_ids:
|
|
400
|
+
entry["_rank"] = 0.0 # No FTS rank
|
|
401
|
+
results.append(entry)
|
|
402
|
+
seen_ids.add(entry["id"])
|
|
403
|
+
|
|
404
|
+
# Score and sort
|
|
405
|
+
results = self._score_results(results, scope_candidates)
|
|
406
|
+
return results[:max_results]
|
|
407
|
+
|
|
408
|
+
def list_all(
|
|
409
|
+
self,
|
|
410
|
+
status: str = "active",
|
|
411
|
+
type: str | None = None, # noqa: A002
|
|
412
|
+
) -> list[dict]:
|
|
413
|
+
"""List all learnings, optionally filtered by status and type."""
|
|
414
|
+
conn = self._get_conn()
|
|
415
|
+
|
|
416
|
+
query = (
|
|
417
|
+
"SELECT id, type, description, details, scope, confidence,"
|
|
418
|
+
" apply_count, help_count, unhelpful_count, created_at, updated_at,"
|
|
419
|
+
" anchor_blob_oid"
|
|
420
|
+
" FROM learnings WHERE status = ?"
|
|
421
|
+
)
|
|
422
|
+
params: list[str] = [status]
|
|
423
|
+
|
|
424
|
+
if type:
|
|
425
|
+
query += " AND type = ?"
|
|
426
|
+
params.append(type)
|
|
427
|
+
|
|
428
|
+
query += " ORDER BY confidence DESC, updated_at DESC"
|
|
429
|
+
|
|
430
|
+
cursor = conn.execute(query, params)
|
|
431
|
+
return [self._row_to_dict(row) for row in cursor]
|
|
432
|
+
|
|
433
|
+
# ------------------------------------------------------------------
|
|
434
|
+
# Helpers
|
|
435
|
+
# ------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
def _escape_fts_query(self, query: str) -> str:
|
|
438
|
+
"""Escape FTS5 special characters by quoting each term."""
|
|
439
|
+
terms = query.split()
|
|
440
|
+
return " ".join(f'"{t}"' for t in terms if t)
|
|
441
|
+
|
|
442
|
+
def _row_to_dict(self, row: tuple) -> dict:
|
|
443
|
+
"""Convert a row tuple to a dict.
|
|
444
|
+
|
|
445
|
+
Tolerant of the row length — if the caller's SELECT doesn't
|
|
446
|
+
include ``anchor_blob_oid`` (legacy code paths not yet updated),
|
|
447
|
+
the field defaults to an empty string.
|
|
448
|
+
"""
|
|
449
|
+
result = {
|
|
450
|
+
"id": row[0],
|
|
451
|
+
"type": row[1],
|
|
452
|
+
"description": row[2],
|
|
453
|
+
"details": row[3],
|
|
454
|
+
"scope": row[4],
|
|
455
|
+
"confidence": row[5],
|
|
456
|
+
"apply_count": row[6],
|
|
457
|
+
"help_count": row[7],
|
|
458
|
+
"unhelpful_count": row[8],
|
|
459
|
+
"created_at": row[9],
|
|
460
|
+
"updated_at": row[10],
|
|
461
|
+
}
|
|
462
|
+
if len(row) > 11:
|
|
463
|
+
result["anchor_blob_oid"] = row[11]
|
|
464
|
+
else:
|
|
465
|
+
result["anchor_blob_oid"] = ""
|
|
466
|
+
return result
|
|
467
|
+
|
|
468
|
+
def _build_scope_hierarchy(self, scope: str) -> list[str]:
|
|
469
|
+
"""Build scope candidates from most specific to global."""
|
|
470
|
+
if not scope:
|
|
471
|
+
return [""]
|
|
472
|
+
|
|
473
|
+
candidates = [scope]
|
|
474
|
+
parts = scope.rstrip("/").split("/")
|
|
475
|
+
for i in range(len(parts) - 1, 0, -1):
|
|
476
|
+
candidates.append("/".join(parts[:i]) + "/")
|
|
477
|
+
candidates.append("") # Global
|
|
478
|
+
return candidates
|
|
479
|
+
|
|
480
|
+
def _score_results(
|
|
481
|
+
self,
|
|
482
|
+
results: list[dict],
|
|
483
|
+
scope_candidates: list[str],
|
|
484
|
+
) -> list[dict]:
|
|
485
|
+
"""Score results by FTS relevance x scope proximity x confidence."""
|
|
486
|
+
scope_weights = {s: 1.0 - (i * 0.15) for i, s in enumerate(scope_candidates)}
|
|
487
|
+
|
|
488
|
+
for r in results:
|
|
489
|
+
fts_score = 1.0 / (1.0 + abs(r.get("_rank", 0.0)))
|
|
490
|
+
scope_weight = scope_weights.get(r["scope"], 0.3)
|
|
491
|
+
r["_score"] = fts_score * scope_weight * r["confidence"]
|
|
492
|
+
|
|
493
|
+
results.sort(key=lambda r: r["_score"], reverse=True)
|
|
494
|
+
|
|
495
|
+
# Clean up internal keys
|
|
496
|
+
for r in results:
|
|
497
|
+
r.pop("_rank", None)
|
|
498
|
+
r.pop("_score", None)
|
|
499
|
+
|
|
500
|
+
return results
|
|
501
|
+
|
|
502
|
+
def _apply_decay(self) -> None:
|
|
503
|
+
"""Apply very slow confidence decay to all active learnings.
|
|
504
|
+
|
|
505
|
+
Time-gated to once per hour to prevent excessive decay in
|
|
506
|
+
swarm mode where multiple workers call recall() frequently.
|
|
507
|
+
"""
|
|
508
|
+
now = time.time()
|
|
509
|
+
if now - self._last_decay_at < 3600:
|
|
510
|
+
return
|
|
511
|
+
self._last_decay_at = now
|
|
512
|
+
|
|
513
|
+
conn = self._get_conn()
|
|
514
|
+
conn.execute(
|
|
515
|
+
"""UPDATE learnings
|
|
516
|
+
SET confidence = MAX(?, confidence - ?)
|
|
517
|
+
WHERE status = 'active'""",
|
|
518
|
+
(_CONFIDENCE_FLOOR, _RECALL_DECAY),
|
|
519
|
+
)
|
|
520
|
+
# Auto-archive any that fell below threshold
|
|
521
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
522
|
+
conn.execute(
|
|
523
|
+
"""UPDATE learnings
|
|
524
|
+
SET status = 'archived', updated_at = ?
|
|
525
|
+
WHERE status = 'active' AND confidence < ?""",
|
|
526
|
+
(now, _AUTO_ARCHIVE_THRESHOLD),
|
|
527
|
+
)
|
|
528
|
+
conn.commit()
|
|
529
|
+
|
|
530
|
+
# ------------------------------------------------------------------
|
|
531
|
+
# Deletion
|
|
532
|
+
# ------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
def clear_all(self, status_filter: str = "") -> int:
|
|
535
|
+
"""Hard-delete learnings.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
status_filter: If non-empty, only delete learnings whose
|
|
539
|
+
``status`` matches (e.g. ``"archived"``). Empty string
|
|
540
|
+
wipes every row.
|
|
541
|
+
|
|
542
|
+
Returns the count deleted. FTS rows are removed via the existing
|
|
543
|
+
AFTER DELETE trigger on the ``learnings`` table. MemoryStore
|
|
544
|
+
doesn't serialize writes with a lock elsewhere (SQLite's own
|
|
545
|
+
busy handler is the contention path), so we follow the same
|
|
546
|
+
pattern here.
|
|
547
|
+
"""
|
|
548
|
+
conn = self._get_conn()
|
|
549
|
+
if status_filter:
|
|
550
|
+
cursor = conn.execute(
|
|
551
|
+
"DELETE FROM learnings WHERE status = ?",
|
|
552
|
+
(status_filter,),
|
|
553
|
+
)
|
|
554
|
+
else:
|
|
555
|
+
cursor = conn.execute("DELETE FROM learnings")
|
|
556
|
+
conn.commit()
|
|
557
|
+
return cursor.rowcount
|
|
558
|
+
|
|
559
|
+
def delete_by_id(self, learning_id: int) -> bool:
|
|
560
|
+
"""Hard-delete one learning by id. Returns True on success."""
|
|
561
|
+
conn = self._get_conn()
|
|
562
|
+
cursor = conn.execute(
|
|
563
|
+
"DELETE FROM learnings WHERE id = ?", (learning_id,),
|
|
564
|
+
)
|
|
565
|
+
conn.commit()
|
|
566
|
+
return cursor.rowcount > 0
|
|
567
|
+
|
|
568
|
+
def archive_by_id(self, learning_id: int) -> bool:
|
|
569
|
+
"""Soft-delete a learning by setting ``status='archived'``.
|
|
570
|
+
|
|
571
|
+
Preserves the row and bumps ``updated_at`` so consumers can audit
|
|
572
|
+
when the archival happened.
|
|
573
|
+
|
|
574
|
+
Returns True if a row was updated, False if no matching learning
|
|
575
|
+
was found.
|
|
576
|
+
"""
|
|
577
|
+
conn = self._get_conn()
|
|
578
|
+
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
579
|
+
cursor = conn.execute(
|
|
580
|
+
"""UPDATE learnings
|
|
581
|
+
SET status = 'archived', updated_at = ?
|
|
582
|
+
WHERE id = ?""",
|
|
583
|
+
(now, learning_id),
|
|
584
|
+
)
|
|
585
|
+
conn.commit()
|
|
586
|
+
return cursor.rowcount > 0
|
|
587
|
+
|
|
588
|
+
# ------------------------------------------------------------------
|
|
589
|
+
# Lifecycle
|
|
590
|
+
# ------------------------------------------------------------------
|
|
591
|
+
|
|
592
|
+
def __enter__(self) -> MemoryStore:
|
|
593
|
+
return self
|
|
594
|
+
|
|
595
|
+
def __exit__(self, *exc: object) -> None:
|
|
596
|
+
self.close()
|
|
597
|
+
|
|
598
|
+
def close(self) -> None:
|
|
599
|
+
"""Close the database connection."""
|
|
600
|
+
if self._conn:
|
|
601
|
+
self._conn.close()
|
|
602
|
+
self._conn = None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Repository-local JavaScript/TypeScript project configuration.
|
|
2
|
+
|
|
3
|
+
No package installation or execution is needed to resolve local path aliases.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FileIndex(dict):
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
self.js_projects = []
|
|
16
|
+
|
|
17
|
+
def configure_javascript(self, root):
|
|
18
|
+
for name in list(self):
|
|
19
|
+
if Path(name).name != "tsconfig.json":
|
|
20
|
+
continue
|
|
21
|
+
try:
|
|
22
|
+
options = json.loads((Path(root) / name).read_text()).get("compilerOptions", {})
|
|
23
|
+
except (OSError, ValueError, AttributeError):
|
|
24
|
+
continue
|
|
25
|
+
if not isinstance(options, dict) or not isinstance(options.get("baseUrl", "."), str):
|
|
26
|
+
continue
|
|
27
|
+
directory = str(Path(name).parent)
|
|
28
|
+
base = os.path.normpath(str(Path(directory) / options.get("baseUrl", ".")))
|
|
29
|
+
paths = options.get("paths", {})
|
|
30
|
+
self.js_projects.append((directory, base, paths if isinstance(paths, dict) else {}))
|
|
31
|
+
self.js_projects.sort(key=lambda item: len(item[0]), reverse=True)
|
|
32
|
+
|
|
33
|
+
def javascript_candidates(self, module, source):
|
|
34
|
+
for directory, base, paths in self.js_projects:
|
|
35
|
+
if directory != "." and not source.startswith(directory + "/"):
|
|
36
|
+
continue
|
|
37
|
+
for pattern, targets in paths.items():
|
|
38
|
+
prefix, wildcard, suffix = pattern.partition("*")
|
|
39
|
+
if wildcard:
|
|
40
|
+
if not module.startswith(prefix) or not module.endswith(suffix):
|
|
41
|
+
continue
|
|
42
|
+
middle = module[len(prefix):len(module) - len(suffix) if suffix else None]
|
|
43
|
+
elif module == pattern:
|
|
44
|
+
middle = ""
|
|
45
|
+
else:
|
|
46
|
+
continue
|
|
47
|
+
if isinstance(targets, list):
|
|
48
|
+
for target in targets:
|
|
49
|
+
if isinstance(target, str):
|
|
50
|
+
yield os.path.normpath(str(Path(base) / target.replace("*", middle)))
|
|
51
|
+
yield os.path.normpath(str(Path(base) / module))
|
|
52
|
+
break
|