superlocalmemory 4.1.13 → 4.1.14
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/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +48 -0
- package/README.md +3 -3
- package/package.json +1 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/agents/slm-memory-advisor.md +1 -1
- package/plugin-src/agents/slm-optimize-advisor.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +1 -1
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/scripts/postinstall.js +71 -2
- package/pyproject.toml +0 -250
- package/src/superlocalmemory/__init__.py +0 -82
- package/src/superlocalmemory/access/__init__.py +0 -3
- package/src/superlocalmemory/access/rbac.py +0 -575
- package/src/superlocalmemory/attribution/__init__.py +0 -9
- package/src/superlocalmemory/attribution/signer.py +0 -173
- package/src/superlocalmemory/attribution/watermark.py +0 -189
- package/src/superlocalmemory/brain/__init__.py +0 -5
- package/src/superlocalmemory/brain/truth.py +0 -418
- package/src/superlocalmemory/cli/__init__.py +0 -5
- package/src/superlocalmemory/cli/__main__.py +0 -17
- package/src/superlocalmemory/cli/_lazy_init.py +0 -115
- package/src/superlocalmemory/cli/cache_cmd.py +0 -198
- package/src/superlocalmemory/cli/commands.py +0 -4710
- package/src/superlocalmemory/cli/compress_cmd.py +0 -151
- package/src/superlocalmemory/cli/context_commands.py +0 -193
- package/src/superlocalmemory/cli/daemon.py +0 -909
- package/src/superlocalmemory/cli/db_migrate.py +0 -150
- package/src/superlocalmemory/cli/diagnostics_cmd.py +0 -101
- package/src/superlocalmemory/cli/escape_hatch.py +0 -220
- package/src/superlocalmemory/cli/evidence_cmd.py +0 -103
- package/src/superlocalmemory/cli/gdpr_cmd.py +0 -792
- package/src/superlocalmemory/cli/gdpr_io.py +0 -109
- package/src/superlocalmemory/cli/help_cmd.py +0 -197
- package/src/superlocalmemory/cli/host_upgrades.py +0 -189
- package/src/superlocalmemory/cli/ingest_cmd.py +0 -327
- package/src/superlocalmemory/cli/json_output.py +0 -81
- package/src/superlocalmemory/cli/loop_cmd.py +0 -187
- package/src/superlocalmemory/cli/main.py +0 -1145
- package/src/superlocalmemory/cli/mesh_cmd.py +0 -38
- package/src/superlocalmemory/cli/migrate_cmd.py +0 -55
- package/src/superlocalmemory/cli/ops_cmd.py +0 -281
- package/src/superlocalmemory/cli/optimize_cmd.py +0 -179
- package/src/superlocalmemory/cli/optimize_constants.py +0 -31
- package/src/superlocalmemory/cli/pending_store.py +0 -296
- package/src/superlocalmemory/cli/proxy_cmd.py +0 -108
- package/src/superlocalmemory/cli/scale_engine_cmd.py +0 -56
- package/src/superlocalmemory/cli/service_installer.py +0 -373
- package/src/superlocalmemory/cli/setup_wizard.py +0 -1162
- package/src/superlocalmemory/cli/summary_cmd.py +0 -215
- package/src/superlocalmemory/cli/version_banner.py +0 -202
- package/src/superlocalmemory/cli/wizard_v3426_options.py +0 -129
- package/src/superlocalmemory/code_graph/__init__.py +0 -46
- package/src/superlocalmemory/code_graph/blast_radius.py +0 -177
- package/src/superlocalmemory/code_graph/bridge/__init__.py +0 -36
- package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +0 -490
- package/src/superlocalmemory/code_graph/bridge/event_listeners.py +0 -206
- package/src/superlocalmemory/code_graph/bridge/fact_enricher.py +0 -159
- package/src/superlocalmemory/code_graph/bridge/hebbian_linker.py +0 -170
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +0 -220
- package/src/superlocalmemory/code_graph/bridge/temporal_checker.py +0 -152
- package/src/superlocalmemory/code_graph/changes.py +0 -363
- package/src/superlocalmemory/code_graph/communities.py +0 -299
- package/src/superlocalmemory/code_graph/config.py +0 -154
- package/src/superlocalmemory/code_graph/database.py +0 -526
- package/src/superlocalmemory/code_graph/extractors/__init__.py +0 -95
- package/src/superlocalmemory/code_graph/extractors/python.py +0 -413
- package/src/superlocalmemory/code_graph/extractors/typescript.py +0 -556
- package/src/superlocalmemory/code_graph/flows.py +0 -350
- package/src/superlocalmemory/code_graph/git_hooks.py +0 -226
- package/src/superlocalmemory/code_graph/graph_engine.py +0 -295
- package/src/superlocalmemory/code_graph/graph_store.py +0 -335
- package/src/superlocalmemory/code_graph/incremental.py +0 -200
- package/src/superlocalmemory/code_graph/models.py +0 -130
- package/src/superlocalmemory/code_graph/parser.py +0 -687
- package/src/superlocalmemory/code_graph/resolver.py +0 -321
- package/src/superlocalmemory/code_graph/search.py +0 -460
- package/src/superlocalmemory/code_graph/service.py +0 -95
- package/src/superlocalmemory/code_graph/watcher.py +0 -207
- package/src/superlocalmemory/compliance/__init__.py +0 -0
- package/src/superlocalmemory/compliance/abac.py +0 -204
- package/src/superlocalmemory/compliance/audit.py +0 -385
- package/src/superlocalmemory/compliance/eu_ai_act.py +0 -101
- package/src/superlocalmemory/compliance/gdpr.py +0 -1479
- package/src/superlocalmemory/compliance/lifecycle.py +0 -158
- package/src/superlocalmemory/compliance/retention.py +0 -415
- package/src/superlocalmemory/compliance/scheduler.py +0 -217
- package/src/superlocalmemory/contracts/__init__.py +0 -1
- package/src/superlocalmemory/contracts/schemas/agent-experience-v1.schema.json +0 -92
- package/src/superlocalmemory/contracts/schemas/agent-integration-contract-v2.schema.json +0 -46
- package/src/superlocalmemory/contracts/schemas/cognitive-turn-receipt-v1.schema.json +0 -59
- package/src/superlocalmemory/contracts/v402.py +0 -62
- package/src/superlocalmemory/core/__init__.py +0 -0
- package/src/superlocalmemory/core/actor_context.py +0 -166
- package/src/superlocalmemory/core/admission.py +0 -769
- package/src/superlocalmemory/core/backend_orchestrator.py +0 -637
- package/src/superlocalmemory/core/block_hygiene.py +0 -147
- package/src/superlocalmemory/core/community_summary.py +0 -267
- package/src/superlocalmemory/core/component_healer.py +0 -144
- package/src/superlocalmemory/core/component_registry.py +0 -514
- package/src/superlocalmemory/core/config.py +0 -2204
- package/src/superlocalmemory/core/consolidation_engine.py +0 -983
- package/src/superlocalmemory/core/context_cache.py +0 -574
- package/src/superlocalmemory/core/derivation_lineage.py +0 -246
- package/src/superlocalmemory/core/embedding_worker.py +0 -208
- package/src/superlocalmemory/core/embeddings.py +0 -1052
- package/src/superlocalmemory/core/engine.py +0 -1395
- package/src/superlocalmemory/core/engine_capabilities.py +0 -24
- package/src/superlocalmemory/core/engine_ingestion.py +0 -983
- package/src/superlocalmemory/core/engine_lock.py +0 -75
- package/src/superlocalmemory/core/engine_wiring.py +0 -776
- package/src/superlocalmemory/core/entity_community.py +0 -178
- package/src/superlocalmemory/core/error_envelope.py +0 -60
- package/src/superlocalmemory/core/evidence_bundle.py +0 -528
- package/src/superlocalmemory/core/fact_consolidator.py +0 -812
- package/src/superlocalmemory/core/file_lock.py +0 -92
- package/src/superlocalmemory/core/graph_analyzer.py +0 -456
- package/src/superlocalmemory/core/graph_metrics.py +0 -597
- package/src/superlocalmemory/core/graph_pruner.py +0 -939
- package/src/superlocalmemory/core/health_monitor.py +0 -338
- package/src/superlocalmemory/core/hooks.py +0 -65
- package/src/superlocalmemory/core/ingest_gate.py +0 -133
- package/src/superlocalmemory/core/ingest_policy.py +0 -38
- package/src/superlocalmemory/core/ingestion_command.py +0 -1042
- package/src/superlocalmemory/core/injection.py +0 -434
- package/src/superlocalmemory/core/install_detector.py +0 -131
- package/src/superlocalmemory/core/key_expander.py +0 -138
- package/src/superlocalmemory/core/lifecycle_state.py +0 -153
- package/src/superlocalmemory/core/maintenance.py +0 -777
- package/src/superlocalmemory/core/maintenance_scheduler.py +0 -507
- package/src/superlocalmemory/core/materialization_control.py +0 -20
- package/src/superlocalmemory/core/mcp_embedder_proxy.py +0 -89
- package/src/superlocalmemory/core/memory_health.py +0 -266
- package/src/superlocalmemory/core/mode_capability.py +0 -111
- package/src/superlocalmemory/core/modes.py +0 -168
- package/src/superlocalmemory/core/mutations.py +0 -688
- package/src/superlocalmemory/core/ollama_embedder.py +0 -266
- package/src/superlocalmemory/core/ollama_validator.py +0 -315
- package/src/superlocalmemory/core/operation_policy.py +0 -92
- package/src/superlocalmemory/core/operation_policy_registry.py +0 -542
- package/src/superlocalmemory/core/operation_request.py +0 -127
- package/src/superlocalmemory/core/ops_remediation.py +0 -542
- package/src/superlocalmemory/core/pii.py +0 -105
- package/src/superlocalmemory/core/platform_utils.py +0 -138
- package/src/superlocalmemory/core/profiles.py +0 -234
- package/src/superlocalmemory/core/progressive_abstraction.py +0 -208
- package/src/superlocalmemory/core/projection_drain.py +0 -380
- package/src/superlocalmemory/core/queue_consumer.py +0 -168
- package/src/superlocalmemory/core/ram_lock.py +0 -160
- package/src/superlocalmemory/core/rate_limit.py +0 -151
- package/src/superlocalmemory/core/recall_gate.py +0 -95
- package/src/superlocalmemory/core/recall_pipeline.py +0 -1337
- package/src/superlocalmemory/core/recall_queue.py +0 -377
- package/src/superlocalmemory/core/recall_worker.py +0 -414
- package/src/superlocalmemory/core/registry.py +0 -121
- package/src/superlocalmemory/core/remember_admission.py +0 -161
- package/src/superlocalmemory/core/remember_runtime.py +0 -1190
- package/src/superlocalmemory/core/remote_mode.py +0 -214
- package/src/superlocalmemory/core/reranker_worker.py +0 -338
- package/src/superlocalmemory/core/safe_fs.py +0 -108
- package/src/superlocalmemory/core/scale_autopromote.py +0 -196
- package/src/superlocalmemory/core/scale_engine.py +0 -915
- package/src/superlocalmemory/core/score_contract.py +0 -82
- package/src/superlocalmemory/core/security_primitives.py +0 -672
- package/src/superlocalmemory/core/session_identity.py +0 -98
- package/src/superlocalmemory/core/shadow_router.py +0 -319
- package/src/superlocalmemory/core/slm_disabled.py +0 -85
- package/src/superlocalmemory/core/status_contract.py +0 -108
- package/src/superlocalmemory/core/store_pipeline.py +0 -1404
- package/src/superlocalmemory/core/summarizer.py +0 -200
- package/src/superlocalmemory/core/tier_manager.py +0 -461
- package/src/superlocalmemory/core/topic_signature.py +0 -156
- package/src/superlocalmemory/core/transactions/__init__.py +0 -78
- package/src/superlocalmemory/core/transactions/concrete_owners.py +0 -604
- package/src/superlocalmemory/core/transactions/erasure.py +0 -825
- package/src/superlocalmemory/core/transactions/manifest.py +0 -255
- package/src/superlocalmemory/core/transactions/manifest_key.py +0 -155
- package/src/superlocalmemory/core/transactions/obligations.py +0 -272
- package/src/superlocalmemory/core/transactions/owners.py +0 -114
- package/src/superlocalmemory/core/transactions/reconciler.py +0 -285
- package/src/superlocalmemory/core/transactions/service.py +0 -330
- package/src/superlocalmemory/core/worker_pool.py +0 -377
- package/src/superlocalmemory/core/working_memory.py +0 -288
- package/src/superlocalmemory/dynamics/__init__.py +0 -0
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +0 -374
- package/src/superlocalmemory/dynamics/eap_scheduler.py +0 -294
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +0 -171
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +0 -227
- package/src/superlocalmemory/encoding/__init__.py +0 -0
- package/src/superlocalmemory/encoding/auto_linker.py +0 -308
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +0 -899
- package/src/superlocalmemory/encoding/consolidator.py +0 -472
- package/src/superlocalmemory/encoding/context_generator.py +0 -175
- package/src/superlocalmemory/encoding/emotional.py +0 -189
- package/src/superlocalmemory/encoding/entity_reflexion.py +0 -200
- package/src/superlocalmemory/encoding/entity_resolver.py +0 -687
- package/src/superlocalmemory/encoding/entropy_gate.py +0 -101
- package/src/superlocalmemory/encoding/fact_extractor.py +0 -877
- package/src/superlocalmemory/encoding/foresight.py +0 -93
- package/src/superlocalmemory/encoding/graph_builder.py +0 -346
- package/src/superlocalmemory/encoding/observation_builder.py +0 -177
- package/src/superlocalmemory/encoding/prospective_markers.py +0 -262
- package/src/superlocalmemory/encoding/scene_builder.py +0 -410
- package/src/superlocalmemory/encoding/signal_inference.py +0 -90
- package/src/superlocalmemory/encoding/temporal_parser.py +0 -432
- package/src/superlocalmemory/encoding/temporal_validator.py +0 -572
- package/src/superlocalmemory/encoding/type_router.py +0 -237
- package/src/superlocalmemory/evaluation/__init__.py +0 -13
- package/src/superlocalmemory/evaluation/calibration.py +0 -308
- package/src/superlocalmemory/evolution/__init__.py +0 -29
- package/src/superlocalmemory/evolution/blind_verifier.py +0 -122
- package/src/superlocalmemory/evolution/budget.py +0 -356
- package/src/superlocalmemory/evolution/evolution_store.py +0 -619
- package/src/superlocalmemory/evolution/llm_dispatch.py +0 -559
- package/src/superlocalmemory/evolution/model_selection.py +0 -175
- package/src/superlocalmemory/evolution/mutation_generator.py +0 -226
- package/src/superlocalmemory/evolution/skill_activator.py +0 -270
- package/src/superlocalmemory/evolution/skill_evolver.py +0 -928
- package/src/superlocalmemory/evolution/triggers.py +0 -376
- package/src/superlocalmemory/evolution/types.py +0 -114
- package/src/superlocalmemory/graph/__init__.py +0 -9
- package/src/superlocalmemory/graph/cozo_adjacency.py +0 -122
- package/src/superlocalmemory/graph/cozo_backend.py +0 -751
- package/src/superlocalmemory/hooks/__init__.py +0 -3
- package/src/superlocalmemory/hooks/_outcome_common.py +0 -523
- package/src/superlocalmemory/hooks/adapter_base.py +0 -347
- package/src/superlocalmemory/hooks/antigravity_adapter.py +0 -171
- package/src/superlocalmemory/hooks/auto_capture.py +0 -133
- package/src/superlocalmemory/hooks/auto_invoker.py +0 -521
- package/src/superlocalmemory/hooks/auto_parameterize.py +0 -147
- package/src/superlocalmemory/hooks/auto_recall.py +0 -191
- package/src/superlocalmemory/hooks/auto_recall_hook.py +0 -251
- package/src/superlocalmemory/hooks/before_web_hook.py +0 -131
- package/src/superlocalmemory/hooks/claude_code_hooks.py +0 -637
- package/src/superlocalmemory/hooks/codex_assets.py +0 -251
- package/src/superlocalmemory/hooks/codex_hooks.py +0 -186
- package/src/superlocalmemory/hooks/context_payload.py +0 -311
- package/src/superlocalmemory/hooks/copilot_adapter.py +0 -208
- package/src/superlocalmemory/hooks/cross_platform_connector.py +0 -90
- package/src/superlocalmemory/hooks/cursor_adapter.py +0 -173
- package/src/superlocalmemory/hooks/hook_daemon.py +0 -295
- package/src/superlocalmemory/hooks/hook_handlers.py +0 -822
- package/src/superlocalmemory/hooks/ide_connector.py +0 -246
- package/src/superlocalmemory/hooks/memory_protocol.py +0 -158
- package/src/superlocalmemory/hooks/portable_kit.py +0 -755
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +0 -183
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +0 -351
- package/src/superlocalmemory/hooks/prewarm_auth.py +0 -187
- package/src/superlocalmemory/hooks/rules_engine.py +0 -99
- package/src/superlocalmemory/hooks/session_registry.py +0 -330
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +0 -138
- package/src/superlocalmemory/hooks/sync_loop.py +0 -114
- package/src/superlocalmemory/hooks/topic_shift_hook.py +0 -302
- package/src/superlocalmemory/hooks/user_prompt_hook.py +0 -131
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +0 -210
- package/src/superlocalmemory/infra/__init__.py +0 -3
- package/src/superlocalmemory/infra/auth_middleware.py +0 -145
- package/src/superlocalmemory/infra/backup.py +0 -974
- package/src/superlocalmemory/infra/backup_obligations.py +0 -423
- package/src/superlocalmemory/infra/cache_manager.py +0 -267
- package/src/superlocalmemory/infra/cloud_backup.py +0 -788
- package/src/superlocalmemory/infra/daemon_identity.py +0 -300
- package/src/superlocalmemory/infra/data_root.py +0 -238
- package/src/superlocalmemory/infra/event_bus.py +0 -637
- package/src/superlocalmemory/infra/local_diagnostics.py +0 -327
- package/src/superlocalmemory/infra/pid_manager.py +0 -193
- package/src/superlocalmemory/infra/process_identity.py +0 -180
- package/src/superlocalmemory/infra/process_reaper.py +0 -624
- package/src/superlocalmemory/infra/rate_limiter.py +0 -228
- package/src/superlocalmemory/infra/self_heal.py +0 -401
- package/src/superlocalmemory/infra/version_integrity.py +0 -229
- package/src/superlocalmemory/ingestion/__init__.py +0 -13
- package/src/superlocalmemory/ingestion/adapter_manager.py +0 -255
- package/src/superlocalmemory/ingestion/base_adapter.py +0 -171
- package/src/superlocalmemory/ingestion/calendar_adapter.py +0 -349
- package/src/superlocalmemory/ingestion/credentials.py +0 -125
- package/src/superlocalmemory/ingestion/gmail_adapter.py +0 -378
- package/src/superlocalmemory/ingestion/parsers.py +0 -100
- package/src/superlocalmemory/ingestion/transcript_adapter.py +0 -161
- package/src/superlocalmemory/integrations/__init__.py +0 -1
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +0 -431
- package/src/superlocalmemory/integrations/bounded_loops_v051.py +0 -236
- package/src/superlocalmemory/learning/__init__.py +0 -0
- package/src/superlocalmemory/learning/adaptive.py +0 -172
- package/src/superlocalmemory/learning/arm_catalog.py +0 -97
- package/src/superlocalmemory/learning/assertion_miner.py +0 -403
- package/src/superlocalmemory/learning/bandit.py +0 -654
- package/src/superlocalmemory/learning/bandit_cache.py +0 -131
- package/src/superlocalmemory/learning/behavioral.py +0 -542
- package/src/superlocalmemory/learning/bootstrap.py +0 -298
- package/src/superlocalmemory/learning/consolidation_cycle.py +0 -398
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +0 -115
- package/src/superlocalmemory/learning/consolidation_worker.py +0 -261
- package/src/superlocalmemory/learning/cross_project.py +0 -408
- package/src/superlocalmemory/learning/database.py +0 -698
- package/src/superlocalmemory/learning/dedup_hnsw.py +0 -413
- package/src/superlocalmemory/learning/engagement.py +0 -487
- package/src/superlocalmemory/learning/engagement_features.py +0 -279
- package/src/superlocalmemory/learning/ensemble.py +0 -309
- package/src/superlocalmemory/learning/entity_compiler.py +0 -356
- package/src/superlocalmemory/learning/fact_outcome_joins.py +0 -207
- package/src/superlocalmemory/learning/features.py +0 -138
- package/src/superlocalmemory/learning/feedback.py +0 -724
- package/src/superlocalmemory/learning/forgetting_scheduler.py +0 -375
- package/src/superlocalmemory/learning/hnsw_dedup.py +0 -69
- package/src/superlocalmemory/learning/labeler.py +0 -85
- package/src/superlocalmemory/learning/legacy_migration.py +0 -316
- package/src/superlocalmemory/learning/lightgbm_subprocess.py +0 -236
- package/src/superlocalmemory/learning/memory_merge.py +0 -175
- package/src/superlocalmemory/learning/model_cache.py +0 -267
- package/src/superlocalmemory/learning/model_rollback.py +0 -281
- package/src/superlocalmemory/learning/outcome_queue.py +0 -306
- package/src/superlocalmemory/learning/outcomes.py +0 -286
- package/src/superlocalmemory/learning/pattern_miner.py +0 -465
- package/src/superlocalmemory/learning/pattern_miner_constants.py +0 -90
- package/src/superlocalmemory/learning/pcos.py +0 -291
- package/src/superlocalmemory/learning/project_context.py +0 -366
- package/src/superlocalmemory/learning/propensity.py +0 -131
- package/src/superlocalmemory/learning/ranker.py +0 -300
- package/src/superlocalmemory/learning/ranker_common.py +0 -163
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +0 -210
- package/src/superlocalmemory/learning/ranker_retrain_online.py +0 -423
- package/src/superlocalmemory/learning/reward.py +0 -888
- package/src/superlocalmemory/learning/reward_archive.py +0 -223
- package/src/superlocalmemory/learning/reward_boost.py +0 -211
- package/src/superlocalmemory/learning/reward_from_outcomes.py +0 -365
- package/src/superlocalmemory/learning/reward_model.py +0 -144
- package/src/superlocalmemory/learning/reward_proxy.py +0 -578
- package/src/superlocalmemory/learning/shadow_test.py +0 -524
- package/src/superlocalmemory/learning/signal_kinds.py +0 -79
- package/src/superlocalmemory/learning/signal_worker.py +0 -268
- package/src/superlocalmemory/learning/signals.py +0 -646
- package/src/superlocalmemory/learning/skill_performance_miner.py +0 -422
- package/src/superlocalmemory/learning/source_quality.py +0 -828
- package/src/superlocalmemory/learning/trigram_index.py +0 -548
- package/src/superlocalmemory/learning/workflows.py +0 -309
- package/src/superlocalmemory/llm/__init__.py +0 -0
- package/src/superlocalmemory/llm/backbone.py +0 -364
- package/src/superlocalmemory/loops/__init__.py +0 -58
- package/src/superlocalmemory/loops/budget.py +0 -58
- package/src/superlocalmemory/loops/engine.py +0 -174
- package/src/superlocalmemory/loops/ledger.py +0 -298
- package/src/superlocalmemory/loops/models.py +0 -152
- package/src/superlocalmemory/loops/rules.py +0 -52
- package/src/superlocalmemory/math/__init__.py +0 -0
- package/src/superlocalmemory/math/ebbinghaus.py +0 -352
- package/src/superlocalmemory/math/fisher.py +0 -356
- package/src/superlocalmemory/math/fisher_quantized.py +0 -255
- package/src/superlocalmemory/math/hopfield.py +0 -282
- package/src/superlocalmemory/math/langevin.py +0 -411
- package/src/superlocalmemory/math/polar_quant.py +0 -414
- package/src/superlocalmemory/math/qjl.py +0 -115
- package/src/superlocalmemory/math/sheaf.py +0 -261
- package/src/superlocalmemory/math/turbo_quant.py +0 -318
- package/src/superlocalmemory/mcp/__init__.py +0 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +0 -203
- package/src/superlocalmemory/mcp/_pool_adapter.py +0 -181
- package/src/superlocalmemory/mcp/_stdin_guard.py +0 -60
- package/src/superlocalmemory/mcp/agent_context.py +0 -115
- package/src/superlocalmemory/mcp/cli_fallback.py +0 -602
- package/src/superlocalmemory/mcp/http_transport.py +0 -85
- package/src/superlocalmemory/mcp/profiles.py +0 -154
- package/src/superlocalmemory/mcp/resources.py +0 -281
- package/src/superlocalmemory/mcp/server.py +0 -480
- package/src/superlocalmemory/mcp/session_binding.py +0 -98
- package/src/superlocalmemory/mcp/shared.py +0 -112
- package/src/superlocalmemory/mcp/tools.py +0 -18
- package/src/superlocalmemory/mcp/tools_active.py +0 -958
- package/src/superlocalmemory/mcp/tools_brain.py +0 -298
- package/src/superlocalmemory/mcp/tools_code_graph.py +0 -1717
- package/src/superlocalmemory/mcp/tools_context.py +0 -239
- package/src/superlocalmemory/mcp/tools_core.py +0 -1112
- package/src/superlocalmemory/mcp/tools_evolution.py +0 -343
- package/src/superlocalmemory/mcp/tools_learning.py +0 -393
- package/src/superlocalmemory/mcp/tools_loops.py +0 -345
- package/src/superlocalmemory/mcp/tools_mesh.py +0 -429
- package/src/superlocalmemory/mcp/tools_ops.py +0 -115
- package/src/superlocalmemory/mcp/tools_optimize.py +0 -322
- package/src/superlocalmemory/mcp/tools_summaries.py +0 -147
- package/src/superlocalmemory/mcp/tools_v28.py +0 -292
- package/src/superlocalmemory/mcp/tools_v3.py +0 -398
- package/src/superlocalmemory/mcp/tools_v33.py +0 -507
- package/src/superlocalmemory/mesh/__init__.py +0 -12
- package/src/superlocalmemory/mesh/broker.py +0 -812
- package/src/superlocalmemory/mesh/broker_security.py +0 -470
- package/src/superlocalmemory/mesh/discovery.py +0 -365
- package/src/superlocalmemory/mesh/lock_protocol.py +0 -313
- package/src/superlocalmemory/mesh/node_identity.py +0 -97
- package/src/superlocalmemory/mesh/outbox_remote.py +0 -429
- package/src/superlocalmemory/mesh/remote_sync.py +0 -829
- package/src/superlocalmemory/mesh/state_sync.py +0 -286
- package/src/superlocalmemory/migrations/__init__.py +0 -5
- package/src/superlocalmemory/migrations/v3_4_25_to_v3_4_26.py +0 -144
- package/src/superlocalmemory/optimize/NOTICE +0 -6
- package/src/superlocalmemory/optimize/__init__.py +0 -0
- package/src/superlocalmemory/optimize/adapters/__init__.py +0 -68
- package/src/superlocalmemory/optimize/adapters/_agent_registry.py +0 -120
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +0 -112
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +0 -122
- package/src/superlocalmemory/optimize/adapters/wrap.py +0 -228
- package/src/superlocalmemory/optimize/cache/__init__.py +0 -31
- package/src/superlocalmemory/optimize/cache/boundary_store.py +0 -488
- package/src/superlocalmemory/optimize/cache/centroid_store.py +0 -199
- package/src/superlocalmemory/optimize/cache/context_key.py +0 -67
- package/src/superlocalmemory/optimize/cache/exact.py +0 -88
- package/src/superlocalmemory/optimize/cache/invalidation.py +0 -36
- package/src/superlocalmemory/optimize/cache/key_builder.py +0 -111
- package/src/superlocalmemory/optimize/cache/manager.py +0 -737
- package/src/superlocalmemory/optimize/cache/semantic.py +0 -637
- package/src/superlocalmemory/optimize/cache/stampede.py +0 -50
- package/src/superlocalmemory/optimize/compress/__init__.py +0 -17
- package/src/superlocalmemory/optimize/compress/align.py +0 -159
- package/src/superlocalmemory/optimize/compress/ccr.py +0 -116
- package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +0 -71
- package/src/superlocalmemory/optimize/compress/router.py +0 -667
- package/src/superlocalmemory/optimize/config/__init__.py +0 -56
- package/src/superlocalmemory/optimize/config/defaults.py +0 -43
- package/src/superlocalmemory/optimize/config/schema.py +0 -327
- package/src/superlocalmemory/optimize/config/store.py +0 -270
- package/src/superlocalmemory/optimize/metrics/__init__.py +0 -8
- package/src/superlocalmemory/optimize/metrics/counters.py +0 -155
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -87
- package/src/superlocalmemory/optimize/metrics/exporters.py +0 -77
- package/src/superlocalmemory/optimize/metrics/persistence.py +0 -115
- package/src/superlocalmemory/optimize/proxy/__init__.py +0 -28
- package/src/superlocalmemory/optimize/proxy/_helpers.py +0 -730
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +0 -375
- package/src/superlocalmemory/optimize/proxy/capture.py +0 -550
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +0 -528
- package/src/superlocalmemory/optimize/proxy/lifecycle.py +0 -126
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +0 -465
- package/src/superlocalmemory/optimize/proxy/server.py +0 -199
- package/src/superlocalmemory/optimize/proxy/vertex_surface.py +0 -246
- package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
- package/src/superlocalmemory/optimize/storage/db.py +0 -1185
- package/src/superlocalmemory/optimize/storage/schema.py +0 -205
- package/src/superlocalmemory/parameterization/__init__.py +0 -47
- package/src/superlocalmemory/parameterization/cross_project.py +0 -12
- package/src/superlocalmemory/parameterization/pattern_extractor.py +0 -584
- package/src/superlocalmemory/parameterization/pii_filter.py +0 -106
- package/src/superlocalmemory/parameterization/prompt_injector.py +0 -219
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +0 -281
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +0 -542
- package/src/superlocalmemory/parameterization/workflow_miner.py +0 -17
- package/src/superlocalmemory/reliability/__init__.py +0 -45
- package/src/superlocalmemory/reliability/join_liveness.py +0 -301
- package/src/superlocalmemory/reliability/prior_distance.py +0 -243
- package/src/superlocalmemory/retrieval/__init__.py +0 -0
- package/src/superlocalmemory/retrieval/agentic.py +0 -367
- package/src/superlocalmemory/retrieval/ann_index.py +0 -235
- package/src/superlocalmemory/retrieval/bm25_channel.py +0 -451
- package/src/superlocalmemory/retrieval/bridge_discovery.py +0 -253
- package/src/superlocalmemory/retrieval/channel_registry.py +0 -154
- package/src/superlocalmemory/retrieval/channel_status.py +0 -117
- package/src/superlocalmemory/retrieval/engine.py +0 -1615
- package/src/superlocalmemory/retrieval/entity_channel.py +0 -994
- package/src/superlocalmemory/retrieval/forgetting_filter.py +0 -160
- package/src/superlocalmemory/retrieval/fusion.py +0 -81
- package/src/superlocalmemory/retrieval/graph_adjacency.py +0 -219
- package/src/superlocalmemory/retrieval/hopfield_channel.py +0 -465
- package/src/superlocalmemory/retrieval/profile_channel.py +0 -105
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +0 -147
- package/src/superlocalmemory/retrieval/remote_reranker.py +0 -758
- package/src/superlocalmemory/retrieval/reranker.py +0 -674
- package/src/superlocalmemory/retrieval/scope_policy.py +0 -126
- package/src/superlocalmemory/retrieval/semantic_channel.py +0 -638
- package/src/superlocalmemory/retrieval/spreading.py +0 -288
- package/src/superlocalmemory/retrieval/spreading_activation.py +0 -616
- package/src/superlocalmemory/retrieval/strategy.py +0 -248
- package/src/superlocalmemory/retrieval/temporal_channel.py +0 -433
- package/src/superlocalmemory/retrieval/temporal_frame.py +0 -102
- package/src/superlocalmemory/retrieval/temporal_utils.py +0 -122
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +0 -499
- package/src/superlocalmemory/retrieval/time_window.py +0 -181
- package/src/superlocalmemory/retrieval/vector_store.py +0 -863
- package/src/superlocalmemory/server/__init__.py +0 -1
- package/src/superlocalmemory/server/api.py +0 -310
- package/src/superlocalmemory/server/asset_versions.py +0 -171
- package/src/superlocalmemory/server/bandit_loops.py +0 -158
- package/src/superlocalmemory/server/config_file.py +0 -90
- package/src/superlocalmemory/server/consolidation_runner.py +0 -140
- package/src/superlocalmemory/server/egress_policy.py +0 -258
- package/src/superlocalmemory/server/loopback.py +0 -85
- package/src/superlocalmemory/server/middleware/__init__.py +0 -11
- package/src/superlocalmemory/server/middleware/security_headers.py +0 -144
- package/src/superlocalmemory/server/origin.py +0 -55
- package/src/superlocalmemory/server/profile_runtime.py +0 -515
- package/src/superlocalmemory/server/rbac_enforce.py +0 -194
- package/src/superlocalmemory/server/recall_health.py +0 -343
- package/src/superlocalmemory/server/recall_serializer.py +0 -320
- package/src/superlocalmemory/server/route_mutations.py +0 -104
- package/src/superlocalmemory/server/routes/__init__.py +0 -4
- package/src/superlocalmemory/server/routes/abstraction.py +0 -314
- package/src/superlocalmemory/server/routes/adapters.py +0 -63
- package/src/superlocalmemory/server/routes/agents.py +0 -303
- package/src/superlocalmemory/server/routes/backup.py +0 -869
- package/src/superlocalmemory/server/routes/behavioral.py +0 -659
- package/src/superlocalmemory/server/routes/brain.py +0 -1892
- package/src/superlocalmemory/server/routes/chat.py +0 -393
- package/src/superlocalmemory/server/routes/compliance.py +0 -533
- package/src/superlocalmemory/server/routes/config_api.py +0 -703
- package/src/superlocalmemory/server/routes/data_io.py +0 -329
- package/src/superlocalmemory/server/routes/entity.py +0 -237
- package/src/superlocalmemory/server/routes/events.py +0 -214
- package/src/superlocalmemory/server/routes/evolution.py +0 -510
- package/src/superlocalmemory/server/routes/helpers.py +0 -499
- package/src/superlocalmemory/server/routes/ingest.py +0 -137
- package/src/superlocalmemory/server/routes/insights.py +0 -366
- package/src/superlocalmemory/server/routes/learning.py +0 -834
- package/src/superlocalmemory/server/routes/learning_telemetry.py +0 -154
- package/src/superlocalmemory/server/routes/lifecycle.py +0 -184
- package/src/superlocalmemory/server/routes/memories.py +0 -1661
- package/src/superlocalmemory/server/routes/mesh.py +0 -517
- package/src/superlocalmemory/server/routes/mesh_lock.py +0 -54
- package/src/superlocalmemory/server/routes/mesh_state.py +0 -63
- package/src/superlocalmemory/server/routes/optimize.py +0 -197
- package/src/superlocalmemory/server/routes/prewarm.py +0 -173
- package/src/superlocalmemory/server/routes/profiles.py +0 -292
- package/src/superlocalmemory/server/routes/ratelimit.py +0 -132
- package/src/superlocalmemory/server/routes/rbac.py +0 -366
- package/src/superlocalmemory/server/routes/stats.py +0 -385
- package/src/superlocalmemory/server/routes/tiers.py +0 -222
- package/src/superlocalmemory/server/routes/timeline.py +0 -258
- package/src/superlocalmemory/server/routes/token.py +0 -90
- package/src/superlocalmemory/server/routes/v3_api.py +0 -3023
- package/src/superlocalmemory/server/routes/ws.py +0 -171
- package/src/superlocalmemory/server/security_middleware.py +0 -89
- package/src/superlocalmemory/server/ui.py +0 -354
- package/src/superlocalmemory/server/unified_daemon.py +0 -6326
- package/src/superlocalmemory/server/write_identity.py +0 -195
- package/src/superlocalmemory/storage/__init__.py +0 -0
- package/src/superlocalmemory/storage/_migration_internals.py +0 -638
- package/src/superlocalmemory/storage/_schema_version.py +0 -174
- package/src/superlocalmemory/storage/access_log.py +0 -170
- package/src/superlocalmemory/storage/admission_codec.py +0 -129
- package/src/superlocalmemory/storage/admission_journal.py +0 -843
- package/src/superlocalmemory/storage/agent_experience.py +0 -546
- package/src/superlocalmemory/storage/backup.py +0 -531
- package/src/superlocalmemory/storage/correction_cases.py +0 -670
- package/src/superlocalmemory/storage/database.py +0 -3180
- package/src/superlocalmemory/storage/deferred_writes.py +0 -209
- package/src/superlocalmemory/storage/embedding_codec.py +0 -200
- package/src/superlocalmemory/storage/embedding_migrator.py +0 -672
- package/src/superlocalmemory/storage/erasure_fence.py +0 -45
- package/src/superlocalmemory/storage/execution_learning.py +0 -285
- package/src/superlocalmemory/storage/external_evidence.py +0 -359
- package/src/superlocalmemory/storage/generation_fence.py +0 -63
- package/src/superlocalmemory/storage/lineage_retention.py +0 -236
- package/src/superlocalmemory/storage/logical_edges.py +0 -86
- package/src/superlocalmemory/storage/memory_write.py +0 -115
- package/src/superlocalmemory/storage/migration_runner.py +0 -895
- package/src/superlocalmemory/storage/migration_v33.py +0 -140
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +0 -67
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +0 -107
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +0 -38
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +0 -46
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +0 -75
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +0 -75
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +0 -63
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +0 -94
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +0 -80
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +0 -87
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +0 -72
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +0 -55
- package/src/superlocalmemory/storage/migrations/M014_v345_scale_ready.py +0 -45
- package/src/superlocalmemory/storage/migrations/M015_add_pinned_column.py +0 -58
- package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +0 -120
- package/src/superlocalmemory/storage/migrations/M017_ccq_scope_column.py +0 -79
- package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +0 -120
- package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +0 -54
- package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +0 -52
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +0 -108
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +0 -86
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +0 -194
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +0 -87
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +0 -90
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +0 -136
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +0 -163
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +0 -305
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +0 -137
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +0 -93
- package/src/superlocalmemory/storage/migrations/M031_dead_letter_operations.py +0 -80
- package/src/superlocalmemory/storage/migrations/M032_write_coordinator_admission.py +0 -188
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +0 -148
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +0 -58
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +0 -113
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +0 -107
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +0 -162
- package/src/superlocalmemory/storage/migrations/M038_learning_feedback_channel.py +0 -77
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +0 -137
- package/src/superlocalmemory/storage/migrations/M040_agent_experience_receipts.py +0 -254
- package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +0 -189
- package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +0 -245
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +0 -512
- package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +0 -127
- package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +0 -158
- package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +0 -620
- package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +0 -306
- package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +0 -229
- package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +0 -201
- package/src/superlocalmemory/storage/migrations/M050_execution_learning_v2.py +0 -70
- package/src/superlocalmemory/storage/migrations/__init__.py +0 -103
- package/src/superlocalmemory/storage/migrations.py +0 -333
- package/src/superlocalmemory/storage/models.py +0 -500
- package/src/superlocalmemory/storage/projection_outbox.py +0 -346
- package/src/superlocalmemory/storage/quantized_store.py +0 -280
- package/src/superlocalmemory/storage/read_connection.py +0 -115
- package/src/superlocalmemory/storage/retention_policy.py +0 -860
- package/src/superlocalmemory/storage/schema.py +0 -1108
- package/src/superlocalmemory/storage/schema_code_graph.py +0 -282
- package/src/superlocalmemory/storage/schema_v32.py +0 -382
- package/src/superlocalmemory/storage/schema_v3410.py +0 -159
- package/src/superlocalmemory/storage/schema_v3411.py +0 -149
- package/src/superlocalmemory/storage/schema_v343.py +0 -315
- package/src/superlocalmemory/storage/schema_v345.py +0 -109
- package/src/superlocalmemory/storage/schema_v347.py +0 -140
- package/src/superlocalmemory/storage/sqlite_vectors.py +0 -169
- package/src/superlocalmemory/storage/v2_migrator.py +0 -466
- package/src/superlocalmemory/storage/write_coordinator.py +0 -949
- package/src/superlocalmemory/storage/write_lock.py +0 -88
- package/src/superlocalmemory/summaries/__init__.py +0 -37
- package/src/superlocalmemory/summaries/base.py +0 -267
- package/src/superlocalmemory/summaries/daily_reflection.py +0 -340
- package/src/superlocalmemory/summaries/non_answer.py +0 -223
- package/src/superlocalmemory/summaries/project_work_log.py +0 -440
- package/src/superlocalmemory/summaries/session_summary.py +0 -311
- package/src/superlocalmemory/trust/__init__.py +0 -0
- package/src/superlocalmemory/trust/gate.py +0 -171
- package/src/superlocalmemory/trust/provenance.py +0 -124
- package/src/superlocalmemory/trust/scorer.py +0 -413
- package/src/superlocalmemory/trust/signals.py +0 -153
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +0 -64
- package/src/superlocalmemory/ui/assets/slm-icon.svg +0 -36
- package/src/superlocalmemory/ui/css/brain.css +0 -409
- package/src/superlocalmemory/ui/css/design-system.css +0 -696
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +0 -663
- package/src/superlocalmemory/ui/css/neural-glass.css +0 -1599
- package/src/superlocalmemory/ui/css/od-bridge.css +0 -158
- package/src/superlocalmemory/ui/favicon.svg +0 -36
- package/src/superlocalmemory/ui/index.html +0 -1638
- package/src/superlocalmemory/ui/js/agents.js +0 -192
- package/src/superlocalmemory/ui/js/auto-settings.js +0 -624
- package/src/superlocalmemory/ui/js/brain.js +0 -1400
- package/src/superlocalmemory/ui/js/clusters.js +0 -326
- package/src/superlocalmemory/ui/js/compliance.js +0 -307
- package/src/superlocalmemory/ui/js/core.js +0 -566
- package/src/superlocalmemory/ui/js/dashboard.js +0 -503
- package/src/superlocalmemory/ui/js/event-delegation.js +0 -113
- package/src/superlocalmemory/ui/js/events.js +0 -178
- package/src/superlocalmemory/ui/js/fact-detail.js +0 -142
- package/src/superlocalmemory/ui/js/feedback.js +0 -339
- package/src/superlocalmemory/ui/js/graph-event-bus.js +0 -83
- package/src/superlocalmemory/ui/js/graph-filters.js +0 -220
- package/src/superlocalmemory/ui/js/graph-ui.js +0 -214
- package/src/superlocalmemory/ui/js/ide-status.js +0 -115
- package/src/superlocalmemory/ui/js/init.js +0 -54
- package/src/superlocalmemory/ui/js/knowledge-graph.js +0 -945
- package/src/superlocalmemory/ui/js/lifecycle.js +0 -387
- package/src/superlocalmemory/ui/js/math-health.js +0 -114
- package/src/superlocalmemory/ui/js/memories.js +0 -394
- package/src/superlocalmemory/ui/js/memory-chat.js +0 -371
- package/src/superlocalmemory/ui/js/memory-timeline.js +0 -265
- package/src/superlocalmemory/ui/js/modal.js +0 -733
- package/src/superlocalmemory/ui/js/ng-entities.js +0 -298
- package/src/superlocalmemory/ui/js/ng-health.js +0 -208
- package/src/superlocalmemory/ui/js/ng-ingestion.js +0 -203
- package/src/superlocalmemory/ui/js/ng-mesh.js +0 -374
- package/src/superlocalmemory/ui/js/ng-shell.js +0 -524
- package/src/superlocalmemory/ui/js/ng-skills.js +0 -663
- package/src/superlocalmemory/ui/js/od-agents.js +0 -588
- package/src/superlocalmemory/ui/js/od-auth-gate.js +0 -257
- package/src/superlocalmemory/ui/js/od-backup.js +0 -878
- package/src/superlocalmemory/ui/js/od-boundedloops.js +0 -324
- package/src/superlocalmemory/ui/js/od-brain.js +0 -1095
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +0 -301
- package/src/superlocalmemory/ui/js/od-components.js +0 -147
- package/src/superlocalmemory/ui/js/od-entities.js +0 -622
- package/src/superlocalmemory/ui/js/od-graph.js +0 -776
- package/src/superlocalmemory/ui/js/od-health.js +0 -579
- package/src/superlocalmemory/ui/js/od-mcp.js +0 -508
- package/src/superlocalmemory/ui/js/od-memories.js +0 -1499
- package/src/superlocalmemory/ui/js/od-mesh.js +0 -645
- package/src/superlocalmemory/ui/js/od-operations.js +0 -1268
- package/src/superlocalmemory/ui/js/od-ops-health.js +0 -417
- package/src/superlocalmemory/ui/js/od-optimize.js +0 -828
- package/src/superlocalmemory/ui/js/od-settings.js +0 -1275
- package/src/superlocalmemory/ui/js/od-shell.js +0 -819
- package/src/superlocalmemory/ui/js/od-skills.js +0 -600
- package/src/superlocalmemory/ui/js/od-team.js +0 -265
- package/src/superlocalmemory/ui/js/optimize.js +0 -191
- package/src/superlocalmemory/ui/js/profiles.js +0 -362
- package/src/superlocalmemory/ui/js/quick-actions.js +0 -334
- package/src/superlocalmemory/ui/js/recall-lab.js +0 -373
- package/src/superlocalmemory/ui/js/search.js +0 -86
- package/src/superlocalmemory/ui/js/settings.js +0 -556
- package/src/superlocalmemory/ui/js/timeline.js +0 -62
- package/src/superlocalmemory/ui/js/trust-dashboard.js +0 -225
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +0 -2018
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +0 -7
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +0 -6
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +0 -2
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +0 -2
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +0 -2
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +0 -8
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +0 -1
- package/src/superlocalmemory/vector/__init__.py +0 -9
- package/src/superlocalmemory/vector/lancedb_backend.py +0 -366
|
@@ -1,1479 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
-
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
-
|
|
5
|
-
"""SuperLocalMemory V3 — GDPR Compliance.
|
|
6
|
-
|
|
7
|
-
Implements GDPR rights: right to access, right to erasure (forget),
|
|
8
|
-
right to data portability (export), and audit trail.
|
|
9
|
-
Profile-scoped. All operations logged to compliance_audit.
|
|
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 sqlite3
|
|
19
|
-
from datetime import UTC, datetime
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
|
|
22
|
-
logger = logging.getLogger(__name__)
|
|
23
|
-
|
|
24
|
-
# C1 — Backup residue obligations
|
|
25
|
-
# Imported lazily inside methods to avoid circular-import risk at module load.
|
|
26
|
-
# The sentinel guards against environments where infra.backup_obligations is
|
|
27
|
-
# unavailable (e.g. minimal test installs); compliance logic degrades safely.
|
|
28
|
-
_BACKUP_OBLIGATIONS_AVAILABLE: bool | None = None
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _get_backup_obligations_module():
|
|
32
|
-
"""Lazy import guard — returns module or None if unavailable."""
|
|
33
|
-
global _BACKUP_OBLIGATIONS_AVAILABLE
|
|
34
|
-
try:
|
|
35
|
-
import superlocalmemory.infra.backup_obligations as _m
|
|
36
|
-
_BACKUP_OBLIGATIONS_AVAILABLE = True
|
|
37
|
-
return _m
|
|
38
|
-
except Exception as exc: # noqa: BLE001
|
|
39
|
-
if _BACKUP_OBLIGATIONS_AVAILABLE is None:
|
|
40
|
-
logger.warning("backup_obligations module unavailable: %s", exc)
|
|
41
|
-
_BACKUP_OBLIGATIONS_AVAILABLE = False
|
|
42
|
-
return None
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _retention_days_from_config() -> int:
|
|
46
|
-
"""Read the configured obligation retention window (default 90 days).
|
|
47
|
-
|
|
48
|
-
Consumes ``SLMConfig.backup_retention_days`` or any attribute matching
|
|
49
|
-
('retention', 'retain') with numeric value on ``SLMConfig``. The config
|
|
50
|
-
field is owned by another agent; we consume it here and fall back to 90.
|
|
51
|
-
"""
|
|
52
|
-
try:
|
|
53
|
-
from superlocalmemory.core.config import SLMConfig
|
|
54
|
-
cfg = SLMConfig()
|
|
55
|
-
# Try the canonical field name first
|
|
56
|
-
for attr in ("backup_retention_days", "obligation_retention_days",
|
|
57
|
-
"retention_window_days", "backup_obligation_retention_days"):
|
|
58
|
-
val = getattr(cfg, attr, None)
|
|
59
|
-
if isinstance(val, int) and val > 0:
|
|
60
|
-
return val
|
|
61
|
-
# Fallback: search nested sub-configs
|
|
62
|
-
for attr in dir(cfg):
|
|
63
|
-
if attr.startswith("_"):
|
|
64
|
-
continue
|
|
65
|
-
sub = getattr(cfg, attr, None)
|
|
66
|
-
if not hasattr(sub, "__dict__") and not hasattr(sub, "__dataclass_fields__"):
|
|
67
|
-
continue
|
|
68
|
-
for sub_attr in dir(sub):
|
|
69
|
-
if any(h in sub_attr.lower() for h in ("retention", "retain")):
|
|
70
|
-
v = getattr(sub, sub_attr, None)
|
|
71
|
-
if isinstance(v, int) and v > 0:
|
|
72
|
-
return v
|
|
73
|
-
except Exception: # noqa: BLE001
|
|
74
|
-
pass
|
|
75
|
-
return 90 # owner-set default
|
|
76
|
-
|
|
77
|
-
# Friendly export keys → canonical table names (stable Art.20 export contract).
|
|
78
|
-
_EXPORT_ALIASES = {
|
|
79
|
-
"facts": "atomic_facts",
|
|
80
|
-
"entities": "canonical_entities",
|
|
81
|
-
"edges": "graph_edges",
|
|
82
|
-
"feedback": "feedback_records",
|
|
83
|
-
"scenes": "memory_scenes",
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _unproject_entity(entity_id: str) -> bool:
|
|
88
|
-
"""Remove an entity node from the graph projection, if there is one.
|
|
89
|
-
|
|
90
|
-
The projection is a separate storage engine, so deleting the row from
|
|
91
|
-
SQLite reaches nothing there. An entity node left behind still carries the
|
|
92
|
-
name it was created with, which is exactly the thing an erasure was asked
|
|
93
|
-
to remove.
|
|
94
|
-
|
|
95
|
-
Returns False when a projection exists and refused, so a caller that
|
|
96
|
-
reports completeness can report this honestly. No projection at all is not
|
|
97
|
-
a failure -- there is nothing to remove.
|
|
98
|
-
"""
|
|
99
|
-
try:
|
|
100
|
-
from superlocalmemory.core.backend_orchestrator import get_orchestrator
|
|
101
|
-
except Exception: # noqa: BLE001
|
|
102
|
-
return True
|
|
103
|
-
try:
|
|
104
|
-
orchestrator = get_orchestrator()
|
|
105
|
-
graph = orchestrator.get_graph_backend() if orchestrator else None
|
|
106
|
-
if graph is None:
|
|
107
|
-
return True
|
|
108
|
-
remove = getattr(graph, "remove_entity", None)
|
|
109
|
-
if remove is None:
|
|
110
|
-
return True
|
|
111
|
-
remove(entity_id)
|
|
112
|
-
return True
|
|
113
|
-
except Exception as exc: # noqa: BLE001
|
|
114
|
-
logger.warning(
|
|
115
|
-
"the graph projection still holds entity %s (%s)", entity_id[:12], exc,
|
|
116
|
-
)
|
|
117
|
-
return False
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
class GDPRCompliance:
|
|
121
|
-
"""GDPR compliance operations for memory data.
|
|
122
|
-
|
|
123
|
-
Supports:
|
|
124
|
-
- Right to Access (Art. 15): Export all data for a profile
|
|
125
|
-
- Right to Erasure (Art. 17): Delete all data for a profile/entity
|
|
126
|
-
- Right to Portability (Art. 20): Export in machine-readable format
|
|
127
|
-
- Audit Trail: Log all data operations
|
|
128
|
-
"""
|
|
129
|
-
|
|
130
|
-
# Tables that carry a profile_id column but are NOT tenant memory to be
|
|
131
|
-
# erased/exported wholesale.
|
|
132
|
-
# `profiles` — the tenant record (handled separately, deleted last).
|
|
133
|
-
# `erasure_receipts` — tamper-evident audit chain for Art.17 erasure events;
|
|
134
|
-
# must survive the profile wipe so operators can prove deletion occurred.
|
|
135
|
-
_NON_MEMORY_SCOPED = frozenset({"profiles", "erasure_receipts"})
|
|
136
|
-
|
|
137
|
-
def __init__(self, db, *, engine=None, data_root: str | Path | None = None) -> None:
|
|
138
|
-
self._db = db
|
|
139
|
-
self._engine = engine
|
|
140
|
-
self._data_root = Path(data_root).resolve() if data_root is not None else None
|
|
141
|
-
|
|
142
|
-
def _memory_has_siblings(self, memory_id: str, profile_id: str) -> bool:
|
|
143
|
-
try:
|
|
144
|
-
return bool(
|
|
145
|
-
self._db.execute(
|
|
146
|
-
"SELECT 1 FROM atomic_facts WHERE memory_id = ? AND profile_id = ? LIMIT 1",
|
|
147
|
-
(memory_id, profile_id),
|
|
148
|
-
)
|
|
149
|
-
)
|
|
150
|
-
except Exception:
|
|
151
|
-
return True
|
|
152
|
-
|
|
153
|
-
def _tombstone(self, fact_id: str, profile_id: str, memory_id: str | None) -> None:
|
|
154
|
-
try:
|
|
155
|
-
import time
|
|
156
|
-
import uuid
|
|
157
|
-
|
|
158
|
-
from superlocalmemory.core.transactions.erasure import write_tombstones
|
|
159
|
-
|
|
160
|
-
write_tombstones(
|
|
161
|
-
self._db,
|
|
162
|
-
profile_id,
|
|
163
|
-
(fact_id,),
|
|
164
|
-
uuid.uuid4().hex,
|
|
165
|
-
time.time(),
|
|
166
|
-
memory_id,
|
|
167
|
-
)
|
|
168
|
-
except Exception:
|
|
169
|
-
pass
|
|
170
|
-
|
|
171
|
-
def _purge_graph_and_vector_projections(
|
|
172
|
-
self, profile_id: str,
|
|
173
|
-
) -> tuple[int, int]:
|
|
174
|
-
"""Remove a tenant's facts from CozoDB and LanceDB. Returns (purged, failed).
|
|
175
|
-
|
|
176
|
-
These projections live in their own storage engines — RocksDB under Cozo,
|
|
177
|
-
Lance files under the vector store — so nothing about deleting rows from
|
|
178
|
-
SQLite reaches them. A memory left in the graph after erasure is still
|
|
179
|
-
reachable by recall, which makes the erasure receipt a false statement.
|
|
180
|
-
|
|
181
|
-
A store with no projection open returns ``(0, 0)``: there is nothing to
|
|
182
|
-
purge, which is not a failure. A projection that is open and refuses to
|
|
183
|
-
delete IS a failure and is counted, so ``erasure_complete`` cannot come
|
|
184
|
-
back true while a tenant's facts are still in the graph.
|
|
185
|
-
"""
|
|
186
|
-
try:
|
|
187
|
-
from superlocalmemory.core.backend_orchestrator import get_orchestrator
|
|
188
|
-
except Exception:
|
|
189
|
-
return 0, 0
|
|
190
|
-
orchestrator = get_orchestrator()
|
|
191
|
-
if orchestrator is None:
|
|
192
|
-
return 0, 0
|
|
193
|
-
graph = orchestrator.get_graph_backend()
|
|
194
|
-
vector = orchestrator.get_vector_backend()
|
|
195
|
-
if graph is None and vector is None:
|
|
196
|
-
return 0, 0
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
fact_ids = [
|
|
200
|
-
dict(r)["fact_id"]
|
|
201
|
-
for r in self._db.execute(
|
|
202
|
-
"SELECT fact_id FROM atomic_facts WHERE profile_id = ?",
|
|
203
|
-
(profile_id,),
|
|
204
|
-
)
|
|
205
|
-
]
|
|
206
|
-
except Exception as exc:
|
|
207
|
-
logger.warning("GDPR erase: could not list facts to unproject: %s", exc)
|
|
208
|
-
return 0, 1
|
|
209
|
-
|
|
210
|
-
purged = failures = 0
|
|
211
|
-
for fact_id in fact_ids:
|
|
212
|
-
removed = True
|
|
213
|
-
for backend, method in ((graph, "remove_fact"), (vector, "remove_vector")):
|
|
214
|
-
if backend is None:
|
|
215
|
-
continue
|
|
216
|
-
try:
|
|
217
|
-
getattr(backend, method)(fact_id)
|
|
218
|
-
except Exception as exc:
|
|
219
|
-
logger.warning(
|
|
220
|
-
"GDPR erase: %s failed for %s: %s", method, fact_id[:12], exc,
|
|
221
|
-
)
|
|
222
|
-
removed = False
|
|
223
|
-
if removed:
|
|
224
|
-
purged += 1
|
|
225
|
-
else:
|
|
226
|
-
failures += 1
|
|
227
|
-
return purged, failures
|
|
228
|
-
|
|
229
|
-
def _purge_vector_and_ann(self, profile_id: str) -> tuple[int, int]:
|
|
230
|
-
engine = self._engine
|
|
231
|
-
if engine is None:
|
|
232
|
-
return 0, 0
|
|
233
|
-
store = getattr(engine, "_vector_store", None)
|
|
234
|
-
ann = getattr(engine, "_ann_index", None)
|
|
235
|
-
|
|
236
|
-
purged = 0
|
|
237
|
-
failures = 0
|
|
238
|
-
|
|
239
|
-
try:
|
|
240
|
-
db_fact_ids = [
|
|
241
|
-
dict(r)["fact_id"]
|
|
242
|
-
for r in self._db.execute(
|
|
243
|
-
"SELECT fact_id FROM atomic_facts WHERE profile_id = ?",
|
|
244
|
-
(profile_id,),
|
|
245
|
-
)
|
|
246
|
-
]
|
|
247
|
-
except Exception as exc:
|
|
248
|
-
logger.warning("GDPR erase: fact_id enumeration failed: %s", exc)
|
|
249
|
-
db_fact_ids = []
|
|
250
|
-
failures += 1
|
|
251
|
-
|
|
252
|
-
store_available = store is not None and getattr(store, "available", False)
|
|
253
|
-
store_fact_ids: list[str] = []
|
|
254
|
-
if store_available:
|
|
255
|
-
try:
|
|
256
|
-
store_fact_ids = list(store.indexed_fact_ids(profile_id))
|
|
257
|
-
except Exception as exc:
|
|
258
|
-
logger.warning("GDPR erase: vector enumeration failed: %s", exc)
|
|
259
|
-
failures += 1
|
|
260
|
-
store_fact_ids = list(db_fact_ids)
|
|
261
|
-
for fid in store_fact_ids:
|
|
262
|
-
try:
|
|
263
|
-
if store.delete(fid):
|
|
264
|
-
purged += 1
|
|
265
|
-
else:
|
|
266
|
-
failures += 1
|
|
267
|
-
except Exception as exc:
|
|
268
|
-
logger.warning("GDPR erase: vector delete failed for %s: %s", fid, exc)
|
|
269
|
-
failures += 1
|
|
270
|
-
else:
|
|
271
|
-
# No usable vector backend: raw vec0/map payload cannot be removed.
|
|
272
|
-
# Count residual raw vectors as failures so the receipt cannot claim
|
|
273
|
-
# a complete erasure while physical vectors survive.
|
|
274
|
-
residue = self._count_vector_residue(profile_id)
|
|
275
|
-
if residue:
|
|
276
|
-
failures += residue
|
|
277
|
-
|
|
278
|
-
if ann is not None and hasattr(ann, "remove"):
|
|
279
|
-
all_to_purge = set(store_fact_ids) | set(db_fact_ids)
|
|
280
|
-
for fid in all_to_purge:
|
|
281
|
-
try:
|
|
282
|
-
ann.remove(fid)
|
|
283
|
-
except Exception as exc:
|
|
284
|
-
logger.warning("GDPR erase: ANN remove failed for %s: %s", fid, exc)
|
|
285
|
-
|
|
286
|
-
return purged, failures
|
|
287
|
-
|
|
288
|
-
def _count_vector_residue(self, profile_id: str) -> int:
|
|
289
|
-
total = 0
|
|
290
|
-
for table in ("vector_row_map", "embedding_metadata"):
|
|
291
|
-
try:
|
|
292
|
-
rows = self._db.execute(
|
|
293
|
-
f"SELECT COUNT(*) AS c FROM {table} WHERE profile_id = ?",
|
|
294
|
-
(profile_id,),
|
|
295
|
-
)
|
|
296
|
-
total = max(total, int(dict(rows[0])["c"]) if rows else 0)
|
|
297
|
-
except Exception:
|
|
298
|
-
continue
|
|
299
|
-
return total
|
|
300
|
-
|
|
301
|
-
def _fact_vector_residue(self, profile_id: str, fact_ids: list[str]) -> int:
|
|
302
|
-
if not fact_ids:
|
|
303
|
-
return 0
|
|
304
|
-
residue: set[str] = set()
|
|
305
|
-
placeholders = ",".join("?" for _ in fact_ids)
|
|
306
|
-
for table in ("vector_row_map", "embedding_metadata"):
|
|
307
|
-
try:
|
|
308
|
-
rows = self._db.execute(
|
|
309
|
-
f"SELECT fact_id FROM {table} "
|
|
310
|
-
f"WHERE profile_id = ? AND fact_id IN ({placeholders})",
|
|
311
|
-
(profile_id, *fact_ids),
|
|
312
|
-
)
|
|
313
|
-
residue |= {dict(r)["fact_id"] for r in rows}
|
|
314
|
-
except Exception:
|
|
315
|
-
continue
|
|
316
|
-
return len(residue)
|
|
317
|
-
|
|
318
|
-
def _profile_scoped_tables(self) -> list[str]:
|
|
319
|
-
"""Every table carrying a ``profile_id`` column — discovered live from
|
|
320
|
-
the schema so a newly-added table can never be silently missed by
|
|
321
|
-
export or erasure (the class of bug that breaks GDPR completeness)."""
|
|
322
|
-
try:
|
|
323
|
-
names = [
|
|
324
|
-
dict(r)["name"]
|
|
325
|
-
for r in self._db.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
326
|
-
]
|
|
327
|
-
except Exception:
|
|
328
|
-
return []
|
|
329
|
-
out: list[str] = []
|
|
330
|
-
for t in names:
|
|
331
|
-
if t.startswith("sqlite_") or t in self._NON_MEMORY_SCOPED:
|
|
332
|
-
continue
|
|
333
|
-
try:
|
|
334
|
-
cols = {dict(c)["name"] for c in self._db.execute(f"PRAGMA table_info({t})")}
|
|
335
|
-
except Exception:
|
|
336
|
-
continue
|
|
337
|
-
if "profile_id" in cols:
|
|
338
|
-
out.append(t)
|
|
339
|
-
return out
|
|
340
|
-
|
|
341
|
-
# -- Right to Access (Art. 15) -----------------------------------------
|
|
342
|
-
|
|
343
|
-
def export_profile_data(self, profile_id: str) -> dict:
|
|
344
|
-
"""Export ALL data for a profile in machine-readable format (Art. 15 /
|
|
345
|
-
Art. 20). Covers every profile-scoped table discovered from the schema,
|
|
346
|
-
plus the profile record itself."""
|
|
347
|
-
self._audit("export", "profile", profile_id, "Full data export")
|
|
348
|
-
|
|
349
|
-
data: dict = {"profile_id": profile_id, "exported_at": _now()}
|
|
350
|
-
for table in self._profile_scoped_tables():
|
|
351
|
-
try:
|
|
352
|
-
rows = self._db.execute(
|
|
353
|
-
f"SELECT * FROM {table} WHERE profile_id = ?", (profile_id,)
|
|
354
|
-
)
|
|
355
|
-
data[table] = [dict(r) for r in rows]
|
|
356
|
-
except Exception as exc: # pragma: no cover — defensive per-table
|
|
357
|
-
logger.warning("export: table %s skipped: %s", table, exc)
|
|
358
|
-
|
|
359
|
-
# Profile record itself (the tenant metadata).
|
|
360
|
-
try:
|
|
361
|
-
rows = self._db.execute("SELECT * FROM profiles WHERE profile_id = ?", (profile_id,))
|
|
362
|
-
data["profile_record"] = [dict(r) for r in rows]
|
|
363
|
-
except Exception:
|
|
364
|
-
data["profile_record"] = []
|
|
365
|
-
|
|
366
|
-
# C2 — include code_graph.db in Art.15 export (repo paths, file names,
|
|
367
|
-
# and symbol names are identifying data in a work context).
|
|
368
|
-
if self._data_root is not None:
|
|
369
|
-
code_graph_data = self._export_code_graph(self._data_root)
|
|
370
|
-
if code_graph_data is not None:
|
|
371
|
-
data["code_graph"] = code_graph_data
|
|
372
|
-
|
|
373
|
-
# What the system learned about how this person works: which
|
|
374
|
-
# questions they asked, which answers they found useful, and the
|
|
375
|
-
# weights derived from that. It was erased on request and could not
|
|
376
|
-
# be asked for -- which makes an export that claims to be everything
|
|
377
|
-
# untrue. It lives in a second file, so the table sweep above never
|
|
378
|
-
# reached it.
|
|
379
|
-
behaviour = self._export_learning_signals(self._data_root, profile_id)
|
|
380
|
-
if behaviour is not None:
|
|
381
|
-
data["learning_signals"] = behaviour
|
|
382
|
-
|
|
383
|
-
# total_items counts the canonical (table-name) keys only, before
|
|
384
|
-
# friendly aliases are added, so it is not double-counted.
|
|
385
|
-
data["total_items"] = sum(len(v) for v in data.values() if isinstance(v, list))
|
|
386
|
-
|
|
387
|
-
# Backward-compatible friendly aliases for the well-known keys (stable
|
|
388
|
-
# export contract) — they reference the same lists, not copies.
|
|
389
|
-
for friendly, table in _EXPORT_ALIASES.items():
|
|
390
|
-
if table in data:
|
|
391
|
-
data[friendly] = data[table]
|
|
392
|
-
|
|
393
|
-
logger.info("Exported %d items for profile '%s'", data["total_items"], profile_id)
|
|
394
|
-
return data
|
|
395
|
-
|
|
396
|
-
# -- Right to Erasure (Art. 17) ----------------------------------------
|
|
397
|
-
|
|
398
|
-
def forget_profile(self, profile_id: str) -> dict:
|
|
399
|
-
"""Delete ALL data for a profile (right to be forgotten, Art. 17).
|
|
400
|
-
|
|
401
|
-
Erases every profile-scoped table discovered from the live schema, so a
|
|
402
|
-
newly-added table is covered automatically. The erasure is recorded in
|
|
403
|
-
the tamper-proof audit chain BEFORE any deletion (Art. 5(2)
|
|
404
|
-
accountability) — the in-DB compliance_audit row is itself erased, so
|
|
405
|
-
the chain in a separate DB is the durable evidence.
|
|
406
|
-
"""
|
|
407
|
-
if profile_id == "default":
|
|
408
|
-
raise ValueError(
|
|
409
|
-
"Cannot delete the default profile via GDPR erasure. Use profile deletion instead."
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
counts: dict[str, int] = {}
|
|
413
|
-
|
|
414
|
-
# 1) Durable, tamper-evident record FIRST — a HARD precondition
|
|
415
|
-
# (Art. 5(2) accountability). If it cannot be written we fail closed
|
|
416
|
-
# and delete nothing, so no erasure ever occurs without an
|
|
417
|
-
# accountability record.
|
|
418
|
-
try:
|
|
419
|
-
from superlocalmemory.compliance.audit import AuditChain
|
|
420
|
-
from superlocalmemory.infra.data_root import state_path
|
|
421
|
-
|
|
422
|
-
AuditChain(str(state_path("audit_chain.db"))).log(
|
|
423
|
-
"gdpr_erase",
|
|
424
|
-
agent_id="gdpr",
|
|
425
|
-
profile_id=profile_id,
|
|
426
|
-
metadata={"basis": "GDPR Art.17 right-to-erasure"},
|
|
427
|
-
)
|
|
428
|
-
except Exception as exc:
|
|
429
|
-
logger.error(
|
|
430
|
-
"GDPR erase ABORTED for %r: pre-deletion audit-chain log failed: %s",
|
|
431
|
-
profile_id,
|
|
432
|
-
exc,
|
|
433
|
-
)
|
|
434
|
-
counts["audit_request_failed"] = 1
|
|
435
|
-
counts["erasure_aborted"] = 1
|
|
436
|
-
return counts
|
|
437
|
-
self._audit("delete", "profile", profile_id, "GDPR erasure request")
|
|
438
|
-
tables = self._profile_scoped_tables()
|
|
439
|
-
# Pass 1 — count every table BEFORE any deletion, so a CASCADE that
|
|
440
|
-
# removes a child (e.g. atomic_facts via memories) does not zero the
|
|
441
|
-
# attribution. Completeness is independent of this.
|
|
442
|
-
for table in tables:
|
|
443
|
-
try:
|
|
444
|
-
rows = self._db.execute(
|
|
445
|
-
f"SELECT COUNT(*) AS c FROM {table} WHERE profile_id = ?",
|
|
446
|
-
(profile_id,),
|
|
447
|
-
)
|
|
448
|
-
counts[table] = int(dict(rows[0])["c"]) if rows else 0
|
|
449
|
-
except Exception as exc: # pragma: no cover
|
|
450
|
-
logger.warning("GDPR erase: count %s failed: %s", table, exc)
|
|
451
|
-
counts[table] = 0
|
|
452
|
-
|
|
453
|
-
# Purge context-cache entries BEFORE main-DB row deletions.
|
|
454
|
-
#
|
|
455
|
-
# Crash-recovery rationale: the cache and the main DB live in separate
|
|
456
|
-
# SQLite files — they cannot share one ACID transaction. Ordering the
|
|
457
|
-
# cache purge first ensures that any crash between the two steps leaves
|
|
458
|
-
# the profile record still present in the main DB, so a retry of
|
|
459
|
-
# forget_profile re-runs the full sequence and completes safely. The
|
|
460
|
-
# reverse order (cache after main delete) would orphan cache PII in a
|
|
461
|
-
# state that no retry can reach.
|
|
462
|
-
#
|
|
463
|
-
# The cache DB lives under the data root (same directory as the main DB)
|
|
464
|
-
# or in an immediate subdirectory. Scan both levels to cover the default
|
|
465
|
-
# layout and any explicitly-namespaced cache dirs.
|
|
466
|
-
#
|
|
467
|
-
# Destructive sidecar erasure requires an authoritative root. Never
|
|
468
|
-
# fall back to a process-global default, which might be another SLM
|
|
469
|
-
# installation.
|
|
470
|
-
data_root = self._data_root
|
|
471
|
-
try:
|
|
472
|
-
from superlocalmemory.core.context_cache import purge_profile_from_cache_db
|
|
473
|
-
|
|
474
|
-
if data_root is None:
|
|
475
|
-
db_path = getattr(self._db, "db_path", None)
|
|
476
|
-
if db_path is not None:
|
|
477
|
-
data_root = Path(db_path).resolve().parent
|
|
478
|
-
|
|
479
|
-
if data_root is None:
|
|
480
|
-
logger.warning(
|
|
481
|
-
"GDPR erase: context-cache purge skipped for profile %r — "
|
|
482
|
-
"data root could not be resolved; pass data_root explicitly "
|
|
483
|
-
"for custom DB wrappers.",
|
|
484
|
-
profile_id,
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
if data_root is not None:
|
|
488
|
-
cache_name = "active_brain_cache.db"
|
|
489
|
-
candidates: list = [data_root / cache_name]
|
|
490
|
-
try:
|
|
491
|
-
for child in data_root.iterdir():
|
|
492
|
-
if child.is_dir():
|
|
493
|
-
candidates.append(child / cache_name)
|
|
494
|
-
except Exception:
|
|
495
|
-
pass
|
|
496
|
-
cache_purged = 0
|
|
497
|
-
for candidate in candidates:
|
|
498
|
-
cache_purged += purge_profile_from_cache_db(candidate, profile_id)
|
|
499
|
-
if cache_purged:
|
|
500
|
-
counts["context_cache"] = cache_purged
|
|
501
|
-
except Exception as exc:
|
|
502
|
-
# Fail-closed: a context-cache purge failure must not be silently
|
|
503
|
-
# tolerated — it can leave profile PII in the cache DB.
|
|
504
|
-
logger.warning("GDPR erase: context-cache purge failed: %s", exc)
|
|
505
|
-
counts["context_cache_failed"] = 1
|
|
506
|
-
|
|
507
|
-
try:
|
|
508
|
-
vector_purged, vector_failures = self._purge_vector_and_ann(profile_id)
|
|
509
|
-
counts["vector_store"] = vector_purged
|
|
510
|
-
if vector_failures:
|
|
511
|
-
counts["vector_store_failures"] = vector_failures
|
|
512
|
-
except Exception as exc:
|
|
513
|
-
# Fail-closed: a top-level vector-purge exception (as opposed to the
|
|
514
|
-
# per-fact failures returned in vector_failures) must set an explicit
|
|
515
|
-
# marker, or erasure_complete could still report 1 despite the vector
|
|
516
|
-
# projection never being purged.
|
|
517
|
-
logger.warning("GDPR erase: vector purge failed: %s", exc)
|
|
518
|
-
counts["vector_store_failures"] = counts.get("vector_store_failures", 0) or 1
|
|
519
|
-
|
|
520
|
-
# The graph and vector projections are separate storage engines, so a
|
|
521
|
-
# profile wipe against SQLite does not reach them. They have to be
|
|
522
|
-
# purged here, before Pass 2: that sweep finds a tenant's tables by
|
|
523
|
-
# looking for a profile_id column, which includes projection_outbox — so
|
|
524
|
-
# it would delete the very rows that carry the queued removals, leaving
|
|
525
|
-
# the erased facts in the graph with nothing left to take them out.
|
|
526
|
-
try:
|
|
527
|
-
graph_purged, graph_failures = self._purge_graph_and_vector_projections(
|
|
528
|
-
profile_id
|
|
529
|
-
)
|
|
530
|
-
if graph_purged:
|
|
531
|
-
counts["graph_projection"] = graph_purged
|
|
532
|
-
if graph_failures:
|
|
533
|
-
counts["graph_projection_failures"] = graph_failures
|
|
534
|
-
except Exception as exc:
|
|
535
|
-
logger.warning("GDPR erase: graph projection purge failed: %s", exc)
|
|
536
|
-
counts["graph_projection_failures"] = (
|
|
537
|
-
counts.get("graph_projection_failures", 0) or 1
|
|
538
|
-
)
|
|
539
|
-
|
|
540
|
-
# Erasure receipt (P1-5) — route the profile wipe through ErasureService
|
|
541
|
-
# so the receipt captures real per-owner proofs (not proofs:[]).
|
|
542
|
-
#
|
|
543
|
-
# erasure_receipts is in _NON_MEMORY_SCOPED so Pass 2 does NOT delete
|
|
544
|
-
# the receipt — it survives as the tamper-evident Art.17 audit chain.
|
|
545
|
-
# remove() + finalize() therefore run here, before Pass 2, while
|
|
546
|
-
# atomic_facts is still queryable for embedding presence checks.
|
|
547
|
-
#
|
|
548
|
-
# Wrapped in try-except so a missing M033/M035 schema never blocks the
|
|
549
|
-
# Art.17 right-to-erasure.
|
|
550
|
-
import time as _time
|
|
551
|
-
import uuid as _uuid
|
|
552
|
-
|
|
553
|
-
_profile_fact_ids: tuple[str, ...] = ()
|
|
554
|
-
try:
|
|
555
|
-
_fact_rows = self._db.execute(
|
|
556
|
-
"SELECT fact_id FROM atomic_facts WHERE profile_id = ?",
|
|
557
|
-
(profile_id,),
|
|
558
|
-
)
|
|
559
|
-
_profile_fact_ids = tuple(
|
|
560
|
-
sorted(dict(r)["fact_id"] for r in _fact_rows if dict(r).get("fact_id") is not None)
|
|
561
|
-
)
|
|
562
|
-
except Exception as exc:
|
|
563
|
-
logger.warning("GDPR profile erase: fact_id scan failed: %s", exc)
|
|
564
|
-
|
|
565
|
-
# Always write an erasure receipt — even for empty profiles (fact_ids=()).
|
|
566
|
-
# Skipping the receipt for no-fact profiles left an Art.17 accountability
|
|
567
|
-
# gap: a destructive wipe with no durable audit record. ErasureService
|
|
568
|
-
# handles empty fact_ids safely (all owners vacuously return erased=True).
|
|
569
|
-
# If finalize() raises (e.g. signing-key unavailable), propagate — we must
|
|
570
|
-
# not silently proceed with a wipe that has no accountability record.
|
|
571
|
-
try:
|
|
572
|
-
from superlocalmemory.core.transactions.concrete_owners import (
|
|
573
|
-
build_erasure_service_for_db,
|
|
574
|
-
)
|
|
575
|
-
from superlocalmemory.core.transactions.owners import OperationContext
|
|
576
|
-
|
|
577
|
-
_erasure_svc = build_erasure_service_for_db(self._db, self._engine)
|
|
578
|
-
_ctx = OperationContext(
|
|
579
|
-
operation_id=_uuid.uuid4().hex,
|
|
580
|
-
profile_id=profile_id,
|
|
581
|
-
subject_id=profile_id,
|
|
582
|
-
fact_ids=_profile_fact_ids,
|
|
583
|
-
)
|
|
584
|
-
_remove_result = _erasure_svc.remove(self._db, _ctx)
|
|
585
|
-
_receipt = _erasure_svc.finalize(
|
|
586
|
-
self._db,
|
|
587
|
-
_ctx,
|
|
588
|
-
subject_type="profile",
|
|
589
|
-
subject_id=profile_id,
|
|
590
|
-
requested_by="gdpr",
|
|
591
|
-
requested_at=_time.time(),
|
|
592
|
-
remove_result=_remove_result,
|
|
593
|
-
)
|
|
594
|
-
if not _receipt.persisted:
|
|
595
|
-
counts["receipt_persist_failed"] = 1
|
|
596
|
-
if not _receipt.all_erased:
|
|
597
|
-
counts["owner_erasure_incomplete"] = 1
|
|
598
|
-
except Exception as exc:
|
|
599
|
-
counts["receipt_error"] = str(exc)
|
|
600
|
-
raise
|
|
601
|
-
|
|
602
|
-
# C2 — erase this profile's share of code_graph.db before the main
|
|
603
|
-
# profile rows. How much of it is "this profile's share" depends on
|
|
604
|
-
# whether anybody else is in the store; see _erase_code_graph. A failure
|
|
605
|
-
# here does not abort the rest of the erasure, but it does block the
|
|
606
|
-
# completeness claim below.
|
|
607
|
-
if data_root is not None:
|
|
608
|
-
code_graph_result = self._erase_code_graph(
|
|
609
|
-
data_root,
|
|
610
|
-
profile_id=profile_id,
|
|
611
|
-
sole_profile=self._is_sole_profile(profile_id),
|
|
612
|
-
)
|
|
613
|
-
counts["code_graph"] = code_graph_result.get("rows_deleted", 0)
|
|
614
|
-
counts["code_graph_scope"] = code_graph_result.get("scope", "")
|
|
615
|
-
if code_graph_result.get("retained_reason"):
|
|
616
|
-
counts["code_graph_retained_reason"] = code_graph_result[
|
|
617
|
-
"retained_reason"
|
|
618
|
-
]
|
|
619
|
-
if code_graph_result.get("error"):
|
|
620
|
-
counts["code_graph_failed"] = 1
|
|
621
|
-
|
|
622
|
-
# Purge the learning sidecar *before* removing memory/profile rows. A
|
|
623
|
-
# learning failure is retryable and must leave the profile intact; the
|
|
624
|
-
# former best-effort-after-delete ordering could orphan receipts.
|
|
625
|
-
if data_root is None:
|
|
626
|
-
# Compatibility for third-party legacy wrappers that expose no
|
|
627
|
-
# durable path. We cannot safely guess another installation's
|
|
628
|
-
# sidecar. Native v4.0.2 runtime objects always provide the root.
|
|
629
|
-
logger.warning(
|
|
630
|
-
"GDPR erase: learning receipt purge skipped for profile %r — "
|
|
631
|
-
"data root could not be resolved",
|
|
632
|
-
profile_id,
|
|
633
|
-
)
|
|
634
|
-
counts["learning_db_skipped"] = 1
|
|
635
|
-
else:
|
|
636
|
-
try:
|
|
637
|
-
from superlocalmemory.learning.database import LearningDatabase
|
|
638
|
-
|
|
639
|
-
learning_db = LearningDatabase(data_root / "learning.db")
|
|
640
|
-
learning_db.reset(profile_id)
|
|
641
|
-
counts["learning_db"] = 1
|
|
642
|
-
except Exception as exc:
|
|
643
|
-
logger.warning("GDPR erase: learning-db reset failed: %s", exc)
|
|
644
|
-
counts["learning_db_failed"] = 1
|
|
645
|
-
raise RuntimeError(
|
|
646
|
-
"learning receipt purge failed; profile deletion was not started"
|
|
647
|
-
) from exc
|
|
648
|
-
|
|
649
|
-
# Tables keyed on a fact rather than on a profile are invisible to the
|
|
650
|
-
# profile-scoped sweep below, which discovers tables by looking for a
|
|
651
|
-
# profile_id column. They have to go FIRST, while the facts that name
|
|
652
|
-
# them still exist to be joined against.
|
|
653
|
-
self._erase_fact_keyed_tables(profile_id, counts)
|
|
654
|
-
|
|
655
|
-
# Pass 2 — full-tenant wipe with FK enforcement OFF so table order is
|
|
656
|
-
# irrelevant (every profile row in every table goes). FTS shadow rows
|
|
657
|
-
# are still removed by the base-table delete triggers.
|
|
658
|
-
try:
|
|
659
|
-
self._db.execute("PRAGMA foreign_keys=OFF")
|
|
660
|
-
except Exception:
|
|
661
|
-
pass
|
|
662
|
-
table_delete_failures: list[str] = []
|
|
663
|
-
try:
|
|
664
|
-
for table in tables:
|
|
665
|
-
try:
|
|
666
|
-
self._db.execute(f"DELETE FROM {table} WHERE profile_id = ?", (profile_id,))
|
|
667
|
-
except Exception as exc: # pragma: no cover — defensive per-table
|
|
668
|
-
logger.warning("GDPR erase: delete %s failed: %s", table, exc)
|
|
669
|
-
table_delete_failures.append(table)
|
|
670
|
-
# Delete the profile record itself.
|
|
671
|
-
self._db.execute("DELETE FROM profiles WHERE profile_id = ?", (profile_id,))
|
|
672
|
-
counts["profiles"] = 1
|
|
673
|
-
finally:
|
|
674
|
-
try:
|
|
675
|
-
self._db.execute("PRAGMA foreign_keys=ON")
|
|
676
|
-
except Exception:
|
|
677
|
-
pass
|
|
678
|
-
if table_delete_failures:
|
|
679
|
-
counts["table_delete_failures"] = len(table_delete_failures)
|
|
680
|
-
|
|
681
|
-
# VACUUM to remove deleted data from physical file
|
|
682
|
-
try:
|
|
683
|
-
self._db.execute("VACUUM")
|
|
684
|
-
except Exception:
|
|
685
|
-
pass
|
|
686
|
-
|
|
687
|
-
# Fail-closed completeness: re-count residue across the wiped tables and
|
|
688
|
-
# surface an explicit erasure_complete flag so a partial wipe is reported
|
|
689
|
-
# as failure rather than silent success.
|
|
690
|
-
residue_rows = 0
|
|
691
|
-
residue_recount_failed = False
|
|
692
|
-
for table in tables:
|
|
693
|
-
try:
|
|
694
|
-
_r = self._db.execute(
|
|
695
|
-
f"SELECT COUNT(*) AS c FROM {table} WHERE profile_id = ?",
|
|
696
|
-
(profile_id,),
|
|
697
|
-
)
|
|
698
|
-
residue_rows += int(dict(_r[0])["c"]) if _r else 0
|
|
699
|
-
except Exception as exc:
|
|
700
|
-
# Fail-closed: a residue re-count that cannot be performed is a
|
|
701
|
-
# verification failure, not zero residue. We cannot certify the
|
|
702
|
-
# table is clean, so erasure must not report complete.
|
|
703
|
-
logger.warning("GDPR erase: residue re-count for %s failed: %s", table, exc)
|
|
704
|
-
residue_recount_failed = True
|
|
705
|
-
counts["residue_rows"] = residue_rows
|
|
706
|
-
if residue_recount_failed:
|
|
707
|
-
counts["residue_recount_failed"] = 1
|
|
708
|
-
|
|
709
|
-
# I7 — post-erasure residue sweep: FTS shadow tables + WAL sanity.
|
|
710
|
-
# This makes I7 enforceable rather than aspirational.
|
|
711
|
-
self._scan_fts_residue(profile_id, tables, counts)
|
|
712
|
-
self._scan_wal_residue(counts)
|
|
713
|
-
|
|
714
|
-
# C1 — record outstanding obligations against backup snapshots.
|
|
715
|
-
# Done AFTER the main-DB residue scan so counts reflect live-store state
|
|
716
|
-
# before we tally the backups outstanding obligation count.
|
|
717
|
-
# Fail-closed: if the scan itself errors, set backup_scan_failed so
|
|
718
|
-
# completeness cannot be claimed.
|
|
719
|
-
backup_obligations_pending = 0
|
|
720
|
-
if data_root is not None:
|
|
721
|
-
try:
|
|
722
|
-
backup_obligations_pending = self._record_backup_obligations(
|
|
723
|
-
data_root=data_root,
|
|
724
|
-
profile_id=profile_id,
|
|
725
|
-
erasure_id=_uuid.uuid4().hex, # unique id for this obligation batch
|
|
726
|
-
counts=counts,
|
|
727
|
-
)
|
|
728
|
-
counts["backup_obligations_pending"] = backup_obligations_pending
|
|
729
|
-
except Exception as exc:
|
|
730
|
-
logger.error(
|
|
731
|
-
"GDPR erase: backup obligation recording FAILED: %s — "
|
|
732
|
-
"setting backup_scan_failed to block completeness",
|
|
733
|
-
exc,
|
|
734
|
-
)
|
|
735
|
-
counts["backup_scan_failed"] = 1
|
|
736
|
-
else:
|
|
737
|
-
# Cannot scan backups without data_root — treat as outstanding
|
|
738
|
-
# obligation so completeness is blocked.
|
|
739
|
-
counts["backup_obligations_pending"] = 0 # unknown but not confirmed clean
|
|
740
|
-
# We won't block completeness when data_root is unknown (legacy wrapper)
|
|
741
|
-
# but we do log the gap.
|
|
742
|
-
logger.warning(
|
|
743
|
-
"GDPR erase: backup obligation scan skipped — data_root unknown. "
|
|
744
|
-
"Backup snapshots may still contain the erased profile's data."
|
|
745
|
-
)
|
|
746
|
-
|
|
747
|
-
# In-process residue. The erased profile's recently-shown memories are
|
|
748
|
-
# held in a per-session working set that biases ranking; those are the
|
|
749
|
-
# subject's data too, and a later session reusing one of their session
|
|
750
|
-
# ids would otherwise inherit the bias.
|
|
751
|
-
#
|
|
752
|
-
# Runs after every DB delete — an in-memory dict cannot participate in a
|
|
753
|
-
# rollback, so clearing it earlier would be unrecoverable if the wipe
|
|
754
|
-
# then failed — but BEFORE the completeness verdict below, and counted by
|
|
755
|
-
# it. Computing the verdict first would let this fail while the API
|
|
756
|
-
# reported a complete erasure, which is the one thing an Art.17 receipt
|
|
757
|
-
# must never do.
|
|
758
|
-
try:
|
|
759
|
-
from superlocalmemory.core.working_memory import discard_profile
|
|
760
|
-
|
|
761
|
-
counts["working_sets"] = discard_profile(profile_id)
|
|
762
|
-
except Exception as exc: # pragma: no cover — defensive
|
|
763
|
-
logger.warning("GDPR erase: working-set discard failed: %s", exc)
|
|
764
|
-
counts["working_sets_failed"] = 1
|
|
765
|
-
|
|
766
|
-
counts["erasure_complete"] = (
|
|
767
|
-
1
|
|
768
|
-
if (
|
|
769
|
-
residue_rows == 0
|
|
770
|
-
and not residue_recount_failed
|
|
771
|
-
and not table_delete_failures
|
|
772
|
-
and not counts.get("learning_db_failed")
|
|
773
|
-
and not counts.get("learning_db_skipped")
|
|
774
|
-
and not counts.get("vector_store_failures")
|
|
775
|
-
# A fact still in the graph is still recallable, so an erasure
|
|
776
|
-
# that could not reach the projection is not complete.
|
|
777
|
-
and not counts.get("graph_projection_failures")
|
|
778
|
-
and not counts.get("context_cache_failed")
|
|
779
|
-
and not counts.get("owner_erasure_incomplete")
|
|
780
|
-
and not counts.get("backup_obligations_pending")
|
|
781
|
-
and not counts.get("backup_scan_failed")
|
|
782
|
-
and not counts.get("fts_residue_rows")
|
|
783
|
-
and not counts.get("working_sets_failed")
|
|
784
|
-
and not counts.get("code_graph_failed")
|
|
785
|
-
and not any(
|
|
786
|
-
counts.get(f"{t}_failed") for t, _ in self._FACT_KEYED_TABLES
|
|
787
|
-
)
|
|
788
|
-
)
|
|
789
|
-
else 0
|
|
790
|
-
)
|
|
791
|
-
|
|
792
|
-
# Whether the data is gone and whether we can PROVE it is gone are two
|
|
793
|
-
# different questions, and one answer cannot carry both. Article 5(2) is
|
|
794
|
-
# accountability: an erasure whose tamper-evident receipt was not
|
|
795
|
-
# written really did delete the rows, and really cannot be demonstrated
|
|
796
|
-
# afterwards. Folding that into erasure_complete would report an
|
|
797
|
-
# erasure that happened as one that did not; leaving it out entirely —
|
|
798
|
-
# which is what happened until now — lets a caller reading one field
|
|
799
|
-
# believe it covers both.
|
|
800
|
-
counts["erasure_provable"] = (
|
|
801
|
-
1
|
|
802
|
-
if (
|
|
803
|
-
counts["erasure_complete"] == 1
|
|
804
|
-
and not counts.get("receipt_persist_failed")
|
|
805
|
-
and not counts.get("receipt_error")
|
|
806
|
-
)
|
|
807
|
-
else 0
|
|
808
|
-
)
|
|
809
|
-
|
|
810
|
-
try:
|
|
811
|
-
from superlocalmemory.compliance.audit import AuditChain
|
|
812
|
-
from superlocalmemory.infra.data_root import state_path
|
|
813
|
-
|
|
814
|
-
AuditChain(str(state_path("audit_chain.db"))).log(
|
|
815
|
-
"gdpr_erase_complete",
|
|
816
|
-
agent_id="gdpr",
|
|
817
|
-
profile_id=profile_id,
|
|
818
|
-
metadata={
|
|
819
|
-
"basis": "GDPR Art.17 right-to-erasure",
|
|
820
|
-
"tables_erased": len(tables),
|
|
821
|
-
"vector_store_failures": counts.get("vector_store_failures", 0),
|
|
822
|
-
"backup_obligations_pending": backup_obligations_pending,
|
|
823
|
-
},
|
|
824
|
-
)
|
|
825
|
-
except Exception as exc:
|
|
826
|
-
logger.error("GDPR erase: completion audit-chain log failed: %s", exc)
|
|
827
|
-
counts["audit_completion_failed"] = 1
|
|
828
|
-
|
|
829
|
-
logger.info("GDPR erasure for '%s': %d tables, %s", profile_id, len(tables), counts)
|
|
830
|
-
return counts
|
|
831
|
-
|
|
832
|
-
def forget_entity(self, entity_name: str, profile_id: str) -> dict:
|
|
833
|
-
"""Delete all data related to a specific entity.
|
|
834
|
-
|
|
835
|
-
Removes facts mentioning the entity, edges, temporal events,
|
|
836
|
-
and the entity itself. For targeted erasure requests.
|
|
837
|
-
"""
|
|
838
|
-
import time
|
|
839
|
-
|
|
840
|
-
requested_at = time.time()
|
|
841
|
-
audit_request_ok = True
|
|
842
|
-
try:
|
|
843
|
-
from superlocalmemory.compliance.audit import AuditChain
|
|
844
|
-
from superlocalmemory.infra.data_root import state_path
|
|
845
|
-
|
|
846
|
-
AuditChain(str(state_path("audit_chain.db"))).log(
|
|
847
|
-
"gdpr_erase_entity",
|
|
848
|
-
agent_id="gdpr",
|
|
849
|
-
profile_id=profile_id,
|
|
850
|
-
metadata={
|
|
851
|
-
"basis": "GDPR Art.17 right-to-erasure",
|
|
852
|
-
"entity": entity_name,
|
|
853
|
-
},
|
|
854
|
-
)
|
|
855
|
-
except Exception as exc:
|
|
856
|
-
logger.warning("GDPR entity erase: audit-chain log failed: %s", exc)
|
|
857
|
-
audit_request_ok = False
|
|
858
|
-
self._audit(
|
|
859
|
-
"delete",
|
|
860
|
-
"entity",
|
|
861
|
-
entity_name,
|
|
862
|
-
f"GDPR entity erasure in profile {profile_id}",
|
|
863
|
-
profile_id=profile_id,
|
|
864
|
-
)
|
|
865
|
-
|
|
866
|
-
entity = self._db.get_entity_by_name(entity_name, profile_id)
|
|
867
|
-
if entity is None:
|
|
868
|
-
result: dict[str, object] = {"deleted": 0, "entity": entity_name, "found": False}
|
|
869
|
-
if not audit_request_ok:
|
|
870
|
-
result["audit_request_failed"] = 1
|
|
871
|
-
return result
|
|
872
|
-
|
|
873
|
-
eid = entity.entity_id
|
|
874
|
-
counts: dict[str, int] = {}
|
|
875
|
-
|
|
876
|
-
# Delete facts mentioning this entity — use ErasureService for projection
|
|
877
|
-
# erasure so the receipt captures real per-owner proofs (not proofs:[]).
|
|
878
|
-
rows = self._db.execute(
|
|
879
|
-
"SELECT fact_id, memory_id FROM atomic_facts WHERE profile_id = ? "
|
|
880
|
-
"AND canonical_entities_json LIKE ?",
|
|
881
|
-
(profile_id, f'%"{eid}"%'),
|
|
882
|
-
)
|
|
883
|
-
targets = [(dict(r)["fact_id"], dict(r).get("memory_id")) for r in rows]
|
|
884
|
-
target_fact_ids = [fid for fid, _ in targets]
|
|
885
|
-
counts["facts"] = len(targets)
|
|
886
|
-
|
|
887
|
-
if targets:
|
|
888
|
-
import uuid as _uuid
|
|
889
|
-
|
|
890
|
-
from superlocalmemory.core.transactions.concrete_owners import (
|
|
891
|
-
build_erasure_service_for_db,
|
|
892
|
-
)
|
|
893
|
-
from superlocalmemory.core.transactions.owners import OperationContext
|
|
894
|
-
|
|
895
|
-
erasure_svc = build_erasure_service_for_db(self._db, self._engine)
|
|
896
|
-
op_id = _uuid.uuid4().hex
|
|
897
|
-
ctx = OperationContext(
|
|
898
|
-
operation_id=op_id,
|
|
899
|
-
profile_id=profile_id,
|
|
900
|
-
subject_id=entity_name,
|
|
901
|
-
fact_ids=tuple(sorted(target_fact_ids)),
|
|
902
|
-
)
|
|
903
|
-
erasure_svc.remove(self._db, ctx)
|
|
904
|
-
receipt = erasure_svc.finalize(
|
|
905
|
-
self._db,
|
|
906
|
-
ctx,
|
|
907
|
-
subject_type="entity",
|
|
908
|
-
subject_id=entity_name,
|
|
909
|
-
requested_by="gdpr",
|
|
910
|
-
requested_at=requested_at,
|
|
911
|
-
)
|
|
912
|
-
if not receipt.persisted:
|
|
913
|
-
counts["receipt_persist_failed"] = 1
|
|
914
|
-
if not receipt.all_erased:
|
|
915
|
-
counts["vector_store_failures"] = sum(1 for p in receipt.proofs if not p.erased)
|
|
916
|
-
|
|
917
|
-
# The same residue the profile wipe had: the search-expansion index is
|
|
918
|
-
# keyed on a fact, not a profile, and ``delete_fact`` does not touch it.
|
|
919
|
-
# Erasing an entity left the alternate keys of its memories behind, and
|
|
920
|
-
# a search could still match them.
|
|
921
|
-
self._erase_fact_keyed_tables_for(
|
|
922
|
-
[fid for fid, _mid in targets], counts,
|
|
923
|
-
)
|
|
924
|
-
|
|
925
|
-
for fid, mid in targets:
|
|
926
|
-
self._db.delete_fact(fid)
|
|
927
|
-
if mid and not self._memory_has_siblings(mid, profile_id):
|
|
928
|
-
try:
|
|
929
|
-
self._db.execute(
|
|
930
|
-
"DELETE FROM memories WHERE memory_id = ? AND profile_id = ?",
|
|
931
|
-
(mid, profile_id),
|
|
932
|
-
)
|
|
933
|
-
except Exception:
|
|
934
|
-
pass
|
|
935
|
-
|
|
936
|
-
# Delete temporal events
|
|
937
|
-
self._db.execute(
|
|
938
|
-
"DELETE FROM temporal_events WHERE entity_id = ? AND profile_id = ?",
|
|
939
|
-
(eid, profile_id),
|
|
940
|
-
)
|
|
941
|
-
|
|
942
|
-
# Delete entity profile
|
|
943
|
-
self._db.execute(
|
|
944
|
-
"DELETE FROM entity_profiles WHERE entity_id = ? AND profile_id = ?",
|
|
945
|
-
(eid, profile_id),
|
|
946
|
-
)
|
|
947
|
-
|
|
948
|
-
# Delete aliases + entity (profile-scoped — entity_id is UUID-global but
|
|
949
|
-
# keep the tenant predicate for consistent Art.17 isolation).
|
|
950
|
-
self._db.execute(
|
|
951
|
-
"DELETE FROM entity_aliases WHERE entity_id = ? AND profile_id = ?", (eid, profile_id)
|
|
952
|
-
)
|
|
953
|
-
self._db.execute(
|
|
954
|
-
"DELETE FROM canonical_entities WHERE entity_id = ? AND profile_id = ?",
|
|
955
|
-
(eid, profile_id),
|
|
956
|
-
)
|
|
957
|
-
# The graph holds its own copy of this node, with the name in it.
|
|
958
|
-
# Deleting the row above does not reach it.
|
|
959
|
-
if not _unproject_entity(eid):
|
|
960
|
-
counts["projection_failed"] = 1
|
|
961
|
-
counts["entity"] = 1
|
|
962
|
-
if not audit_request_ok:
|
|
963
|
-
counts["audit_request_failed"] = 1
|
|
964
|
-
|
|
965
|
-
# The same two questions the profile path answers. Their absence here
|
|
966
|
-
# meant a caller could not tell a complete entity erasure from a partial
|
|
967
|
-
# one at all — it just got a dict of counts.
|
|
968
|
-
counts["erasure_complete"] = (
|
|
969
|
-
0
|
|
970
|
-
if any(
|
|
971
|
-
counts.get(marker)
|
|
972
|
-
for marker in (
|
|
973
|
-
"vector_store_failures",
|
|
974
|
-
"audit_request_failed",
|
|
975
|
-
*(f"{table}_failed" for table, _ in self._FACT_KEYED_TABLES),
|
|
976
|
-
)
|
|
977
|
-
)
|
|
978
|
-
else 1
|
|
979
|
-
)
|
|
980
|
-
counts["erasure_provable"] = (
|
|
981
|
-
1
|
|
982
|
-
if counts["erasure_complete"] == 1
|
|
983
|
-
and not counts.get("receipt_persist_failed")
|
|
984
|
-
and not counts.get("audit_completion_failed")
|
|
985
|
-
else 0
|
|
986
|
-
)
|
|
987
|
-
|
|
988
|
-
logger.info("Entity erasure '%s' in '%s': %s", entity_name, profile_id, counts)
|
|
989
|
-
return counts
|
|
990
|
-
|
|
991
|
-
# -- C2: code_graph helpers --------------------------------------------
|
|
992
|
-
|
|
993
|
-
#: Tables that hold a person's text but are keyed on a fact, not a profile.
|
|
994
|
-
#: The erasure sweep finds tables by looking for a profile_id column, so
|
|
995
|
-
#: these are invisible to it — the search-expansion index kept a copy of a
|
|
996
|
-
#: memory's alternate keys after every trace of the memory itself was gone.
|
|
997
|
-
#: Erasing them needs a join, and the join needs the facts to still exist,
|
|
998
|
-
#: so it runs before the sweep rather than after.
|
|
999
|
-
_FACT_KEYED_TABLES: tuple[tuple[str, str], ...] = (
|
|
1000
|
-
("fact_expansion_fts", "fact_id"),
|
|
1001
|
-
)
|
|
1002
|
-
|
|
1003
|
-
def _erase_fact_keyed_tables_for(
|
|
1004
|
-
self, fact_ids: list[str], counts: dict,
|
|
1005
|
-
) -> None:
|
|
1006
|
-
"""Erase fact-keyed rows for an explicit list of facts.
|
|
1007
|
-
|
|
1008
|
-
The profile wipe derives the list from a profile; a targeted entity
|
|
1009
|
-
erasure already knows which facts it is removing. Both need the same
|
|
1010
|
-
tables cleared, so they share the loop rather than one of them
|
|
1011
|
-
forgetting — which is exactly what happened.
|
|
1012
|
-
"""
|
|
1013
|
-
if not fact_ids:
|
|
1014
|
-
return
|
|
1015
|
-
for table, column in self._FACT_KEYED_TABLES:
|
|
1016
|
-
try:
|
|
1017
|
-
exists = self._db.execute(
|
|
1018
|
-
"SELECT 1 FROM sqlite_master WHERE name = ?", (table,)
|
|
1019
|
-
)
|
|
1020
|
-
except Exception as exc: # noqa: BLE001
|
|
1021
|
-
logger.warning("GDPR erase: cannot look for %s: %s", table, exc)
|
|
1022
|
-
counts[f"{table}_failed"] = 1
|
|
1023
|
-
continue
|
|
1024
|
-
if not exists:
|
|
1025
|
-
continue
|
|
1026
|
-
removed = 0
|
|
1027
|
-
try:
|
|
1028
|
-
for start in range(0, len(fact_ids), 500):
|
|
1029
|
-
chunk = fact_ids[start:start + 500]
|
|
1030
|
-
placeholders = ",".join("?" * len(chunk))
|
|
1031
|
-
present = self._db.execute(
|
|
1032
|
-
f"SELECT COUNT(*) AS c FROM {table} "
|
|
1033
|
-
f"WHERE {column} IN ({placeholders})",
|
|
1034
|
-
tuple(chunk),
|
|
1035
|
-
)
|
|
1036
|
-
removed += int(dict(present[0])["c"]) if present else 0
|
|
1037
|
-
self._db.execute(
|
|
1038
|
-
f"DELETE FROM {table} WHERE {column} IN ({placeholders})",
|
|
1039
|
-
tuple(chunk),
|
|
1040
|
-
)
|
|
1041
|
-
except Exception as exc: # noqa: BLE001
|
|
1042
|
-
logger.warning("GDPR erase: delete from %s failed: %s", table, exc)
|
|
1043
|
-
counts[f"{table}_failed"] = 1
|
|
1044
|
-
continue
|
|
1045
|
-
counts[table] = counts.get(table, 0) + removed
|
|
1046
|
-
|
|
1047
|
-
def _erase_fact_keyed_tables(self, profile_id: str, counts: dict) -> None:
|
|
1048
|
-
"""Delete rows that name this profile's facts but not the profile.
|
|
1049
|
-
|
|
1050
|
-
Delegates rather than repeating the loop. It WAS a second copy, and the
|
|
1051
|
-
commit that introduced the shared helper claimed otherwise — which is
|
|
1052
|
-
exactly the failure the helper existed to prevent: the entity path could
|
|
1053
|
-
have been fixed with the profile path left untouched, and nothing would
|
|
1054
|
-
have noticed.
|
|
1055
|
-
"""
|
|
1056
|
-
fact_ids = self._fact_ids_for(profile_id)
|
|
1057
|
-
if fact_ids is None:
|
|
1058
|
-
# Could not find out what to erase. Say so; do not report zero.
|
|
1059
|
-
for table, _column in self._FACT_KEYED_TABLES:
|
|
1060
|
-
counts[f"{table}_failed"] = 1
|
|
1061
|
-
return
|
|
1062
|
-
self._erase_fact_keyed_tables_for(fact_ids, counts)
|
|
1063
|
-
|
|
1064
|
-
def _is_sole_profile(self, profile_id: str) -> bool:
|
|
1065
|
-
"""Whether this profile is the only one in the store.
|
|
1066
|
-
|
|
1067
|
-
Decides how much of the shared code graph an erasure may take. Errs
|
|
1068
|
-
toward FALSE — the narrower erasure — because failing to answer is not
|
|
1069
|
-
a reason to delete somebody else's records.
|
|
1070
|
-
"""
|
|
1071
|
-
try:
|
|
1072
|
-
rows = self._db.execute("SELECT profile_id FROM profiles")
|
|
1073
|
-
except Exception as exc: # noqa: BLE001 - reported by erring narrow
|
|
1074
|
-
logger.warning(
|
|
1075
|
-
"GDPR erase: could not count profiles (%s); erasing only this "
|
|
1076
|
-
"profile's own rows from the code graph", exc,
|
|
1077
|
-
)
|
|
1078
|
-
return False
|
|
1079
|
-
found = set()
|
|
1080
|
-
for row in rows:
|
|
1081
|
-
try:
|
|
1082
|
-
found.add(str(dict(row)["profile_id"]))
|
|
1083
|
-
except Exception: # noqa: BLE001 - row shape varies by driver
|
|
1084
|
-
found.add(str(row[0]))
|
|
1085
|
-
return found <= {profile_id}
|
|
1086
|
-
|
|
1087
|
-
def _fact_ids_for(self, profile_id: str) -> list[str] | None:
|
|
1088
|
-
"""Every fact id belonging to this profile, for cross-database joins.
|
|
1089
|
-
|
|
1090
|
-
Returns None when the listing FAILED, which is not the same as a
|
|
1091
|
-
profile with no facts. Collapsing the two is how a locked database
|
|
1092
|
-
turned into "nothing to erase" and then into a receipt saying complete.
|
|
1093
|
-
"""
|
|
1094
|
-
try:
|
|
1095
|
-
rows = self._db.execute(
|
|
1096
|
-
"SELECT fact_id FROM atomic_facts WHERE profile_id = ?",
|
|
1097
|
-
(profile_id,),
|
|
1098
|
-
)
|
|
1099
|
-
except Exception as exc: # noqa: BLE001
|
|
1100
|
-
logger.warning("GDPR erase: could not list facts for %r: %s", profile_id, exc)
|
|
1101
|
-
return None
|
|
1102
|
-
out: list[str] = []
|
|
1103
|
-
for row in rows:
|
|
1104
|
-
try:
|
|
1105
|
-
out.append(str(dict(row)["fact_id"]))
|
|
1106
|
-
except Exception: # noqa: BLE001
|
|
1107
|
-
out.append(str(row[0]))
|
|
1108
|
-
return out
|
|
1109
|
-
|
|
1110
|
-
def _erase_code_graph(
|
|
1111
|
-
self, data_root: Path, *, profile_id: str = "", sole_profile: bool = True,
|
|
1112
|
-
) -> dict:
|
|
1113
|
-
"""Erase this profile's share of the live code_graph.db (C2 — Art.17).
|
|
1114
|
-
|
|
1115
|
-
The graph carries repository paths, file names and symbol names, and no
|
|
1116
|
-
table in it has a profile_id. When the store holds ONE profile the whole
|
|
1117
|
-
graph belongs to that person and wiping it is exactly right.
|
|
1118
|
-
|
|
1119
|
-
When it holds more than one, wiping it destroys the other people's data
|
|
1120
|
-
too — and an erasure request that erases a second data subject is itself
|
|
1121
|
-
a breach, not an over-achievement. So in that case only the rows that
|
|
1122
|
-
are unambiguously this profile's are removed: ``code_memory_links``
|
|
1123
|
-
joins a code node to an SLM fact, and facts carry a profile. The graph
|
|
1124
|
-
of the source code stays, because it describes a repository rather than
|
|
1125
|
-
a person, and the receipt says plainly that it was left.
|
|
1126
|
-
|
|
1127
|
-
Fail-open: an error is recorded in the returned dict so the caller can
|
|
1128
|
-
surface it, but it does NOT abort the rest of the erasure.
|
|
1129
|
-
"""
|
|
1130
|
-
result: dict = {"rows_deleted": 0}
|
|
1131
|
-
code_graph_path = data_root / "code_graph.db"
|
|
1132
|
-
if not code_graph_path.exists():
|
|
1133
|
-
return result
|
|
1134
|
-
try:
|
|
1135
|
-
conn = sqlite3.connect(str(code_graph_path))
|
|
1136
|
-
conn.isolation_level = None # autocommit so VACUUM can run
|
|
1137
|
-
try:
|
|
1138
|
-
conn.execute("PRAGMA foreign_keys=OFF")
|
|
1139
|
-
tables = [
|
|
1140
|
-
row[0]
|
|
1141
|
-
for row in conn.execute(
|
|
1142
|
-
"SELECT name FROM sqlite_master WHERE type='table' "
|
|
1143
|
-
"AND name NOT LIKE 'sqlite_%'"
|
|
1144
|
-
).fetchall()
|
|
1145
|
-
]
|
|
1146
|
-
total = 0
|
|
1147
|
-
conn.execute("BEGIN")
|
|
1148
|
-
if sole_profile:
|
|
1149
|
-
for tbl in tables:
|
|
1150
|
-
# Skip FTS virtual-table shadow files — deleting base rows handles them
|
|
1151
|
-
if tbl.endswith((
|
|
1152
|
-
"_fts", "_fts_data", "_fts_idx",
|
|
1153
|
-
"_fts_content", "_fts_docsize", "_fts_config",
|
|
1154
|
-
)):
|
|
1155
|
-
continue
|
|
1156
|
-
cur = conn.execute(f"DELETE FROM {tbl}") # noqa: S608
|
|
1157
|
-
total += cur.rowcount
|
|
1158
|
-
result["scope"] = "whole_graph"
|
|
1159
|
-
else:
|
|
1160
|
-
result["scope"] = "links_only"
|
|
1161
|
-
result["retained_reason"] = (
|
|
1162
|
-
"another profile shares this graph; the source-code "
|
|
1163
|
-
"structure describes a repository, not this person, and "
|
|
1164
|
-
"deleting it would erase another data subject's records"
|
|
1165
|
-
)
|
|
1166
|
-
owned = self._fact_ids_for(profile_id) if profile_id else []
|
|
1167
|
-
if owned is None:
|
|
1168
|
-
# Not knowing which links are this person's is a failure
|
|
1169
|
-
# to erase, not an empty erasure.
|
|
1170
|
-
raise sqlite3.OperationalError(
|
|
1171
|
-
"could not list this profile's facts, so its code "
|
|
1172
|
-
"links cannot be identified"
|
|
1173
|
-
)
|
|
1174
|
-
if "code_memory_links" in tables and owned:
|
|
1175
|
-
cur = conn.execute(
|
|
1176
|
-
"DELETE FROM code_memory_links WHERE slm_fact_id IN "
|
|
1177
|
-
"(SELECT value FROM json_each(?))",
|
|
1178
|
-
(json.dumps(owned),),
|
|
1179
|
-
)
|
|
1180
|
-
total += cur.rowcount
|
|
1181
|
-
conn.execute("PRAGMA foreign_keys=ON")
|
|
1182
|
-
conn.execute("COMMIT")
|
|
1183
|
-
# VACUUM must run outside any transaction (autocommit mode required)
|
|
1184
|
-
conn.execute("VACUUM")
|
|
1185
|
-
result["rows_deleted"] = total
|
|
1186
|
-
finally:
|
|
1187
|
-
conn.close()
|
|
1188
|
-
except Exception as exc: # noqa: BLE001
|
|
1189
|
-
logger.warning("GDPR erase: code_graph.db wipe failed: %s", exc)
|
|
1190
|
-
result["error"] = str(exc)
|
|
1191
|
-
return result
|
|
1192
|
-
|
|
1193
|
-
def _export_code_graph(self, data_root: Path) -> dict | None:
|
|
1194
|
-
"""Read code_graph.db for Art.15 export (C2).
|
|
1195
|
-
|
|
1196
|
-
Returns a dict keyed by table name whose values are lists of row dicts,
|
|
1197
|
-
or None if the file does not exist or cannot be read.
|
|
1198
|
-
"""
|
|
1199
|
-
code_graph_path = data_root / "code_graph.db"
|
|
1200
|
-
if not code_graph_path.exists():
|
|
1201
|
-
return None
|
|
1202
|
-
export: dict = {}
|
|
1203
|
-
try:
|
|
1204
|
-
uri = f"file:{code_graph_path}?mode=ro"
|
|
1205
|
-
conn = sqlite3.connect(uri, uri=True, timeout=5)
|
|
1206
|
-
conn.row_factory = sqlite3.Row
|
|
1207
|
-
try:
|
|
1208
|
-
tables = [
|
|
1209
|
-
row[0]
|
|
1210
|
-
for row in conn.execute(
|
|
1211
|
-
"SELECT name FROM sqlite_master WHERE type='table' "
|
|
1212
|
-
"AND name NOT LIKE 'sqlite_%'"
|
|
1213
|
-
).fetchall()
|
|
1214
|
-
]
|
|
1215
|
-
for tbl in tables:
|
|
1216
|
-
if tbl.endswith((
|
|
1217
|
-
"_fts", "_fts_data", "_fts_idx",
|
|
1218
|
-
"_fts_content", "_fts_docsize", "_fts_config",
|
|
1219
|
-
)):
|
|
1220
|
-
continue
|
|
1221
|
-
try:
|
|
1222
|
-
rows = conn.execute(
|
|
1223
|
-
f"SELECT * FROM {tbl} LIMIT 10000" # noqa: S608
|
|
1224
|
-
).fetchall()
|
|
1225
|
-
export[tbl] = [dict(r) for r in rows]
|
|
1226
|
-
except Exception as exc: # noqa: BLE001
|
|
1227
|
-
logger.warning("GDPR export: code_graph table %s failed: %s", tbl, exc)
|
|
1228
|
-
finally:
|
|
1229
|
-
conn.close()
|
|
1230
|
-
except Exception as exc: # noqa: BLE001
|
|
1231
|
-
logger.warning("GDPR export: code_graph.db read failed: %s", exc)
|
|
1232
|
-
return None
|
|
1233
|
-
return export if export else None
|
|
1234
|
-
|
|
1235
|
-
def _export_learning_signals(
|
|
1236
|
-
self, data_root: Path, profile_id: str
|
|
1237
|
-
) -> dict | None:
|
|
1238
|
-
"""Read this workspace's rows out of ``learning.db`` for an export.
|
|
1239
|
-
|
|
1240
|
-
Everything the system inferred about how somebody works is personal
|
|
1241
|
-
data about them, and an export that leaves it out is not the export it
|
|
1242
|
-
says it is. Erasure already covers this file; asking for it did not.
|
|
1243
|
-
|
|
1244
|
-
Scoped by ``profile_id`` wherever the table has one, and skipped where
|
|
1245
|
-
it does not -- a table with no workspace column holds nothing that can
|
|
1246
|
-
be attributed to one workspace, and returning it would export another
|
|
1247
|
-
workspace's rows into this person's file.
|
|
1248
|
-
"""
|
|
1249
|
-
learning_path = data_root / "learning.db"
|
|
1250
|
-
if not learning_path.exists():
|
|
1251
|
-
return None
|
|
1252
|
-
export: dict = {}
|
|
1253
|
-
try:
|
|
1254
|
-
conn = sqlite3.connect(
|
|
1255
|
-
f"file:{learning_path}?mode=ro", uri=True, timeout=5,
|
|
1256
|
-
)
|
|
1257
|
-
conn.row_factory = sqlite3.Row
|
|
1258
|
-
try:
|
|
1259
|
-
tables = [
|
|
1260
|
-
row[0] for row in conn.execute(
|
|
1261
|
-
"SELECT name FROM sqlite_master WHERE type='table' "
|
|
1262
|
-
"AND name NOT LIKE 'sqlite_%'"
|
|
1263
|
-
).fetchall()
|
|
1264
|
-
]
|
|
1265
|
-
for table in tables:
|
|
1266
|
-
if table.endswith((
|
|
1267
|
-
"_fts", "_fts_data", "_fts_idx",
|
|
1268
|
-
"_fts_content", "_fts_docsize", "_fts_config",
|
|
1269
|
-
)):
|
|
1270
|
-
continue
|
|
1271
|
-
try:
|
|
1272
|
-
columns = {
|
|
1273
|
-
row[1] for row in
|
|
1274
|
-
conn.execute(f"PRAGMA table_info({table})") # noqa: S608
|
|
1275
|
-
}
|
|
1276
|
-
if "profile_id" not in columns:
|
|
1277
|
-
continue
|
|
1278
|
-
rows = conn.execute(
|
|
1279
|
-
f"SELECT * FROM {table} WHERE profile_id = ? " # noqa: S608
|
|
1280
|
-
f"LIMIT 10000",
|
|
1281
|
-
(profile_id,),
|
|
1282
|
-
).fetchall()
|
|
1283
|
-
if rows:
|
|
1284
|
-
export[table] = [dict(row) for row in rows]
|
|
1285
|
-
except Exception as exc: # noqa: BLE001
|
|
1286
|
-
logger.warning(
|
|
1287
|
-
"export: learning table %s skipped: %s", table, exc,
|
|
1288
|
-
)
|
|
1289
|
-
finally:
|
|
1290
|
-
conn.close()
|
|
1291
|
-
except Exception as exc: # noqa: BLE001
|
|
1292
|
-
logger.warning("export: learning.db could not be read: %s", exc)
|
|
1293
|
-
return None
|
|
1294
|
-
return export or None
|
|
1295
|
-
|
|
1296
|
-
# -- C1: backup obligation helpers -------------------------------------
|
|
1297
|
-
|
|
1298
|
-
def _record_backup_obligations(
|
|
1299
|
-
self,
|
|
1300
|
-
data_root: Path,
|
|
1301
|
-
profile_id: str,
|
|
1302
|
-
erasure_id: str,
|
|
1303
|
-
counts: dict,
|
|
1304
|
-
) -> int:
|
|
1305
|
-
"""Scan backup snapshots and record any that contain *profile_id* data.
|
|
1306
|
-
|
|
1307
|
-
Returns the total count of pending obligations after recording
|
|
1308
|
-
(including those created in prior erasure passes for the same profile).
|
|
1309
|
-
Fail-closed: any unhandled exception propagates to the caller who sets
|
|
1310
|
-
``backup_scan_failed`` to block the completeness claim.
|
|
1311
|
-
"""
|
|
1312
|
-
bom = _get_backup_obligations_module()
|
|
1313
|
-
if bom is None:
|
|
1314
|
-
logger.warning(
|
|
1315
|
-
"GDPR erase: backup_obligations module unavailable — "
|
|
1316
|
-
"backup residue will not be tracked for profile %r",
|
|
1317
|
-
profile_id,
|
|
1318
|
-
)
|
|
1319
|
-
# Cannot track → treat as pending so completeness is blocked.
|
|
1320
|
-
return 1
|
|
1321
|
-
|
|
1322
|
-
backup_dir = data_root / "backups"
|
|
1323
|
-
retention_days = _retention_days_from_config()
|
|
1324
|
-
store = bom.BackupObligationStore(data_root)
|
|
1325
|
-
|
|
1326
|
-
# Scan all backup snapshots for this profile's data.
|
|
1327
|
-
try:
|
|
1328
|
-
hits = bom.scan_backup_snapshots_for_profile(backup_dir, profile_id)
|
|
1329
|
-
except Exception as exc: # noqa: BLE001
|
|
1330
|
-
logger.error(
|
|
1331
|
-
"GDPR erase: backup snapshot scan raised: %s — "
|
|
1332
|
-
"failing closed to block completeness",
|
|
1333
|
-
exc,
|
|
1334
|
-
)
|
|
1335
|
-
counts["backup_scan_error"] = str(exc)
|
|
1336
|
-
raise # propagate so caller sets backup_scan_failed
|
|
1337
|
-
|
|
1338
|
-
snapshots_with_data = len(hits)
|
|
1339
|
-
counts["backup_snapshots_scanned"] = snapshots_with_data
|
|
1340
|
-
recorded = 0
|
|
1341
|
-
for snap_path, snap_epoch in hits:
|
|
1342
|
-
try:
|
|
1343
|
-
store.record(
|
|
1344
|
-
profile_id=profile_id,
|
|
1345
|
-
erasure_id=erasure_id,
|
|
1346
|
-
snapshot_path=snap_path,
|
|
1347
|
-
snapshot_epoch=snap_epoch,
|
|
1348
|
-
retention_days=retention_days,
|
|
1349
|
-
)
|
|
1350
|
-
recorded += 1
|
|
1351
|
-
except Exception as exc: # noqa: BLE001
|
|
1352
|
-
# Recording failure for a single snapshot must not silently
|
|
1353
|
-
# skip the obligation — log and count so completeness is blocked.
|
|
1354
|
-
logger.error(
|
|
1355
|
-
"GDPR erase: failed to record obligation for snapshot %r: %s",
|
|
1356
|
-
snap_path, exc,
|
|
1357
|
-
)
|
|
1358
|
-
counts["backup_record_errors"] = counts.get("backup_record_errors", 0) + 1
|
|
1359
|
-
# Still include in pending count (fail-closed).
|
|
1360
|
-
recorded += 1
|
|
1361
|
-
|
|
1362
|
-
counts["backup_obligations_recorded"] = recorded
|
|
1363
|
-
# Return the authoritative pending count (includes obligations from prior
|
|
1364
|
-
# erasure passes for the same profile that were not yet discharged).
|
|
1365
|
-
return store.count_pending(profile_id)
|
|
1366
|
-
|
|
1367
|
-
# -- I7: post-erasure residue scanner ----------------------------------
|
|
1368
|
-
|
|
1369
|
-
def _scan_fts_residue(
|
|
1370
|
-
self,
|
|
1371
|
-
profile_id: str,
|
|
1372
|
-
tables: list[str],
|
|
1373
|
-
counts: dict,
|
|
1374
|
-
) -> None:
|
|
1375
|
-
"""Sweep FTS5 shadow tables for orphaned rowids after main-table erasure.
|
|
1376
|
-
|
|
1377
|
-
FTS5 tables maintain several shadow tables (``_data``, ``_idx``,
|
|
1378
|
-
``_content``, ``_docsize``, ``_config``). A correct DELETE + VACUUM
|
|
1379
|
-
cycle via FTS5's ``content=`` mechanism will purge shadow rows
|
|
1380
|
-
automatically. This sweep cross-checks: if any main memory/facts table
|
|
1381
|
-
is empty for the profile yet the corresponding FTS ``_content`` shadow
|
|
1382
|
-
still has rows, that is residue. Updates ``counts["fts_residue_rows"]``.
|
|
1383
|
-
"""
|
|
1384
|
-
fts_residue = 0
|
|
1385
|
-
try:
|
|
1386
|
-
all_tables = {
|
|
1387
|
-
row[0]
|
|
1388
|
-
for row in self._db.execute(
|
|
1389
|
-
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
1390
|
-
)
|
|
1391
|
-
}
|
|
1392
|
-
except Exception as exc: # noqa: BLE001
|
|
1393
|
-
logger.warning("GDPR I7: FTS shadow table listing failed: %s", exc)
|
|
1394
|
-
return
|
|
1395
|
-
|
|
1396
|
-
for tbl in tables:
|
|
1397
|
-
content_shadow = f"{tbl}_fts_content"
|
|
1398
|
-
if content_shadow not in all_tables:
|
|
1399
|
-
continue
|
|
1400
|
-
try:
|
|
1401
|
-
# _fts_content stores one row per indexed row. After erasure
|
|
1402
|
-
# the content rows should be zero for this profile. We cannot
|
|
1403
|
-
# filter by profile_id directly (FTS content is a rowid join),
|
|
1404
|
-
# so we count ALL content rows and compare with the main table
|
|
1405
|
-
# row count for this profile (should both be 0).
|
|
1406
|
-
main_count_rows = self._db.execute(
|
|
1407
|
-
f"SELECT COUNT(*) AS c FROM {tbl} WHERE profile_id = ?", # noqa: S608
|
|
1408
|
-
(profile_id,),
|
|
1409
|
-
)
|
|
1410
|
-
main_count = int(dict(main_count_rows[0])["c"]) if main_count_rows else 0
|
|
1411
|
-
if main_count > 0:
|
|
1412
|
-
# Main table still has rows — not an FTS-shadow issue, already
|
|
1413
|
-
# caught by the main residue recount.
|
|
1414
|
-
continue
|
|
1415
|
-
fts_rows = self._db.execute(
|
|
1416
|
-
f"SELECT COUNT(*) AS c FROM {content_shadow}" # noqa: S608
|
|
1417
|
-
)
|
|
1418
|
-
fts_count = int(dict(fts_rows[0])["c"]) if fts_rows else 0
|
|
1419
|
-
if fts_count > 0:
|
|
1420
|
-
logger.warning(
|
|
1421
|
-
"GDPR I7: FTS shadow %s has %d rows after profile %r erasure",
|
|
1422
|
-
content_shadow, fts_count, profile_id,
|
|
1423
|
-
)
|
|
1424
|
-
fts_residue += fts_count
|
|
1425
|
-
except Exception as exc: # noqa: BLE001
|
|
1426
|
-
logger.warning(
|
|
1427
|
-
"GDPR I7: FTS shadow scan for %s failed: %s", content_shadow, exc
|
|
1428
|
-
)
|
|
1429
|
-
if fts_residue:
|
|
1430
|
-
counts["fts_residue_rows"] = fts_residue
|
|
1431
|
-
|
|
1432
|
-
def _scan_wal_residue(self, counts: dict) -> None:
|
|
1433
|
-
"""Trigger a WAL checkpoint so the WAL does not re-introduce erased data.
|
|
1434
|
-
|
|
1435
|
-
After VACUUM the WAL should already be flushed, but an explicit
|
|
1436
|
-
PRAGMA wal_checkpoint(TRUNCATE) ensures the WAL file is zeroed and
|
|
1437
|
-
cannot carry deleted pages forward into a subsequent read.
|
|
1438
|
-
"""
|
|
1439
|
-
try:
|
|
1440
|
-
self._db.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
1441
|
-
except Exception as exc: # noqa: BLE001
|
|
1442
|
-
logger.warning(
|
|
1443
|
-
"GDPR I7: WAL checkpoint failed: %s — WAL may retain erased pages",
|
|
1444
|
-
exc,
|
|
1445
|
-
)
|
|
1446
|
-
counts["wal_checkpoint_failed"] = 1
|
|
1447
|
-
|
|
1448
|
-
# -- Audit Trail -------------------------------------------------------
|
|
1449
|
-
|
|
1450
|
-
def get_audit_trail(self, profile_id: str, limit: int = 100) -> list[dict]:
|
|
1451
|
-
"""Get compliance audit trail for a profile."""
|
|
1452
|
-
rows = self._db.execute(
|
|
1453
|
-
"SELECT * FROM compliance_audit WHERE profile_id = ? ORDER BY timestamp DESC LIMIT ?",
|
|
1454
|
-
(profile_id, limit),
|
|
1455
|
-
)
|
|
1456
|
-
return [dict(r) for r in rows]
|
|
1457
|
-
|
|
1458
|
-
def _audit(
|
|
1459
|
-
self,
|
|
1460
|
-
action: str,
|
|
1461
|
-
target_type: str,
|
|
1462
|
-
target_id: str,
|
|
1463
|
-
details: str,
|
|
1464
|
-
profile_id: str | None = None,
|
|
1465
|
-
) -> None:
|
|
1466
|
-
"""Log a compliance action."""
|
|
1467
|
-
from superlocalmemory.storage.models import _new_id
|
|
1468
|
-
|
|
1469
|
-
pid = profile_id if profile_id is not None else target_id
|
|
1470
|
-
self._db.execute(
|
|
1471
|
-
"INSERT INTO compliance_audit "
|
|
1472
|
-
"(audit_id, profile_id, action, target_type, target_id, details, timestamp) "
|
|
1473
|
-
"VALUES (?,?,?,?,?,?,?)",
|
|
1474
|
-
(_new_id(), pid, action, target_type, target_id, details, _now()),
|
|
1475
|
-
)
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
def _now() -> str:
|
|
1479
|
-
return datetime.now(UTC).isoformat()
|