superlocalmemory 2.8.6 → 3.0.0
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.
- package/LICENSE +9 -1
- package/NOTICE +63 -0
- package/README.md +165 -480
- package/bin/slm +17 -449
- package/bin/slm-npm +1 -1
- package/conftest.py +5 -0
- package/docs/api-reference.md +284 -0
- package/docs/architecture.md +149 -0
- package/docs/auto-memory.md +150 -0
- package/docs/cli-reference.md +276 -0
- package/docs/compliance.md +191 -0
- package/docs/configuration.md +182 -0
- package/docs/getting-started.md +102 -0
- package/docs/ide-setup.md +261 -0
- package/docs/mcp-tools.md +220 -0
- package/docs/migration-from-v2.md +170 -0
- package/docs/profiles.md +173 -0
- package/docs/troubleshooting.md +310 -0
- package/{configs → ide/configs}/antigravity-mcp.json +3 -3
- package/ide/configs/chatgpt-desktop-mcp.json +16 -0
- package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
- package/{configs → ide/configs}/codex-mcp.toml +4 -4
- package/{configs → ide/configs}/continue-mcp.yaml +4 -3
- package/{configs → ide/configs}/continue-skills.yaml +6 -6
- package/ide/configs/cursor-mcp.json +15 -0
- package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
- package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
- package/{configs → ide/configs}/opencode-mcp.json +2 -2
- package/{configs → ide/configs}/perplexity-mcp.json +2 -2
- package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
- package/{configs → ide/configs}/windsurf-mcp.json +3 -3
- package/{configs → ide/configs}/zed-mcp.json +2 -2
- package/{hooks → ide/hooks}/context-hook.js +9 -20
- package/ide/hooks/memory-list-skill.js +70 -0
- package/ide/hooks/memory-profile-skill.js +101 -0
- package/ide/hooks/memory-recall-skill.js +62 -0
- package/ide/hooks/memory-remember-skill.js +68 -0
- package/ide/hooks/memory-reset-skill.js +160 -0
- package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
- package/ide/integrations/langchain/README.md +106 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
- package/ide/integrations/langchain/pyproject.toml +38 -0
- package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
- package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
- package/ide/integrations/langchain/tests/test_security.py +117 -0
- package/ide/integrations/llamaindex/README.md +81 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
- package/ide/integrations/llamaindex/pyproject.toml +43 -0
- package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
- package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
- package/ide/integrations/llamaindex/tests/test_security.py +241 -0
- package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
- package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
- package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
- package/package.json +13 -22
- package/pyproject.toml +85 -0
- package/scripts/build-dmg.sh +417 -0
- package/scripts/install-skills.ps1 +334 -0
- package/scripts/postinstall.js +2 -2
- package/scripts/start-dashboard.ps1 +52 -0
- package/scripts/start-dashboard.sh +41 -0
- package/scripts/sync-wiki.ps1 +127 -0
- package/scripts/sync-wiki.sh +82 -0
- package/scripts/test-dmg.sh +161 -0
- package/scripts/test-npm-package.ps1 +252 -0
- package/scripts/test-npm-package.sh +207 -0
- package/scripts/verify-install.ps1 +294 -0
- package/scripts/verify-install.sh +266 -0
- package/src/superlocalmemory/__init__.py +0 -0
- package/src/superlocalmemory/attribution/__init__.py +9 -0
- package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
- package/src/superlocalmemory/attribution/signer.py +153 -0
- package/src/superlocalmemory/attribution/watermark.py +189 -0
- package/src/superlocalmemory/cli/__init__.py +5 -0
- package/src/superlocalmemory/cli/commands.py +245 -0
- package/src/superlocalmemory/cli/main.py +89 -0
- package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
- package/src/superlocalmemory/cli/post_install.py +99 -0
- package/src/superlocalmemory/cli/setup_wizard.py +129 -0
- package/src/superlocalmemory/compliance/__init__.py +0 -0
- package/src/superlocalmemory/compliance/abac.py +204 -0
- package/src/superlocalmemory/compliance/audit.py +314 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
- package/src/superlocalmemory/compliance/gdpr.py +294 -0
- package/src/superlocalmemory/compliance/lifecycle.py +158 -0
- package/src/superlocalmemory/compliance/retention.py +232 -0
- package/src/superlocalmemory/compliance/scheduler.py +148 -0
- package/src/superlocalmemory/core/__init__.py +0 -0
- package/src/superlocalmemory/core/config.py +391 -0
- package/src/superlocalmemory/core/embeddings.py +293 -0
- package/src/superlocalmemory/core/engine.py +701 -0
- package/src/superlocalmemory/core/hooks.py +65 -0
- package/src/superlocalmemory/core/maintenance.py +172 -0
- package/src/superlocalmemory/core/modes.py +140 -0
- package/src/superlocalmemory/core/profiles.py +234 -0
- package/src/superlocalmemory/core/registry.py +117 -0
- package/src/superlocalmemory/dynamics/__init__.py +0 -0
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
- package/src/superlocalmemory/encoding/__init__.py +0 -0
- package/src/superlocalmemory/encoding/consolidator.py +485 -0
- package/src/superlocalmemory/encoding/emotional.py +125 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
- package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
- package/src/superlocalmemory/encoding/foresight.py +91 -0
- package/src/superlocalmemory/encoding/graph_builder.py +302 -0
- package/src/superlocalmemory/encoding/observation_builder.py +160 -0
- package/src/superlocalmemory/encoding/scene_builder.py +183 -0
- package/src/superlocalmemory/encoding/signal_inference.py +90 -0
- package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
- package/src/superlocalmemory/encoding/type_router.py +235 -0
- package/src/superlocalmemory/hooks/__init__.py +3 -0
- package/src/superlocalmemory/hooks/auto_capture.py +111 -0
- package/src/superlocalmemory/hooks/auto_recall.py +93 -0
- package/src/superlocalmemory/hooks/ide_connector.py +204 -0
- package/src/superlocalmemory/hooks/rules_engine.py +99 -0
- package/src/superlocalmemory/infra/__init__.py +3 -0
- package/src/superlocalmemory/infra/auth_middleware.py +82 -0
- package/src/superlocalmemory/infra/backup.py +317 -0
- package/src/superlocalmemory/infra/cache_manager.py +267 -0
- package/src/superlocalmemory/infra/event_bus.py +381 -0
- package/src/superlocalmemory/infra/rate_limiter.py +135 -0
- package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
- package/src/superlocalmemory/learning/__init__.py +0 -0
- package/src/superlocalmemory/learning/adaptive.py +172 -0
- package/src/superlocalmemory/learning/behavioral.py +490 -0
- package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
- package/src/superlocalmemory/learning/bootstrap.py +298 -0
- package/src/superlocalmemory/learning/cross_project.py +399 -0
- package/src/superlocalmemory/learning/database.py +376 -0
- package/src/superlocalmemory/learning/engagement.py +323 -0
- package/src/superlocalmemory/learning/features.py +138 -0
- package/src/superlocalmemory/learning/feedback.py +316 -0
- package/src/superlocalmemory/learning/outcomes.py +255 -0
- package/src/superlocalmemory/learning/project_context.py +366 -0
- package/src/superlocalmemory/learning/ranker.py +155 -0
- package/src/superlocalmemory/learning/source_quality.py +303 -0
- package/src/superlocalmemory/learning/workflows.py +309 -0
- package/src/superlocalmemory/llm/__init__.py +0 -0
- package/src/superlocalmemory/llm/backbone.py +316 -0
- package/src/superlocalmemory/math/__init__.py +0 -0
- package/src/superlocalmemory/math/fisher.py +356 -0
- package/src/superlocalmemory/math/langevin.py +398 -0
- package/src/superlocalmemory/math/sheaf.py +257 -0
- package/src/superlocalmemory/mcp/__init__.py +0 -0
- package/src/superlocalmemory/mcp/resources.py +245 -0
- package/src/superlocalmemory/mcp/server.py +61 -0
- package/src/superlocalmemory/mcp/tools.py +18 -0
- package/src/superlocalmemory/mcp/tools_core.py +305 -0
- package/src/superlocalmemory/mcp/tools_v28.py +223 -0
- package/src/superlocalmemory/mcp/tools_v3.py +286 -0
- package/src/superlocalmemory/retrieval/__init__.py +0 -0
- package/src/superlocalmemory/retrieval/agentic.py +295 -0
- package/src/superlocalmemory/retrieval/ann_index.py +223 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
- package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
- package/src/superlocalmemory/retrieval/engine.py +390 -0
- package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
- package/src/superlocalmemory/retrieval/fusion.py +78 -0
- package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
- package/src/superlocalmemory/retrieval/reranker.py +154 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
- package/src/superlocalmemory/retrieval/strategy.py +96 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
- package/src/superlocalmemory/server/__init__.py +1 -0
- package/src/superlocalmemory/server/api.py +248 -0
- package/src/superlocalmemory/server/routes/__init__.py +4 -0
- package/src/superlocalmemory/server/routes/agents.py +107 -0
- package/src/superlocalmemory/server/routes/backup.py +91 -0
- package/src/superlocalmemory/server/routes/behavioral.py +127 -0
- package/src/superlocalmemory/server/routes/compliance.py +160 -0
- package/src/superlocalmemory/server/routes/data_io.py +188 -0
- package/src/superlocalmemory/server/routes/events.py +183 -0
- package/src/superlocalmemory/server/routes/helpers.py +85 -0
- package/src/superlocalmemory/server/routes/learning.py +273 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
- package/src/superlocalmemory/server/routes/memories.py +399 -0
- package/src/superlocalmemory/server/routes/profiles.py +219 -0
- package/src/superlocalmemory/server/routes/stats.py +346 -0
- package/src/superlocalmemory/server/routes/v3_api.py +365 -0
- package/src/superlocalmemory/server/routes/ws.py +82 -0
- package/src/superlocalmemory/server/security_middleware.py +57 -0
- package/src/superlocalmemory/server/ui.py +245 -0
- package/src/superlocalmemory/storage/__init__.py +0 -0
- package/src/superlocalmemory/storage/access_control.py +182 -0
- package/src/superlocalmemory/storage/database.py +594 -0
- package/src/superlocalmemory/storage/migrations.py +303 -0
- package/src/superlocalmemory/storage/models.py +406 -0
- package/src/superlocalmemory/storage/schema.py +726 -0
- package/src/superlocalmemory/storage/v2_migrator.py +317 -0
- package/src/superlocalmemory/trust/__init__.py +0 -0
- package/src/superlocalmemory/trust/gate.py +130 -0
- package/src/superlocalmemory/trust/provenance.py +124 -0
- package/src/superlocalmemory/trust/scorer.py +347 -0
- package/src/superlocalmemory/trust/signals.py +153 -0
- package/ui/index.html +278 -5
- package/ui/js/auto-settings.js +70 -0
- package/ui/js/dashboard.js +90 -0
- package/ui/js/fact-detail.js +92 -0
- package/ui/js/feedback.js +2 -2
- package/ui/js/ide-status.js +102 -0
- package/ui/js/math-health.js +98 -0
- package/ui/js/recall-lab.js +127 -0
- package/ui/js/settings.js +2 -2
- package/ui/js/trust-dashboard.js +73 -0
- package/api_server.py +0 -724
- package/bin/aider-smart +0 -72
- package/bin/superlocalmemoryv2-learning +0 -4
- package/bin/superlocalmemoryv2-list +0 -3
- package/bin/superlocalmemoryv2-patterns +0 -4
- package/bin/superlocalmemoryv2-profile +0 -3
- package/bin/superlocalmemoryv2-recall +0 -3
- package/bin/superlocalmemoryv2-remember +0 -3
- package/bin/superlocalmemoryv2-reset +0 -3
- package/bin/superlocalmemoryv2-status +0 -3
- package/configs/chatgpt-desktop-mcp.json +0 -16
- package/configs/cursor-mcp.json +0 -15
- package/hooks/memory-list-skill.js +0 -139
- package/hooks/memory-profile-skill.js +0 -273
- package/hooks/memory-recall-skill.js +0 -114
- package/hooks/memory-remember-skill.js +0 -127
- package/hooks/memory-reset-skill.js +0 -274
- package/mcp_server.py +0 -1808
- package/requirements-core.txt +0 -22
- package/requirements-learning.txt +0 -12
- package/requirements.txt +0 -12
- package/src/agent_registry.py +0 -411
- package/src/auth_middleware.py +0 -61
- package/src/auto_backup.py +0 -459
- package/src/behavioral/__init__.py +0 -49
- package/src/behavioral/behavioral_listener.py +0 -203
- package/src/behavioral/behavioral_patterns.py +0 -275
- package/src/behavioral/cross_project_transfer.py +0 -206
- package/src/behavioral/outcome_inference.py +0 -194
- package/src/behavioral/outcome_tracker.py +0 -193
- package/src/behavioral/tests/__init__.py +0 -4
- package/src/behavioral/tests/test_behavioral_integration.py +0 -108
- package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
- package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
- package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
- package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
- package/src/behavioral/tests/test_outcome_inference.py +0 -107
- package/src/behavioral/tests/test_outcome_tracker.py +0 -96
- package/src/cache_manager.py +0 -518
- package/src/compliance/__init__.py +0 -48
- package/src/compliance/abac_engine.py +0 -149
- package/src/compliance/abac_middleware.py +0 -116
- package/src/compliance/audit_db.py +0 -215
- package/src/compliance/audit_logger.py +0 -148
- package/src/compliance/retention_manager.py +0 -289
- package/src/compliance/retention_scheduler.py +0 -186
- package/src/compliance/tests/__init__.py +0 -4
- package/src/compliance/tests/test_abac_enforcement.py +0 -95
- package/src/compliance/tests/test_abac_engine.py +0 -124
- package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
- package/src/compliance/tests/test_audit_db.py +0 -123
- package/src/compliance/tests/test_audit_logger.py +0 -98
- package/src/compliance/tests/test_mcp_audit.py +0 -128
- package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
- package/src/compliance/tests/test_retention_manager.py +0 -131
- package/src/compliance/tests/test_retention_scheduler.py +0 -99
- package/src/compression/__init__.py +0 -25
- package/src/compression/cli.py +0 -150
- package/src/compression/cold_storage.py +0 -217
- package/src/compression/config.py +0 -72
- package/src/compression/orchestrator.py +0 -133
- package/src/compression/tier2_compressor.py +0 -228
- package/src/compression/tier3_compressor.py +0 -153
- package/src/compression/tier_classifier.py +0 -148
- package/src/db_connection_manager.py +0 -536
- package/src/embedding_engine.py +0 -63
- package/src/embeddings/__init__.py +0 -47
- package/src/embeddings/cache.py +0 -70
- package/src/embeddings/cli.py +0 -113
- package/src/embeddings/constants.py +0 -47
- package/src/embeddings/database.py +0 -91
- package/src/embeddings/engine.py +0 -247
- package/src/embeddings/model_loader.py +0 -145
- package/src/event_bus.py +0 -562
- package/src/graph/__init__.py +0 -36
- package/src/graph/build_helpers.py +0 -74
- package/src/graph/cli.py +0 -87
- package/src/graph/cluster_builder.py +0 -188
- package/src/graph/cluster_summary.py +0 -148
- package/src/graph/constants.py +0 -47
- package/src/graph/edge_builder.py +0 -162
- package/src/graph/entity_extractor.py +0 -95
- package/src/graph/graph_core.py +0 -226
- package/src/graph/graph_search.py +0 -231
- package/src/graph/hierarchical.py +0 -207
- package/src/graph/schema.py +0 -99
- package/src/graph_engine.py +0 -52
- package/src/hnsw_index.py +0 -628
- package/src/hybrid_search.py +0 -46
- package/src/learning/__init__.py +0 -217
- package/src/learning/adaptive_ranker.py +0 -682
- package/src/learning/bootstrap/__init__.py +0 -69
- package/src/learning/bootstrap/constants.py +0 -93
- package/src/learning/bootstrap/db_queries.py +0 -316
- package/src/learning/bootstrap/sampling.py +0 -82
- package/src/learning/bootstrap/text_utils.py +0 -71
- package/src/learning/cross_project_aggregator.py +0 -857
- package/src/learning/db/__init__.py +0 -40
- package/src/learning/db/constants.py +0 -44
- package/src/learning/db/schema.py +0 -279
- package/src/learning/engagement_tracker.py +0 -628
- package/src/learning/feature_extractor.py +0 -708
- package/src/learning/feedback_collector.py +0 -806
- package/src/learning/learning_db.py +0 -915
- package/src/learning/project_context_manager.py +0 -572
- package/src/learning/ranking/__init__.py +0 -33
- package/src/learning/ranking/constants.py +0 -84
- package/src/learning/ranking/helpers.py +0 -278
- package/src/learning/source_quality_scorer.py +0 -676
- package/src/learning/synthetic_bootstrap.py +0 -755
- package/src/learning/tests/test_adaptive_ranker.py +0 -325
- package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
- package/src/learning/tests/test_aggregator.py +0 -306
- package/src/learning/tests/test_auto_retrain_v28.py +0 -35
- package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
- package/src/learning/tests/test_feature_extractor_v28.py +0 -93
- package/src/learning/tests/test_feedback_collector.py +0 -294
- package/src/learning/tests/test_learning_db.py +0 -602
- package/src/learning/tests/test_learning_db_v28.py +0 -110
- package/src/learning/tests/test_learning_init_v28.py +0 -48
- package/src/learning/tests/test_outcome_signals.py +0 -48
- package/src/learning/tests/test_project_context.py +0 -292
- package/src/learning/tests/test_schema_migration.py +0 -319
- package/src/learning/tests/test_signal_inference.py +0 -397
- package/src/learning/tests/test_source_quality.py +0 -351
- package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
- package/src/learning/tests/test_workflow_miner.py +0 -318
- package/src/learning/workflow_pattern_miner.py +0 -655
- package/src/lifecycle/__init__.py +0 -54
- package/src/lifecycle/bounded_growth.py +0 -239
- package/src/lifecycle/compaction_engine.py +0 -226
- package/src/lifecycle/lifecycle_engine.py +0 -355
- package/src/lifecycle/lifecycle_evaluator.py +0 -257
- package/src/lifecycle/lifecycle_scheduler.py +0 -130
- package/src/lifecycle/retention_policy.py +0 -285
- package/src/lifecycle/tests/test_bounded_growth.py +0 -193
- package/src/lifecycle/tests/test_compaction.py +0 -179
- package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
- package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
- package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
- package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
- package/src/lifecycle/tests/test_mcp_compact.py +0 -149
- package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
- package/src/lifecycle/tests/test_retention_policy.py +0 -162
- package/src/mcp_tools_v28.py +0 -281
- package/src/memory/__init__.py +0 -36
- package/src/memory/cli.py +0 -205
- package/src/memory/constants.py +0 -39
- package/src/memory/helpers.py +0 -28
- package/src/memory/schema.py +0 -166
- package/src/memory-profiles.py +0 -595
- package/src/memory-reset.py +0 -491
- package/src/memory_compression.py +0 -989
- package/src/memory_store_v2.py +0 -1155
- package/src/migrate_v1_to_v2.py +0 -629
- package/src/pattern_learner.py +0 -34
- package/src/patterns/__init__.py +0 -24
- package/src/patterns/analyzers.py +0 -251
- package/src/patterns/learner.py +0 -271
- package/src/patterns/scoring.py +0 -171
- package/src/patterns/store.py +0 -225
- package/src/patterns/terminology.py +0 -140
- package/src/provenance_tracker.py +0 -312
- package/src/qualixar_attribution.py +0 -139
- package/src/qualixar_watermark.py +0 -78
- package/src/query_optimizer.py +0 -511
- package/src/rate_limiter.py +0 -83
- package/src/search/__init__.py +0 -20
- package/src/search/cli.py +0 -77
- package/src/search/constants.py +0 -26
- package/src/search/engine.py +0 -241
- package/src/search/fusion.py +0 -122
- package/src/search/index_loader.py +0 -114
- package/src/search/methods.py +0 -162
- package/src/search_engine_v2.py +0 -401
- package/src/setup_validator.py +0 -482
- package/src/subscription_manager.py +0 -391
- package/src/tree/__init__.py +0 -59
- package/src/tree/builder.py +0 -185
- package/src/tree/nodes.py +0 -202
- package/src/tree/queries.py +0 -257
- package/src/tree/schema.py +0 -80
- package/src/tree_manager.py +0 -19
- package/src/trust/__init__.py +0 -45
- package/src/trust/constants.py +0 -66
- package/src/trust/queries.py +0 -157
- package/src/trust/schema.py +0 -95
- package/src/trust/scorer.py +0 -299
- package/src/trust/signals.py +0 -95
- package/src/trust_scorer.py +0 -44
- package/ui/app.js +0 -1588
- package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
- package/ui/js/graph-cytoscape.js +0 -1168
- package/ui/js/graph-d3-backup.js +0 -32
- package/ui/js/graph.js +0 -32
- package/ui_server.py +0 -286
- /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
- /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
- /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
- /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
- /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
- /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
- /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
- /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
- /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
- /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
- /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
- /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
- /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
- /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
- /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
- /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
- /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
- /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
- /package/{completions → ide/completions}/slm.bash +0 -0
- /package/{completions → ide/completions}/slm.zsh +0 -0
- /package/{configs → ide/configs}/cody-commands.json +0 -0
- /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
- /package/{install.ps1 → scripts/install.ps1} +0 -0
- /package/{install.sh → scripts/install.sh} +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Synthetic bootstrap for cold-start ML ranking.
|
|
6
|
+
|
|
7
|
+
Generates (query, fact, label) triples from existing memory patterns
|
|
8
|
+
so the adaptive ranker can operate before real feedback accumulates.
|
|
9
|
+
|
|
10
|
+
Four strategies:
|
|
11
|
+
1. Access-based -- frequently accessed memories are positive for their keywords
|
|
12
|
+
2. Importance-based -- high-importance memories are positive for their tags
|
|
13
|
+
3. Pattern-based -- learned identity patterns generate synthetic queries
|
|
14
|
+
4. Recency-based -- recent memories rank higher for shared-topic queries
|
|
15
|
+
|
|
16
|
+
Ported from V2 SyntheticBootstrapper with V2-specific deps removed.
|
|
17
|
+
|
|
18
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
import re
|
|
27
|
+
import sqlite3
|
|
28
|
+
from collections import Counter
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Optional
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Stopwords for keyword extraction (no NLP dep)
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
_STOPWORDS = frozenset({
|
|
38
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
|
|
39
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "shall",
|
|
40
|
+
"should", "may", "might", "can", "could", "to", "of", "in", "for",
|
|
41
|
+
"on", "with", "at", "by", "from", "as", "into", "through", "during",
|
|
42
|
+
"before", "after", "above", "below", "between", "under", "again",
|
|
43
|
+
"further", "then", "once", "here", "there", "when", "where", "why",
|
|
44
|
+
"how", "all", "each", "every", "both", "few", "more", "most", "other",
|
|
45
|
+
"some", "such", "no", "not", "only", "own", "same", "so", "than",
|
|
46
|
+
"too", "very", "just", "because", "but", "and", "or", "if", "while",
|
|
47
|
+
"about", "up", "out", "off", "over", "also", "it", "its", "this",
|
|
48
|
+
"that", "i", "me", "my", "we", "our", "you", "your", "he", "him",
|
|
49
|
+
"she", "her", "they", "them", "what", "which", "who", "whom",
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
_WORD_RE = re.compile(r"[a-zA-Z0-9_]+")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SyntheticBootstrap:
|
|
56
|
+
"""Generate synthetic training data for cold-start ML ranking.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
db_path: Path to the sqlite database containing a ``memories`` table.
|
|
60
|
+
A fresh database is created if the file does not exist.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, db_path: Path | str) -> None:
|
|
64
|
+
self._db_path = Path(db_path)
|
|
65
|
+
self._ensure_schema()
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# Public API
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def generate(self, profile_id: str, count: int = 200) -> list[dict]:
|
|
72
|
+
"""Generate synthetic (query, fact_id, label) triples.
|
|
73
|
+
|
|
74
|
+
Returns up to *count* records. Returns an empty list when
|
|
75
|
+
the profile has no memories to mine from.
|
|
76
|
+
"""
|
|
77
|
+
memories = self._fetch_memories(profile_id)
|
|
78
|
+
if not memories:
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
records: list[dict] = []
|
|
82
|
+
records.extend(self._access_based(memories))
|
|
83
|
+
records.extend(self._importance_based(memories))
|
|
84
|
+
records.extend(self._recency_based(memories))
|
|
85
|
+
|
|
86
|
+
# Trim to target count with source diversity
|
|
87
|
+
if len(records) > count:
|
|
88
|
+
records = self._diverse_sample(records, count)
|
|
89
|
+
|
|
90
|
+
return records
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
# Strategy 1: access-based
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def _access_based(self, memories: list[dict]) -> list[dict]:
|
|
97
|
+
"""Memories accessed 5+ times are positive for their keywords."""
|
|
98
|
+
records: list[dict] = []
|
|
99
|
+
high_access = [m for m in memories if m.get("access_count", 0) >= 5]
|
|
100
|
+
|
|
101
|
+
for mem in high_access:
|
|
102
|
+
keywords = _extract_keywords(mem.get("content", ""))
|
|
103
|
+
if not keywords:
|
|
104
|
+
continue
|
|
105
|
+
query = " ".join(keywords)
|
|
106
|
+
|
|
107
|
+
records.append(_build_record(query, mem, label=1.0, source="access_pos"))
|
|
108
|
+
|
|
109
|
+
# Pick negatives from memories with different tags
|
|
110
|
+
for neg in self._find_negatives(memories, mem, limit=2):
|
|
111
|
+
records.append(_build_record(query, neg, label=0.0, source="access_neg"))
|
|
112
|
+
|
|
113
|
+
return records
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
# Strategy 2: importance-based
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def _importance_based(self, memories: list[dict]) -> list[dict]:
|
|
120
|
+
"""High-importance memories (>=8) are positive for their tags."""
|
|
121
|
+
records: list[dict] = []
|
|
122
|
+
important = [m for m in memories if (m.get("importance") or 0) >= 8]
|
|
123
|
+
|
|
124
|
+
for mem in important:
|
|
125
|
+
query = self._tags_to_query(mem)
|
|
126
|
+
if not query:
|
|
127
|
+
keywords = _extract_keywords(mem.get("content", ""))
|
|
128
|
+
query = " ".join(keywords) if keywords else ""
|
|
129
|
+
if not query:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
records.append(_build_record(query, mem, label=1.0, source="importance_pos"))
|
|
133
|
+
|
|
134
|
+
for neg in self._find_negatives(memories, mem, limit=2):
|
|
135
|
+
records.append(_build_record(query, neg, label=0.0, source="importance_neg"))
|
|
136
|
+
|
|
137
|
+
return records
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# Strategy 3: recency-based
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def _recency_based(self, memories: list[dict]) -> list[dict]:
|
|
144
|
+
"""Recent memories rank higher for shared-topic queries."""
|
|
145
|
+
records: list[dict] = []
|
|
146
|
+
recent = memories[:30] # Already sorted by created_at DESC
|
|
147
|
+
if len(recent) < 4:
|
|
148
|
+
return records
|
|
149
|
+
|
|
150
|
+
seen_queries: set[str] = set()
|
|
151
|
+
for mem in recent[:15]:
|
|
152
|
+
keywords = _extract_keywords(mem.get("content", ""))
|
|
153
|
+
query = " ".join(keywords) if keywords else ""
|
|
154
|
+
if not query or query in seen_queries:
|
|
155
|
+
continue
|
|
156
|
+
seen_queries.add(query)
|
|
157
|
+
|
|
158
|
+
records.append(_build_record(query, mem, label=0.8, source="recency_pos"))
|
|
159
|
+
|
|
160
|
+
# Older memories about the same topic are weak negatives
|
|
161
|
+
for older in memories[30:]:
|
|
162
|
+
content = (older.get("content") or "").lower()
|
|
163
|
+
if any(kw in content for kw in keywords):
|
|
164
|
+
records.append(
|
|
165
|
+
_build_record(query, older, label=0.3, source="recency_neg")
|
|
166
|
+
)
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
return records
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
# Database
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def _ensure_schema(self) -> None:
|
|
176
|
+
"""Create the memories table if it does not exist (for testing)."""
|
|
177
|
+
conn = sqlite3.connect(str(self._db_path))
|
|
178
|
+
try:
|
|
179
|
+
conn.execute(
|
|
180
|
+
"""CREATE TABLE IF NOT EXISTS memories (
|
|
181
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
182
|
+
profile_id TEXT DEFAULT 'default',
|
|
183
|
+
content TEXT,
|
|
184
|
+
tags TEXT,
|
|
185
|
+
importance INTEGER DEFAULT 5,
|
|
186
|
+
access_count INTEGER DEFAULT 0,
|
|
187
|
+
created_at TEXT
|
|
188
|
+
)"""
|
|
189
|
+
)
|
|
190
|
+
conn.commit()
|
|
191
|
+
finally:
|
|
192
|
+
conn.close()
|
|
193
|
+
|
|
194
|
+
def _fetch_memories(self, profile_id: str) -> list[dict]:
|
|
195
|
+
"""Fetch memories for a profile, ordered by recency."""
|
|
196
|
+
conn = sqlite3.connect(str(self._db_path))
|
|
197
|
+
conn.row_factory = sqlite3.Row
|
|
198
|
+
try:
|
|
199
|
+
cur = conn.execute(
|
|
200
|
+
"SELECT * FROM memories WHERE profile_id = ? "
|
|
201
|
+
"ORDER BY created_at DESC LIMIT 500",
|
|
202
|
+
(profile_id,),
|
|
203
|
+
)
|
|
204
|
+
return [dict(r) for r in cur.fetchall()]
|
|
205
|
+
except sqlite3.OperationalError:
|
|
206
|
+
return []
|
|
207
|
+
finally:
|
|
208
|
+
conn.close()
|
|
209
|
+
|
|
210
|
+
# ------------------------------------------------------------------
|
|
211
|
+
# Helpers
|
|
212
|
+
# ------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _tags_to_query(memory: dict) -> str:
|
|
216
|
+
"""Extract a query string from the memory's tags field."""
|
|
217
|
+
tags = memory.get("tags", "")
|
|
218
|
+
if not tags:
|
|
219
|
+
return ""
|
|
220
|
+
if isinstance(tags, list):
|
|
221
|
+
return " ".join(tags[:5])
|
|
222
|
+
try:
|
|
223
|
+
parsed = json.loads(tags)
|
|
224
|
+
if isinstance(parsed, list):
|
|
225
|
+
return " ".join(str(t) for t in parsed[:5])
|
|
226
|
+
except (json.JSONDecodeError, TypeError):
|
|
227
|
+
pass
|
|
228
|
+
return " ".join(t.strip() for t in str(tags).split(",") if t.strip())[:5]
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _find_negatives(
|
|
232
|
+
memories: list[dict], anchor: dict, limit: int = 2
|
|
233
|
+
) -> list[dict]:
|
|
234
|
+
"""Pick memories dissimilar to anchor (different tags or id)."""
|
|
235
|
+
anchor_id = anchor.get("id")
|
|
236
|
+
anchor_tags = set(
|
|
237
|
+
t.strip().lower()
|
|
238
|
+
for t in str(anchor.get("tags", "")).split(",")
|
|
239
|
+
if t.strip()
|
|
240
|
+
)
|
|
241
|
+
negatives: list[dict] = []
|
|
242
|
+
for mem in memories:
|
|
243
|
+
if mem.get("id") == anchor_id:
|
|
244
|
+
continue
|
|
245
|
+
mem_tags = set(
|
|
246
|
+
t.strip().lower()
|
|
247
|
+
for t in str(mem.get("tags", "")).split(",")
|
|
248
|
+
if t.strip()
|
|
249
|
+
)
|
|
250
|
+
if not mem_tags & anchor_tags:
|
|
251
|
+
negatives.append(mem)
|
|
252
|
+
if len(negatives) >= limit:
|
|
253
|
+
break
|
|
254
|
+
return negatives
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def _diverse_sample(records: list[dict], target: int) -> list[dict]:
|
|
258
|
+
"""Sample records proportionally across sources."""
|
|
259
|
+
by_source: dict[str, list[dict]] = {}
|
|
260
|
+
for r in records:
|
|
261
|
+
by_source.setdefault(r["source"], []).append(r)
|
|
262
|
+
n_sources = len(by_source) or 1
|
|
263
|
+
per_source = max(1, target // n_sources)
|
|
264
|
+
sampled: list[dict] = []
|
|
265
|
+
for src_records in by_source.values():
|
|
266
|
+
sampled.extend(src_records[:per_source])
|
|
267
|
+
return sampled[:target]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ----------------------------------------------------------------------
|
|
271
|
+
# Module-level helpers (stateless)
|
|
272
|
+
# ----------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _extract_keywords(content: str, top_n: int = 3) -> list[str]:
|
|
276
|
+
"""Extract top-N keywords from text via frequency (no NLP deps)."""
|
|
277
|
+
if not content:
|
|
278
|
+
return []
|
|
279
|
+
tokens = _WORD_RE.findall(content.lower())
|
|
280
|
+
filtered = [t for t in tokens if t not in _STOPWORDS and len(t) > 2]
|
|
281
|
+
counts = Counter(filtered)
|
|
282
|
+
return [word for word, _ in counts.most_common(top_n)]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _build_record(
|
|
286
|
+
query: str,
|
|
287
|
+
memory: dict,
|
|
288
|
+
label: float,
|
|
289
|
+
source: str,
|
|
290
|
+
) -> dict:
|
|
291
|
+
"""Build a single training record."""
|
|
292
|
+
return {
|
|
293
|
+
"query": query,
|
|
294
|
+
"query_hash": hashlib.sha256(query.encode()).hexdigest()[:16],
|
|
295
|
+
"fact_id": memory.get("id", 0),
|
|
296
|
+
"label": label,
|
|
297
|
+
"source": source,
|
|
298
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Cross-project knowledge aggregator.
|
|
6
|
+
|
|
7
|
+
Transfers learned preferences and patterns from completed projects to
|
|
8
|
+
new ones via temporal-decay merging and contradiction detection.
|
|
9
|
+
Ported from V2 CrossProjectAggregator with inline sqlite3 queries.
|
|
10
|
+
|
|
11
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import math
|
|
19
|
+
import re
|
|
20
|
+
import sqlite3
|
|
21
|
+
from collections import Counter
|
|
22
|
+
from datetime import UTC, datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Temporal decay half-life in days (1 year)
|
|
29
|
+
DECAY_HALF_LIFE_DAYS = 365.0
|
|
30
|
+
|
|
31
|
+
# Contradiction detection window in days
|
|
32
|
+
CONTRADICTION_WINDOW_DAYS = 90
|
|
33
|
+
|
|
34
|
+
# Minimum evidence to consider a pattern valid
|
|
35
|
+
MIN_EVIDENCE_FOR_MERGE = 2
|
|
36
|
+
|
|
37
|
+
# Minimum confidence for a merged pattern to be stored
|
|
38
|
+
MIN_MERGE_CONFIDENCE = 0.3
|
|
39
|
+
|
|
40
|
+
# Simple keyword regex for frequency analysis
|
|
41
|
+
_WORD_RE = re.compile(r"[a-zA-Z0-9_]+")
|
|
42
|
+
|
|
43
|
+
# Technology categories detected from content
|
|
44
|
+
TECH_CATEGORIES: dict[str, list[str]] = {
|
|
45
|
+
"frontend_framework": [
|
|
46
|
+
"react", "vue", "angular", "svelte", "next", "nuxt", "solid",
|
|
47
|
+
],
|
|
48
|
+
"backend_framework": [
|
|
49
|
+
"express", "fastapi", "django", "flask", "spring", "rails", "nest",
|
|
50
|
+
],
|
|
51
|
+
"language": [
|
|
52
|
+
"python", "typescript", "javascript", "rust", "go", "java", "kotlin",
|
|
53
|
+
],
|
|
54
|
+
"database": [
|
|
55
|
+
"postgres", "mysql", "sqlite", "mongo", "redis", "dynamodb", "supabase",
|
|
56
|
+
],
|
|
57
|
+
"cloud": ["aws", "azure", "gcp", "vercel", "cloudflare", "netlify"],
|
|
58
|
+
"testing": ["pytest", "jest", "vitest", "cypress", "playwright"],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Schema for transferable patterns
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
_SCHEMA = """
|
|
66
|
+
CREATE TABLE IF NOT EXISTS transferable_patterns (
|
|
67
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
+
pattern_type TEXT NOT NULL DEFAULT 'preference',
|
|
69
|
+
key TEXT NOT NULL,
|
|
70
|
+
value TEXT NOT NULL,
|
|
71
|
+
confidence REAL DEFAULT 0.0,
|
|
72
|
+
evidence_count INTEGER DEFAULT 0,
|
|
73
|
+
profiles_seen INTEGER DEFAULT 1,
|
|
74
|
+
decay_factor REAL DEFAULT 1.0,
|
|
75
|
+
contradictions TEXT DEFAULT '[]',
|
|
76
|
+
first_seen TEXT,
|
|
77
|
+
last_seen TEXT,
|
|
78
|
+
UNIQUE(pattern_type, key)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
82
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
83
|
+
profile_id TEXT DEFAULT 'default',
|
|
84
|
+
content TEXT,
|
|
85
|
+
project_name TEXT,
|
|
86
|
+
created_at TEXT
|
|
87
|
+
);
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CrossProjectAggregator:
|
|
92
|
+
"""Aggregate technology preferences across profiles."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, db_path: Path | str) -> None:
|
|
95
|
+
self._db_path = Path(db_path)
|
|
96
|
+
self._ensure_schema()
|
|
97
|
+
|
|
98
|
+
def aggregate(
|
|
99
|
+
self, source_profiles: list[str], target_profile: str
|
|
100
|
+
) -> list[dict]:
|
|
101
|
+
"""Transfer patterns from *source_profiles* to *target_profile*."""
|
|
102
|
+
if not source_profiles:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
# Step 1: Analyse each source profile
|
|
106
|
+
profile_patterns: list[dict] = []
|
|
107
|
+
for profile_id in source_profiles:
|
|
108
|
+
pdata = self._analyse_profile(profile_id)
|
|
109
|
+
if pdata:
|
|
110
|
+
profile_patterns.append(pdata)
|
|
111
|
+
|
|
112
|
+
if not profile_patterns:
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
# Step 2: Merge with temporal decay
|
|
116
|
+
merged = self._merge_with_decay(profile_patterns)
|
|
117
|
+
|
|
118
|
+
# Step 3: Detect contradictions
|
|
119
|
+
for key, pattern_data in merged.items():
|
|
120
|
+
pattern_data["contradictions"] = self._detect_contradictions(
|
|
121
|
+
key, pattern_data
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Step 4: Store and return
|
|
125
|
+
self._store_patterns(merged)
|
|
126
|
+
return [{"key": k, **v} for k, v in merged.items()]
|
|
127
|
+
|
|
128
|
+
def get_preferences(self, min_confidence: float = 0.6) -> dict[str, dict]:
|
|
129
|
+
"""Retrieve stored transferable preferences above *min_confidence*."""
|
|
130
|
+
conn = sqlite3.connect(str(self._db_path))
|
|
131
|
+
conn.row_factory = sqlite3.Row
|
|
132
|
+
try:
|
|
133
|
+
cur = conn.execute(
|
|
134
|
+
"SELECT * FROM transferable_patterns "
|
|
135
|
+
"WHERE confidence >= ? ORDER BY confidence DESC",
|
|
136
|
+
(min_confidence,),
|
|
137
|
+
)
|
|
138
|
+
result: dict[str, dict] = {}
|
|
139
|
+
for row in cur.fetchall():
|
|
140
|
+
d = dict(row)
|
|
141
|
+
contradictions = _parse_json_list(d.get("contradictions", "[]"))
|
|
142
|
+
result[d["key"]] = {
|
|
143
|
+
"value": d["value"],
|
|
144
|
+
"confidence": d["confidence"],
|
|
145
|
+
"evidence_count": d["evidence_count"],
|
|
146
|
+
"profiles_seen": d.get("profiles_seen", 1),
|
|
147
|
+
"decay_factor": d.get("decay_factor", 1.0),
|
|
148
|
+
"contradictions": contradictions,
|
|
149
|
+
}
|
|
150
|
+
return result
|
|
151
|
+
except sqlite3.OperationalError:
|
|
152
|
+
return {}
|
|
153
|
+
finally:
|
|
154
|
+
conn.close()
|
|
155
|
+
|
|
156
|
+
def _analyse_profile(self, profile_id: str) -> Optional[dict]:
|
|
157
|
+
"""Detect tech category preferences in a single profile."""
|
|
158
|
+
conn = sqlite3.connect(str(self._db_path))
|
|
159
|
+
conn.row_factory = sqlite3.Row
|
|
160
|
+
try:
|
|
161
|
+
cur = conn.execute(
|
|
162
|
+
"SELECT content, created_at FROM memories "
|
|
163
|
+
"WHERE profile_id = ? ORDER BY created_at DESC LIMIT 500",
|
|
164
|
+
(profile_id,),
|
|
165
|
+
)
|
|
166
|
+
rows = cur.fetchall()
|
|
167
|
+
except sqlite3.OperationalError:
|
|
168
|
+
return None
|
|
169
|
+
finally:
|
|
170
|
+
conn.close()
|
|
171
|
+
|
|
172
|
+
if not rows:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
# Detect tech keywords per category
|
|
176
|
+
category_counts: dict[str, Counter] = {
|
|
177
|
+
cat: Counter() for cat in TECH_CATEGORIES
|
|
178
|
+
}
|
|
179
|
+
for row in rows:
|
|
180
|
+
content = (dict(row).get("content") or "").lower()
|
|
181
|
+
tokens = set(_WORD_RE.findall(content))
|
|
182
|
+
for category, keywords in TECH_CATEGORIES.items():
|
|
183
|
+
for kw in keywords:
|
|
184
|
+
if kw in tokens:
|
|
185
|
+
category_counts[category][kw] += 1
|
|
186
|
+
|
|
187
|
+
# Build patterns: winner per category with enough evidence
|
|
188
|
+
patterns: dict[str, dict] = {}
|
|
189
|
+
for category, counter in category_counts.items():
|
|
190
|
+
if not counter:
|
|
191
|
+
continue
|
|
192
|
+
winner, win_count = counter.most_common(1)[0]
|
|
193
|
+
total = sum(counter.values())
|
|
194
|
+
if win_count >= MIN_EVIDENCE_FOR_MERGE:
|
|
195
|
+
patterns[category] = {
|
|
196
|
+
"value": winner,
|
|
197
|
+
"confidence": round(win_count / total, 3) if total else 0.0,
|
|
198
|
+
"evidence_count": win_count,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if not patterns:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
# Latest timestamp for decay calculation
|
|
205
|
+
latest_ts = dict(rows[0]).get("created_at", datetime.now(UTC).isoformat())
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
"profile": profile_id,
|
|
209
|
+
"patterns": patterns,
|
|
210
|
+
"latest_timestamp": latest_ts,
|
|
211
|
+
"memory_count": len(rows),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
def _merge_with_decay(
|
|
215
|
+
self, profile_patterns: list[dict]
|
|
216
|
+
) -> dict[str, dict]:
|
|
217
|
+
now = datetime.now(UTC)
|
|
218
|
+
contributions: dict[str, list[dict]] = {}
|
|
219
|
+
|
|
220
|
+
for pdata in profile_patterns:
|
|
221
|
+
age_days = _days_since(pdata["latest_timestamp"], now)
|
|
222
|
+
weight = math.exp(-age_days / DECAY_HALF_LIFE_DAYS)
|
|
223
|
+
|
|
224
|
+
for cat_key, pattern in pdata["patterns"].items():
|
|
225
|
+
contributions.setdefault(cat_key, []).append({
|
|
226
|
+
"value": pattern["value"],
|
|
227
|
+
"confidence": pattern["confidence"],
|
|
228
|
+
"evidence_count": pattern["evidence_count"],
|
|
229
|
+
"weight": weight,
|
|
230
|
+
"profile": pdata["profile"],
|
|
231
|
+
"latest_timestamp": pdata["latest_timestamp"],
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
merged: dict[str, dict] = {}
|
|
235
|
+
for cat_key, contribs in contributions.items():
|
|
236
|
+
result = self._merge_single(contribs)
|
|
237
|
+
if result is not None:
|
|
238
|
+
merged[cat_key] = result
|
|
239
|
+
return merged
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _merge_single(contributions: list[dict]) -> Optional[dict]:
|
|
243
|
+
"""Merge contributions for one category across profiles."""
|
|
244
|
+
value_scores: dict[str, float] = {}
|
|
245
|
+
value_evidence: dict[str, int] = {}
|
|
246
|
+
value_profiles: dict[str, set] = {}
|
|
247
|
+
total_weighted = 0.0
|
|
248
|
+
|
|
249
|
+
for c in contributions:
|
|
250
|
+
v = c["value"]
|
|
251
|
+
w_ev = c["evidence_count"] * c["weight"]
|
|
252
|
+
value_scores[v] = value_scores.get(v, 0.0) + w_ev
|
|
253
|
+
value_evidence[v] = value_evidence.get(v, 0) + c["evidence_count"]
|
|
254
|
+
value_profiles.setdefault(v, set()).add(c["profile"])
|
|
255
|
+
total_weighted += w_ev
|
|
256
|
+
|
|
257
|
+
if total_weighted == 0:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
winner = max(value_scores, key=lambda k: value_scores[k])
|
|
261
|
+
confidence = value_scores[winner] / total_weighted
|
|
262
|
+
ev = value_evidence[winner]
|
|
263
|
+
|
|
264
|
+
if ev < MIN_EVIDENCE_FOR_MERGE or confidence < MIN_MERGE_CONFIDENCE:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"value": winner,
|
|
269
|
+
"confidence": round(min(0.95, confidence), 3),
|
|
270
|
+
"evidence_count": ev,
|
|
271
|
+
"profiles_seen": len(value_profiles[winner]),
|
|
272
|
+
"decay_factor": round(
|
|
273
|
+
max(c["weight"] for c in contributions if c["value"] == winner),
|
|
274
|
+
4,
|
|
275
|
+
),
|
|
276
|
+
"contradictions": [],
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
def _detect_contradictions(
|
|
280
|
+
self, pattern_key: str, pattern_data: dict
|
|
281
|
+
) -> list[str]:
|
|
282
|
+
contradictions: list[str] = []
|
|
283
|
+
|
|
284
|
+
# Check stored value vs new value
|
|
285
|
+
conn = sqlite3.connect(str(self._db_path))
|
|
286
|
+
conn.row_factory = sqlite3.Row
|
|
287
|
+
try:
|
|
288
|
+
cur = conn.execute(
|
|
289
|
+
"SELECT value, last_seen FROM transferable_patterns "
|
|
290
|
+
"WHERE key = ? AND pattern_type = 'preference'",
|
|
291
|
+
(pattern_key,),
|
|
292
|
+
)
|
|
293
|
+
row = cur.fetchone()
|
|
294
|
+
if row:
|
|
295
|
+
d = dict(row)
|
|
296
|
+
old_val = d.get("value", "")
|
|
297
|
+
if old_val and old_val != pattern_data["value"]:
|
|
298
|
+
old_ts = d.get("last_seen", "")
|
|
299
|
+
if old_ts and _is_within_window(old_ts, CONTRADICTION_WINDOW_DAYS):
|
|
300
|
+
contradictions.append(
|
|
301
|
+
f"Changed from '{old_val}' to "
|
|
302
|
+
f"'{pattern_data['value']}' within "
|
|
303
|
+
f"{CONTRADICTION_WINDOW_DAYS} days"
|
|
304
|
+
)
|
|
305
|
+
except sqlite3.OperationalError:
|
|
306
|
+
pass
|
|
307
|
+
finally:
|
|
308
|
+
conn.close()
|
|
309
|
+
|
|
310
|
+
return contradictions
|
|
311
|
+
|
|
312
|
+
def _ensure_schema(self) -> None:
|
|
313
|
+
conn = sqlite3.connect(str(self._db_path))
|
|
314
|
+
try:
|
|
315
|
+
conn.executescript(_SCHEMA)
|
|
316
|
+
finally:
|
|
317
|
+
conn.close()
|
|
318
|
+
|
|
319
|
+
def _store_patterns(self, merged: dict[str, dict]) -> None:
|
|
320
|
+
conn = sqlite3.connect(str(self._db_path))
|
|
321
|
+
now = datetime.now(UTC).isoformat()
|
|
322
|
+
try:
|
|
323
|
+
for key, data in merged.items():
|
|
324
|
+
conn.execute(
|
|
325
|
+
"""INSERT INTO transferable_patterns
|
|
326
|
+
(pattern_type, key, value, confidence, evidence_count,
|
|
327
|
+
profiles_seen, decay_factor, contradictions,
|
|
328
|
+
first_seen, last_seen)
|
|
329
|
+
VALUES ('preference', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
330
|
+
ON CONFLICT(pattern_type, key) DO UPDATE SET
|
|
331
|
+
value = excluded.value,
|
|
332
|
+
confidence = excluded.confidence,
|
|
333
|
+
evidence_count = excluded.evidence_count,
|
|
334
|
+
profiles_seen = excluded.profiles_seen,
|
|
335
|
+
decay_factor = excluded.decay_factor,
|
|
336
|
+
contradictions = excluded.contradictions,
|
|
337
|
+
last_seen = excluded.last_seen
|
|
338
|
+
""",
|
|
339
|
+
(
|
|
340
|
+
key,
|
|
341
|
+
data["value"],
|
|
342
|
+
data["confidence"],
|
|
343
|
+
data["evidence_count"],
|
|
344
|
+
data.get("profiles_seen", 1),
|
|
345
|
+
data.get("decay_factor", 1.0),
|
|
346
|
+
json.dumps(data.get("contradictions", [])),
|
|
347
|
+
now,
|
|
348
|
+
now,
|
|
349
|
+
),
|
|
350
|
+
)
|
|
351
|
+
conn.commit()
|
|
352
|
+
finally:
|
|
353
|
+
conn.close()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ----------------------------------------------------------------------
|
|
357
|
+
# Module-level helpers
|
|
358
|
+
# ----------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _parse_json_list(raw: Any) -> list:
|
|
362
|
+
"""Parse a JSON string into a list, returning [] on failure."""
|
|
363
|
+
if isinstance(raw, list):
|
|
364
|
+
return raw
|
|
365
|
+
if isinstance(raw, str):
|
|
366
|
+
try:
|
|
367
|
+
return json.loads(raw)
|
|
368
|
+
except (json.JSONDecodeError, TypeError):
|
|
369
|
+
pass
|
|
370
|
+
return []
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _days_since(timestamp_str: str, now: datetime | None = None) -> float:
|
|
374
|
+
if now is None:
|
|
375
|
+
now = datetime.now(UTC)
|
|
376
|
+
if not timestamp_str:
|
|
377
|
+
return 0.0
|
|
378
|
+
try:
|
|
379
|
+
ts = datetime.fromisoformat(str(timestamp_str).replace(" ", "T"))
|
|
380
|
+
return max(0.0, (now - ts).total_seconds() / 86400.0)
|
|
381
|
+
except (ValueError, AttributeError, TypeError):
|
|
382
|
+
pass
|
|
383
|
+
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%Y-%m-%dT%H:%M:%S.%f"):
|
|
384
|
+
try:
|
|
385
|
+
ts = datetime.strptime(str(timestamp_str), fmt)
|
|
386
|
+
return max(0.0, (now - ts).total_seconds() / 86400.0)
|
|
387
|
+
except (ValueError, TypeError):
|
|
388
|
+
continue
|
|
389
|
+
return 0.0
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _is_within_window(timestamp_str: str, window_days: int) -> bool:
|
|
393
|
+
if not timestamp_str:
|
|
394
|
+
return False
|
|
395
|
+
try:
|
|
396
|
+
ts = datetime.fromisoformat(str(timestamp_str).replace(" ", "T"))
|
|
397
|
+
return (datetime.now(UTC) - ts).days <= window_days
|
|
398
|
+
except (ValueError, AttributeError, TypeError):
|
|
399
|
+
return False
|