superlocalmemory 3.6.22 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +275 -72
  3. package/bin/slm-npm +43 -89
  4. package/docs/pi-dev-integration.md +43 -0
  5. package/ide/configs/antigravity-mcp.json +2 -2
  6. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  7. package/ide/configs/claude-desktop-mcp.json +2 -2
  8. package/ide/configs/windsurf-mcp.json +2 -2
  9. package/ide/hooks/context-hook.js +6 -2
  10. package/ide/hooks/post-recall-hook.js +7 -3
  11. package/ide/hooks/tool-event-hook.sh +2 -1
  12. package/package.json +19 -10
  13. package/plugin/.claude-plugin/plugin.json +1 -1
  14. package/plugin/_GENERATED.md +1 -1
  15. package/plugin/agents/slm-memory-advisor.md +1 -1
  16. package/plugin/requirements.txt +1 -1
  17. package/plugin/skills/slm-session/SKILL.md +1 -1
  18. package/plugin-src/rules/AGENTS.md +1 -1
  19. package/pyproject.toml +40 -8
  20. package/scripts/postinstall-interactive.js +17 -94
  21. package/scripts/postinstall.js +185 -258
  22. package/scripts/preuninstall.js +9 -50
  23. package/src/superlocalmemory/__init__.py +2 -2
  24. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  25. package/src/superlocalmemory/attribution/signer.py +34 -19
  26. package/src/superlocalmemory/attribution/watermark.py +1 -1
  27. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  28. package/src/superlocalmemory/cli/commands.py +490 -195
  29. package/src/superlocalmemory/cli/context_commands.py +5 -4
  30. package/src/superlocalmemory/cli/daemon.py +282 -187
  31. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  32. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  33. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  34. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  35. package/src/superlocalmemory/cli/main.py +128 -31
  36. package/src/superlocalmemory/cli/pending_store.py +54 -38
  37. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  38. package/src/superlocalmemory/cli/service_installer.py +57 -52
  39. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  40. package/src/superlocalmemory/cli/version_banner.py +2 -1
  41. package/src/superlocalmemory/code_graph/config.py +3 -1
  42. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  43. package/src/superlocalmemory/core/config.py +65 -20
  44. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  45. package/src/superlocalmemory/core/context_cache.py +56 -8
  46. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  47. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  48. package/src/superlocalmemory/core/embeddings.py +54 -18
  49. package/src/superlocalmemory/core/engine.py +150 -104
  50. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  51. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  52. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  53. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  54. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  55. package/src/superlocalmemory/core/health_monitor.py +4 -2
  56. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  57. package/src/superlocalmemory/core/injection.py +69 -18
  58. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  59. package/src/superlocalmemory/core/maintenance.py +23 -22
  60. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  61. package/src/superlocalmemory/core/mutations.py +143 -0
  62. package/src/superlocalmemory/core/platform_utils.py +7 -4
  63. package/src/superlocalmemory/core/ram_lock.py +16 -5
  64. package/src/superlocalmemory/core/rate_limit.py +1 -1
  65. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  66. package/src/superlocalmemory/core/recall_worker.py +76 -59
  67. package/src/superlocalmemory/core/registry.py +1 -1
  68. package/src/superlocalmemory/core/scale_engine.py +293 -0
  69. package/src/superlocalmemory/core/score_contract.py +62 -0
  70. package/src/superlocalmemory/core/security_primitives.py +3 -1
  71. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  72. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  73. package/src/superlocalmemory/core/tier_manager.py +32 -20
  74. package/src/superlocalmemory/core/worker_pool.py +13 -4
  75. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  76. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  77. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  79. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  80. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  81. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  82. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  83. package/src/superlocalmemory/encoding/foresight.py +2 -0
  84. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  85. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  86. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  87. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  88. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  89. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  90. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  91. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  92. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  93. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  94. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  95. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  96. package/src/superlocalmemory/hooks/claude_code_hooks.py +126 -39
  97. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  98. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  99. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  100. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  101. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  102. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  103. package/src/superlocalmemory/hooks/hook_handlers.py +241 -55
  104. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  105. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  106. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  107. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  108. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  109. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  110. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  111. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  112. package/src/superlocalmemory/infra/backup.py +7 -5
  113. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  114. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  115. package/src/superlocalmemory/infra/data_root.py +199 -0
  116. package/src/superlocalmemory/infra/event_bus.py +3 -1
  117. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  118. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  119. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  120. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  121. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  122. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  123. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  124. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  125. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  126. package/src/superlocalmemory/learning/ensemble.py +11 -0
  127. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  128. package/src/superlocalmemory/learning/feedback.py +1 -1
  129. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  130. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  131. package/src/superlocalmemory/learning/ranker.py +4 -1
  132. package/src/superlocalmemory/learning/source_quality.py +1 -1
  133. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  134. package/src/superlocalmemory/llm/backbone.py +13 -8
  135. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  136. package/src/superlocalmemory/math/fisher.py +1 -1
  137. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  138. package/src/superlocalmemory/math/hopfield.py +1 -1
  139. package/src/superlocalmemory/math/langevin.py +1 -1
  140. package/src/superlocalmemory/math/polar_quant.py +3 -4
  141. package/src/superlocalmemory/math/qjl.py +1 -1
  142. package/src/superlocalmemory/math/sheaf.py +1 -1
  143. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  144. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  145. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  146. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  147. package/src/superlocalmemory/mcp/server.py +39 -13
  148. package/src/superlocalmemory/mcp/shared.py +69 -3
  149. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  150. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  151. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  152. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  153. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  154. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  155. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  156. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  157. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  158. package/src/superlocalmemory/mesh/broker.py +2 -0
  159. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  160. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  161. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  162. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  163. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  164. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  165. package/src/superlocalmemory/optimize/config/store.py +6 -4
  166. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  167. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  168. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  169. package/src/superlocalmemory/optimize/storage/db.py +14 -13
  170. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  171. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  172. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  173. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  174. package/src/superlocalmemory/retrieval/engine.py +169 -79
  175. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  176. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  177. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  178. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  179. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  180. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  181. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  182. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  183. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  184. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  185. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  186. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  187. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  188. package/src/superlocalmemory/server/api.py +10 -7
  189. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  190. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  191. package/src/superlocalmemory/server/route_mutations.py +84 -0
  192. package/src/superlocalmemory/server/routes/agents.py +8 -6
  193. package/src/superlocalmemory/server/routes/brain.py +14 -12
  194. package/src/superlocalmemory/server/routes/chat.py +29 -12
  195. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  196. package/src/superlocalmemory/server/routes/helpers.py +29 -4
  197. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  198. package/src/superlocalmemory/server/routes/memories.py +104 -43
  199. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  200. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  201. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  202. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  203. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  204. package/src/superlocalmemory/server/security_middleware.py +1 -1
  205. package/src/superlocalmemory/server/ui.py +6 -3
  206. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  207. package/src/superlocalmemory/server/write_identity.py +147 -0
  208. package/src/superlocalmemory/storage/access_log.py +4 -3
  209. package/src/superlocalmemory/storage/database.py +118 -25
  210. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  211. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  212. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  213. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  214. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  215. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  216. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  217. package/src/superlocalmemory/storage/models.py +16 -0
  218. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  219. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  220. package/src/superlocalmemory/ui/favicon.svg +5 -0
  221. package/src/superlocalmemory/ui/index.html +1 -0
  222. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  223. package/src/superlocalmemory/ui/js/core.js +49 -8
  224. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  225. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  226. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  227. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  228. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  229. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  230. package/src/superlocalmemory/ui/js/settings.js +4 -2
  231. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  232. package/bin/slm +0 -59
  233. package/bin/slm.bat +0 -77
  234. package/bin/slm.cmd +0 -5
  235. package/ide/integrations/langchain/README.md +0 -106
  236. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  237. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  238. package/ide/integrations/langchain/pyproject.toml +0 -38
  239. package/ide/integrations/langchain/tests/__init__.py +0 -3
  240. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  241. package/ide/integrations/langchain/tests/test_security.py +0 -117
  242. package/ide/integrations/llamaindex/README.md +0 -81
  243. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  244. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  245. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  246. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  247. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  248. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  249. package/plugin-src/.mcp.json +0 -12
  250. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  251. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  252. package/plugin-src/hooks/.gitkeep +0 -0
  253. package/plugin-src/hooks/hooks.json +0 -23
  254. package/plugin-src/manifest.json +0 -25
  255. package/plugin-src/requirements.txt +0 -1
  256. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  257. package/plugin-src/scripts/ensure-venv.bat +0 -122
  258. package/plugin-src/scripts/ensure-venv.sh +0 -105
  259. package/plugin-src/scripts/slm-launch +0 -15
  260. package/plugin-src/scripts/slm-launch.bat +0 -17
  261. package/plugin-src/settings.json +0 -16
  262. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  263. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  264. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  265. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  266. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  267. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  268. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  269. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  270. package/scripts/_savings_math.py +0 -270
  271. package/scripts/build-dmg.sh +0 -417
  272. package/scripts/build-plugin.js +0 -742
  273. package/scripts/build-slm-hook.ps1 +0 -40
  274. package/scripts/build-slm-hook.sh +0 -45
  275. package/scripts/build_entry.py +0 -452
  276. package/scripts/ci/stage5b_gate.sh +0 -50
  277. package/scripts/dogfood_savings.py +0 -490
  278. package/scripts/generate-thumbnails.py +0 -218
  279. package/scripts/install-skills.ps1 +0 -4
  280. package/scripts/install-skills.sh +0 -5
  281. package/scripts/install.ps1 +0 -701
  282. package/scripts/install.sh +0 -1015
  283. package/scripts/postinstall_binary.js +0 -287
  284. package/scripts/prepack.js +0 -33
  285. package/scripts/release_manifest.py +0 -273
  286. package/scripts/slm-hook.spec +0 -56
  287. package/scripts/start-dashboard.ps1 +0 -52
  288. package/scripts/start-dashboard.sh +0 -41
  289. package/scripts/sync-wiki.ps1 +0 -127
  290. package/scripts/sync-wiki.sh +0 -82
  291. package/scripts/test-dmg.sh +0 -161
  292. package/scripts/test-npm-package.ps1 +0 -252
  293. package/scripts/test-npm-package.sh +0 -207
  294. package/scripts/verify-install.ps1 +0 -294
  295. package/scripts/verify-install.sh +0 -266
  296. package/scripts/verify-v27.ps1 +0 -301
  297. package/scripts/verify-v27.sh +0 -233
  298. package/src/superlocalmemory.egg-info/PKG-INFO +0 -513
  299. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  300. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  301. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  302. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  303. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -22,7 +22,7 @@ Key patterns implemented:
22
22
  - Narrative fact extraction in LLM modes (self-contained, context-rich)
23
23
 
24
24
  Part of Qualixar | Author: Varun Pratap Bhardwaj
25
- License: Elastic-2.0
25
+ License: AGPL-3.0-or-later
26
26
  """
27
27
 
28
28
  from __future__ import annotations
@@ -73,6 +73,8 @@ def foresight_to_temporal_events(
73
73
  for eid in entity_ids:
74
74
  event = TemporalEvent(
75
75
  profile_id=profile_id,
76
+ scope=fact.scope,
77
+ shared_with=fact.shared_with,
76
78
  entity_id=eid,
77
79
  fact_id=fact.fact_id,
78
80
  observation_date=fact.observation_date,
@@ -12,7 +12,7 @@ Edge types: ENTITY (shared entity, weight 1.0), TEMPORAL (exp-decay,
12
12
  weight 0.8). CONTRADICTION exposed for external Sheaf module (Wave 4).
13
13
 
14
14
  Part of Qualixar | Author: Varun Pratap Bhardwaj
15
- License: Elastic-2.0
15
+ License: AGPL-3.0-or-later
16
16
  """
17
17
  from __future__ import annotations
18
18
 
@@ -306,6 +306,8 @@ class TemporalParser:
306
306
 
307
307
  return TemporalEvent(
308
308
  profile_id=fact.profile_id,
309
+ scope=fact.scope,
310
+ shared_with=fact.shared_with,
309
311
  entity_id=entity_id,
310
312
  fact_id=fact.fact_id,
311
313
  observation_date=obs_date,
@@ -0,0 +1,13 @@
1
+ """Evaluation artifacts that are isolated from production retrieval paths."""
2
+
3
+ from superlocalmemory.evaluation.calibration import (
4
+ CalibrationArtifactError,
5
+ build_calibration_report,
6
+ compute_calibration_metrics,
7
+ )
8
+
9
+ __all__ = [
10
+ "CalibrationArtifactError",
11
+ "build_calibration_report",
12
+ "compute_calibration_metrics",
13
+ ]
@@ -0,0 +1,308 @@
1
+ """Held-out answer-probability calibration report tooling.
2
+
3
+ Raw retrieval scores are ranking utilities. They are retained for distribution
4
+ diagnostics but are never passed to Brier, ECE, reliability, or selective-risk
5
+ calculations. Those metrics require an explicitly supplied probability from a
6
+ separately fitted calibrator and a disjoint held-out evaluation artifact.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import math
14
+ from collections.abc import Iterable, Sequence
15
+ from typing import Any
16
+
17
+ INPUT_SCHEMA = "superlocalmemory.calibration-input/v1"
18
+ REPORT_SCHEMA = "superlocalmemory.calibration-report/v1"
19
+ MIN_HELD_OUT_EXAMPLES = 100
20
+ DEFAULT_THRESHOLDS = (0.0, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95)
21
+ _RAW_AS_PROBABILITY_METHODS = {
22
+ "identity",
23
+ "raw",
24
+ "raw_score",
25
+ "raw_retrieval_score",
26
+ "none",
27
+ }
28
+
29
+
30
+ class CalibrationArtifactError(ValueError):
31
+ """The input cannot support a truthful held-out calibration report."""
32
+
33
+
34
+ def _canonical_sha256(value: object) -> str:
35
+ payload = json.dumps(
36
+ value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
37
+ ).encode("utf-8")
38
+ return hashlib.sha256(payload).hexdigest()
39
+
40
+
41
+ def _finite_number(value: object, *, field: str) -> float:
42
+ if isinstance(value, bool):
43
+ raise CalibrationArtifactError(f"{field} must be a finite number")
44
+ try:
45
+ number = float(value)
46
+ except (TypeError, ValueError) as exc:
47
+ raise CalibrationArtifactError(f"{field} must be a finite number") from exc
48
+ if not math.isfinite(number):
49
+ raise CalibrationArtifactError(f"{field} must be a finite number")
50
+ return number
51
+
52
+
53
+ def _validated_rows(examples: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
54
+ validated: list[dict[str, Any]] = []
55
+ seen_ids: set[str] = set()
56
+ for index, example in enumerate(examples):
57
+ if not isinstance(example, dict):
58
+ raise CalibrationArtifactError(f"examples[{index}] must be an object")
59
+ example_id = str(example.get("example_id", "")).strip()
60
+ if not example_id:
61
+ raise CalibrationArtifactError(f"examples[{index}].example_id is required")
62
+ if example_id in seen_ids:
63
+ raise CalibrationArtifactError("example_id values must be unique")
64
+ seen_ids.add(example_id)
65
+
66
+ if "predicted_probability" not in example:
67
+ raise CalibrationArtifactError(
68
+ "predicted_probability is required; raw retrieval scores are not probabilities"
69
+ )
70
+ probability = _finite_number(
71
+ example["predicted_probability"], field="predicted_probability"
72
+ )
73
+ if probability < 0.0 or probability > 1.0:
74
+ raise CalibrationArtifactError(
75
+ "predicted_probability must be between 0 and 1"
76
+ )
77
+
78
+ label = example.get("label")
79
+ if isinstance(label, bool):
80
+ label = int(label)
81
+ if label not in (0, 1):
82
+ raise CalibrationArtifactError("label must be binary (0 or 1)")
83
+ raw_score = _finite_number(
84
+ example.get("raw_retrieval_score"), field="raw_retrieval_score"
85
+ )
86
+ validated.append(
87
+ {
88
+ "example_id": example_id,
89
+ "raw_retrieval_score": raw_score,
90
+ "predicted_probability": probability,
91
+ "label": int(label),
92
+ }
93
+ )
94
+ return validated
95
+
96
+
97
+ def _round(value: float) -> float:
98
+ return round(value, 12)
99
+
100
+
101
+ def compute_calibration_metrics(
102
+ examples: Sequence[dict[str, Any]],
103
+ *,
104
+ bin_count: int = 10,
105
+ thresholds: Iterable[float] = DEFAULT_THRESHOLDS,
106
+ ) -> dict[str, Any]:
107
+ """Compute probability metrics while reporting raw scores separately."""
108
+ if not isinstance(bin_count, int) or isinstance(bin_count, bool) or bin_count < 2:
109
+ raise CalibrationArtifactError("bin_count must be an integer >= 2")
110
+ rows = _validated_rows(examples)
111
+ if not rows:
112
+ raise CalibrationArtifactError("at least one example is required")
113
+ total = len(rows)
114
+
115
+ brier = sum(
116
+ (row["predicted_probability"] - row["label"]) ** 2 for row in rows
117
+ ) / total
118
+
119
+ reliability: list[dict[str, Any]] = []
120
+ ece = 0.0
121
+ mce = 0.0
122
+ for index in range(bin_count):
123
+ lower = index / bin_count
124
+ upper = (index + 1) / bin_count
125
+ members = [
126
+ row for row in rows
127
+ if lower <= row["predicted_probability"]
128
+ and (row["predicted_probability"] < upper or index == bin_count - 1)
129
+ ]
130
+ if members:
131
+ mean_probability = sum(row["predicted_probability"] for row in members) / len(members)
132
+ observed_accuracy = sum(row["label"] for row in members) / len(members)
133
+ gap = abs(mean_probability - observed_accuracy)
134
+ ece += len(members) / total * gap
135
+ mce = max(mce, gap)
136
+ mean_value: float | None = _round(mean_probability)
137
+ accuracy_value: float | None = _round(observed_accuracy)
138
+ gap_value: float | None = _round(gap)
139
+ else:
140
+ mean_value = accuracy_value = gap_value = None
141
+ reliability.append(
142
+ {
143
+ "bin_index": index,
144
+ "lower_inclusive": _round(lower),
145
+ "upper_inclusive": index == bin_count - 1,
146
+ "upper": _round(upper),
147
+ "count": len(members),
148
+ "mean_probability": mean_value,
149
+ "observed_accuracy": accuracy_value,
150
+ "absolute_gap": gap_value,
151
+ }
152
+ )
153
+
154
+ selective: list[dict[str, Any]] = []
155
+ previous = -1.0
156
+ for raw_threshold in thresholds:
157
+ threshold = _finite_number(raw_threshold, field="selective threshold")
158
+ if threshold < 0.0 or threshold > 1.0:
159
+ raise CalibrationArtifactError("selective threshold must be between 0 and 1")
160
+ if threshold <= previous:
161
+ raise CalibrationArtifactError("selective thresholds must be strictly increasing")
162
+ previous = threshold
163
+ retained = [row for row in rows if row["predicted_probability"] >= threshold]
164
+ retained_count = len(retained)
165
+ coverage = retained_count / total
166
+ risk = (
167
+ sum(1 - row["label"] for row in retained) / retained_count
168
+ if retained_count
169
+ else None
170
+ )
171
+ selective.append(
172
+ {
173
+ "threshold": _round(threshold),
174
+ "retained": retained_count,
175
+ "coverage": _round(coverage),
176
+ "abstention_rate": _round(1.0 - coverage),
177
+ "selective_risk": _round(risk) if risk is not None else None,
178
+ }
179
+ )
180
+
181
+ raw_scores = [row["raw_retrieval_score"] for row in rows]
182
+ probabilities = [row["predicted_probability"] for row in rows]
183
+ return {
184
+ "example_count": total,
185
+ "brier_score": _round(brier),
186
+ "expected_calibration_error": _round(ece),
187
+ "maximum_calibration_error": _round(mce),
188
+ "reliability_bins": reliability,
189
+ "selective_risk": selective,
190
+ "probability_summary": {
191
+ "minimum": _round(min(probabilities)),
192
+ "maximum": _round(max(probabilities)),
193
+ "mean": _round(sum(probabilities) / total),
194
+ "unique_count": len(set(probabilities)),
195
+ "boundary_count": sum(value in (0.0, 1.0) for value in probabilities),
196
+ },
197
+ "raw_retrieval_score_summary": {
198
+ "minimum": _round(min(raw_scores)),
199
+ "maximum": _round(max(raw_scores)),
200
+ "mean": _round(sum(raw_scores) / total),
201
+ "unique_count": len(set(raw_scores)),
202
+ "boundary_count": sum(value in (0.0, 1.0) for value in raw_scores),
203
+ "used_in_probability_metrics": False,
204
+ },
205
+ }
206
+
207
+
208
+ def _require_sha256(value: object, *, field: str) -> str:
209
+ text = str(value or "").lower()
210
+ if len(text) != 64 or any(char not in "0123456789abcdef" for char in text):
211
+ raise CalibrationArtifactError(f"{field} must be a 64-character SHA-256")
212
+ return text
213
+
214
+
215
+ def build_calibration_report(
216
+ artifact: dict[str, Any],
217
+ *,
218
+ bin_count: int = 10,
219
+ thresholds: Iterable[float] = DEFAULT_THRESHOLDS,
220
+ ) -> dict[str, Any]:
221
+ """Validate a disjoint held-out artifact and build a non-activating report."""
222
+ if not isinstance(artifact, dict) or artifact.get("schema") != INPUT_SCHEMA:
223
+ raise CalibrationArtifactError(f"schema must be {INPUT_SCHEMA}")
224
+ calibrator = artifact.get("calibrator")
225
+ evaluation = artifact.get("evaluation")
226
+ examples = artifact.get("examples")
227
+ if not isinstance(calibrator, dict) or not isinstance(evaluation, dict):
228
+ raise CalibrationArtifactError("calibrator and evaluation metadata are required")
229
+ if not isinstance(examples, list):
230
+ raise CalibrationArtifactError("examples must be a list")
231
+
232
+ calibration_id = str(calibrator.get("calibration_id", "")).strip()
233
+ if not calibration_id:
234
+ raise CalibrationArtifactError("calibration_id is required")
235
+ method = str(calibrator.get("method", "")).strip().lower()
236
+ if not method or method in _RAW_AS_PROBABILITY_METHODS:
237
+ raise CalibrationArtifactError(
238
+ "a fitted calibrator is required; a raw score identity mapping is forbidden"
239
+ )
240
+ fit_dataset = str(calibrator.get("fit_dataset_id", "")).strip()
241
+ eval_dataset = str(evaluation.get("dataset_id", "")).strip()
242
+ if not fit_dataset or not eval_dataset or fit_dataset == eval_dataset:
243
+ raise CalibrationArtifactError(
244
+ "calibrator fit and held-out evaluation datasets must be distinct"
245
+ )
246
+ fit_split = str(calibrator.get("fit_split", "")).strip()
247
+ if not fit_split or fit_split == "held_out":
248
+ raise CalibrationArtifactError("fit_split must be distinct from held_out")
249
+ if evaluation.get("split") != "held_out":
250
+ raise CalibrationArtifactError("evaluation split must be held_out")
251
+ fit_artifact_hash = _require_sha256(
252
+ calibrator.get("fit_artifact_sha256"), field="fit_artifact_sha256"
253
+ )
254
+
255
+ validated = _validated_rows(examples)
256
+ if len(validated) < MIN_HELD_OUT_EXAMPLES:
257
+ raise CalibrationArtifactError(
258
+ f"held-out evaluation requires at least {MIN_HELD_OUT_EXAMPLES} examples"
259
+ )
260
+ expected_hash = _require_sha256(
261
+ evaluation.get("examples_sha256"), field="examples_sha256"
262
+ )
263
+ actual_hash = _canonical_sha256(examples)
264
+ if expected_hash != actual_hash:
265
+ raise CalibrationArtifactError("examples hash does not match held-out artifact")
266
+
267
+ metrics = compute_calibration_metrics(
268
+ validated, bin_count=bin_count, thresholds=thresholds
269
+ )
270
+ return {
271
+ "schema": REPORT_SCHEMA,
272
+ "evidence_status": "held_out_report",
273
+ "calibration_id": calibration_id,
274
+ "calibrator": {
275
+ "method": method,
276
+ "fit_dataset_id": fit_dataset,
277
+ "fit_split": fit_split,
278
+ "fit_artifact_sha256": fit_artifact_hash,
279
+ },
280
+ "evaluation": {
281
+ "dataset_id": eval_dataset,
282
+ "split": "held_out",
283
+ "examples_sha256": actual_hash,
284
+ },
285
+ "metrics": metrics,
286
+ "separation": {
287
+ "raw_score_field": "raw_retrieval_score",
288
+ "probability_metric_input": "predicted_probability",
289
+ "raw_retrieval_score_used_as_probability": False,
290
+ },
291
+ "runtime_calibration_status": "uncalibrated",
292
+ "runtime_activation_allowed": False,
293
+ "runtime_activation_reason": (
294
+ "This artifact is evaluation evidence only; production activation "
295
+ "requires a separately reviewed and frozen calibrator contract."
296
+ ),
297
+ "input_artifact_sha256": _canonical_sha256(artifact),
298
+ }
299
+
300
+
301
+ __all__ = [
302
+ "CalibrationArtifactError",
303
+ "INPUT_SCHEMA",
304
+ "MIN_HELD_OUT_EXAMPLES",
305
+ "REPORT_SCHEMA",
306
+ "build_calibration_report",
307
+ "compute_calibration_metrics",
308
+ ]
@@ -25,6 +25,7 @@ from datetime import datetime, timezone
25
25
  from pathlib import Path
26
26
  from typing import Optional
27
27
 
28
+ from superlocalmemory.infra.data_root import canonical_data_root
28
29
  from superlocalmemory.evolution.types import (
29
30
  EvolutionCandidate,
30
31
  EvolutionRecord,
@@ -128,7 +129,7 @@ class SkillEvolver:
128
129
  # at ~/.superlocalmemory so production callers pick it up
129
130
  # automatically. Tests inject their own.
130
131
  if budget is None:
131
- slm_home = Path.home() / ".superlocalmemory"
132
+ slm_home = canonical_data_root()
132
133
  budget = EvolutionBudget(
133
134
  profile_id=profile_id,
134
135
  learning_db=Path(self._db_path),