memorymaster 3.4.0__tar.gz → 3.5.0__tar.gz

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 (278) hide show
  1. {memorymaster-3.4.0 → memorymaster-3.5.0}/PKG-INFO +51 -1
  2. memorymaster-3.4.0/memorymaster.egg-info/PKG-INFO → memorymaster-3.5.0/README.md +42 -36
  3. memorymaster-3.5.0/artifacts/bm25-per-field-eval-harness.py +309 -0
  4. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/__init__.py +1 -1
  5. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/cli.py +9 -1
  6. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/cli_handlers_curation.py +70 -0
  7. memorymaster-3.5.0/memorymaster/config_templates/hooks/memorymaster-auto-ingest.py +222 -0
  8. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/hooks/memorymaster-classify.py +98 -10
  9. memorymaster-3.5.0/memorymaster/config_templates/hooks/memorymaster-dream-sync.py +27 -0
  10. memorymaster-3.5.0/memorymaster/config_templates/hooks/memorymaster-observe.py +39 -0
  11. memorymaster-3.5.0/memorymaster/config_templates/hooks/memorymaster-precompact.py +76 -0
  12. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/hooks/memorymaster-recall.py +8 -3
  13. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/hooks/memorymaster-session-start.py +23 -2
  14. memorymaster-3.5.0/memorymaster/config_templates/hooks/memorymaster-steward-cycle.py +49 -0
  15. memorymaster-3.5.0/memorymaster/context_hook.py +1561 -0
  16. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/embeddings.py +23 -5
  17. memorymaster-3.5.0/memorymaster/entity_extractor.py +690 -0
  18. memorymaster-3.5.0/memorymaster/entity_registry.py +456 -0
  19. memorymaster-3.5.0/memorymaster/graph_store.py +570 -0
  20. memorymaster-3.5.0/memorymaster/hook_log.py +65 -0
  21. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/validator.py +41 -1
  22. memorymaster-3.5.0/memorymaster/key_rotator.py +176 -0
  23. memorymaster-3.5.0/memorymaster/llm_provider.py +469 -0
  24. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/mcp_server.py +45 -2
  25. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/policy.py +23 -0
  26. memorymaster-3.5.0/memorymaster/qdrant_recall_fallback.py +305 -0
  27. memorymaster-3.5.0/memorymaster/query_expansion.py +184 -0
  28. memorymaster-3.5.0/memorymaster/recall_fusion.py +79 -0
  29. memorymaster-3.5.0/memorymaster/recall_tokenizer.py +305 -0
  30. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/security.py +139 -7
  31. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/service.py +35 -7
  32. memorymaster-3.5.0/memorymaster/steward_classifier.py +149 -0
  33. memorymaster-3.5.0/memorymaster/steward_features.py +345 -0
  34. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_linter.py +81 -4
  35. memorymaster-3.5.0/memorymaster/verbatim_recall.py +296 -0
  36. memorymaster-3.5.0/memorymaster/wiki_freshness.py +228 -0
  37. memorymaster-3.5.0/memorymaster/wiki_similarity.py +621 -0
  38. memorymaster-3.4.0/README.md → memorymaster-3.5.0/memorymaster.egg-info/PKG-INFO +86 -0
  39. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster.egg-info/SOURCES.txt +70 -1
  40. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster.egg-info/requires.txt +11 -0
  41. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster.egg-info/top_level.txt +1 -0
  42. {memorymaster-3.4.0 → memorymaster-3.5.0}/pyproject.toml +13 -1
  43. memorymaster-3.5.0/scripts/agg_recall_latency.py +184 -0
  44. memorymaster-3.5.0/scripts/backfill_entity_extraction.py +327 -0
  45. memorymaster-3.5.0/scripts/backfill_graph_store.py +264 -0
  46. memorymaster-3.5.0/scripts/backfill_stop_hook_citations.py +137 -0
  47. memorymaster-3.5.0/scripts/backtest_steward_classifier.py +757 -0
  48. memorymaster-3.5.0/scripts/build_steward_training_set.py +119 -0
  49. memorymaster-3.5.0/scripts/check_hook_template_drift.py +143 -0
  50. memorymaster-3.5.0/scripts/eval_bm25_sweep.py +432 -0
  51. memorymaster-3.5.0/scripts/eval_classify_f1.py +226 -0
  52. memorymaster-3.5.0/scripts/eval_recall_precision_at_5.py +468 -0
  53. memorymaster-3.5.0/scripts/eval_recall_quality.py +132 -0
  54. memorymaster-3.5.0/scripts/eval_steward_pareto.py +273 -0
  55. memorymaster-3.5.0/scripts/eval_verbatim_recall.py +216 -0
  56. memorymaster-3.5.0/scripts/expand_recall_eval.py +404 -0
  57. memorymaster-3.5.0/scripts/index_claims_to_qdrant.py +315 -0
  58. memorymaster-3.5.0/scripts/merge_scope_variants.py +216 -0
  59. memorymaster-3.5.0/scripts/run_longmemeval.py +997 -0
  60. memorymaster-3.5.0/scripts/sync_hook_templates.py +117 -0
  61. memorymaster-3.5.0/scripts/train_steward_classifier.py +457 -0
  62. memorymaster-3.5.0/tests/integration/test_extract_llm_ollama_live.py +49 -0
  63. memorymaster-3.5.0/tests/test_auto_ingest_hook_citations.py +147 -0
  64. memorymaster-3.5.0/tests/test_auto_ingest_hook_schema.py +145 -0
  65. memorymaster-3.5.0/tests/test_bm25_per_field.py +222 -0
  66. memorymaster-3.5.0/tests/test_classify_hook_f1.py +85 -0
  67. memorymaster-3.5.0/tests/test_classify_hook_latency.py +91 -0
  68. memorymaster-3.5.0/tests/test_entity_extractor.py +150 -0
  69. memorymaster-3.5.0/tests/test_entity_extractor_llm.py +296 -0
  70. memorymaster-3.5.0/tests/test_entity_new_kinds.py +241 -0
  71. memorymaster-3.5.0/tests/test_entity_registry.py +406 -0
  72. memorymaster-3.5.0/tests/test_eval_harness.py +291 -0
  73. memorymaster-3.5.0/tests/test_extract_llm_ollama.py +140 -0
  74. memorymaster-3.5.0/tests/test_graph_distance.py +233 -0
  75. memorymaster-3.5.0/tests/test_graph_store.py +321 -0
  76. memorymaster-3.5.0/tests/test_key_rotator.py +159 -0
  77. memorymaster-3.5.0/tests/test_llm_fallback.py +219 -0
  78. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_mcp_helpers.py +93 -0
  79. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_operator.py +6 -0
  80. memorymaster-3.5.0/tests/test_policy_mode_env.py +91 -0
  81. memorymaster-3.5.0/tests/test_query_expansion.py +264 -0
  82. memorymaster-3.5.0/tests/test_recall_entity_fanout.py +459 -0
  83. memorymaster-3.5.0/tests/test_recall_fusion.py +94 -0
  84. memorymaster-3.5.0/tests/test_recall_latency.py +275 -0
  85. memorymaster-3.5.0/tests/test_recall_precision_at_5.py +261 -0
  86. memorymaster-3.5.0/tests/test_recall_tokenizer.py +234 -0
  87. memorymaster-3.5.0/tests/test_recall_vector_fallback.py +306 -0
  88. memorymaster-3.5.0/tests/test_rrf_auto_gate.py +223 -0
  89. memorymaster-3.5.0/tests/test_scope_boost.py +298 -0
  90. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_security_patterns.py +4 -1
  91. memorymaster-3.5.0/tests/test_sensitivity_filter_adversarial.py +93 -0
  92. memorymaster-3.5.0/tests/test_sensitivity_filter_adversarial_v2.py +153 -0
  93. memorymaster-3.5.0/tests/test_steward_classifier.py +261 -0
  94. memorymaster-3.5.0/tests/test_steward_features.py +174 -0
  95. memorymaster-3.5.0/tests/test_steward_features_v3.py +254 -0
  96. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_vector_search.py +3 -1
  97. memorymaster-3.5.0/tests/test_verbatim_recall.py +321 -0
  98. memorymaster-3.5.0/tests/test_wiki_freshness.py +281 -0
  99. memorymaster-3.5.0/tests/test_wiki_similarity_multiscope.py +270 -0
  100. memorymaster-3.4.0/memorymaster/config_templates/hooks/memorymaster-auto-ingest.py +0 -126
  101. memorymaster-3.4.0/memorymaster/config_templates/hooks/memorymaster-precompact.py +0 -35
  102. memorymaster-3.4.0/memorymaster/config_templates/hooks/memorymaster-steward-cycle.py +0 -28
  103. memorymaster-3.4.0/memorymaster/context_hook.py +0 -230
  104. memorymaster-3.4.0/memorymaster/entity_registry.py +0 -255
  105. memorymaster-3.4.0/memorymaster/llm_provider.py +0 -229
  106. {memorymaster-3.4.0 → memorymaster-3.5.0}/LICENSE +0 -0
  107. {memorymaster-3.4.0 → memorymaster-3.5.0}/benchmarks/longmemeval_runner.py +0 -0
  108. {memorymaster-3.4.0 → memorymaster-3.5.0}/benchmarks/longmemeval_vector_runner.py +0 -0
  109. {memorymaster-3.4.0 → memorymaster-3.5.0}/benchmarks/perf_smoke.py +0 -0
  110. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/__main__.py +0 -0
  111. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/_storage_lifecycle.py +0 -0
  112. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/_storage_read.py +0 -0
  113. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/_storage_schema.py +0 -0
  114. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/_storage_shared.py +0 -0
  115. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/_storage_write_claims.py +0 -0
  116. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/access_control.py +0 -0
  117. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/auto_extractor.py +0 -0
  118. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/auto_resolver.py +0 -0
  119. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/claim_verifier.py +0 -0
  120. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/cli_handlers_basic.py +0 -0
  121. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/cli_helpers.py +0 -0
  122. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config.py +0 -0
  123. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/claude-md-append.md +0 -0
  124. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/codex-agents-md-append.md +0 -0
  125. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/hooks/memorymaster-validate-wiki.py +0 -0
  126. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/conflict_resolver.py +0 -0
  127. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/context_optimizer.py +0 -0
  128. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/daily_notes.py +0 -0
  129. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/dashboard.py +0 -0
  130. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/db_merge.py +0 -0
  131. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/dream_bridge.py +0 -0
  132. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/entity_graph.py +0 -0
  133. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/feedback.py +0 -0
  134. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/__init__.py +0 -0
  135. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/compact_summaries.py +0 -0
  136. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/compactor.py +0 -0
  137. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/decay.py +0 -0
  138. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/dedup.py +0 -0
  139. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/deterministic.py +0 -0
  140. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/extractor.py +0 -0
  141. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/staleness.py +0 -0
  142. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/lifecycle.py +0 -0
  143. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/llm_steward.py +0 -0
  144. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/metrics_exporter.py +0 -0
  145. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/models.py +0 -0
  146. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/operator.py +0 -0
  147. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/operator_queue.py +0 -0
  148. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/plugins.py +0 -0
  149. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/postgres_store.py +0 -0
  150. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/qdrant_backend.py +0 -0
  151. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/qmd_bridge.py +0 -0
  152. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/query_classifier.py +0 -0
  153. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/retrieval.py +0 -0
  154. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/retry.py +0 -0
  155. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/review.py +0 -0
  156. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/rl_trainer.py +0 -0
  157. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/scheduler.py +0 -0
  158. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/schema.py +0 -0
  159. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/schema.sql +0 -0
  160. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/schema_postgres.sql +0 -0
  161. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/session_tracker.py +0 -0
  162. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/setup_hooks.py +0 -0
  163. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/skill_evolver.py +0 -0
  164. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/snapshot.py +0 -0
  165. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/steward.py +0 -0
  166. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/storage.py +0 -0
  167. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/store_factory.py +0 -0
  168. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/transcript_miner.py +0 -0
  169. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/turn_schema.py +0 -0
  170. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_bases.py +0 -0
  171. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_curator.py +0 -0
  172. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_exporter.py +0 -0
  173. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_log.py +0 -0
  174. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_query_capture.py +0 -0
  175. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_synthesis.py +0 -0
  176. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/verbatim_store.py +0 -0
  177. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/webhook.py +0 -0
  178. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/wiki_engine.py +0 -0
  179. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster.egg-info/dependency_links.txt +0 -0
  180. {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster.egg-info/entry_points.txt +0 -0
  181. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/alert_operator_metrics.py +0 -0
  182. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/autoresearch_daemon.py +0 -0
  183. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/claude_to_turns.py +0 -0
  184. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/codex_live_to_turns.py +0 -0
  185. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/compaction_edge_cases.py +0 -0
  186. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/compaction_trace_report.py +0 -0
  187. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/compaction_trace_validate.py +0 -0
  188. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/confusion_matrix_eval.py +0 -0
  189. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/conversation_importer.py +0 -0
  190. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/conversation_to_turns.py +0 -0
  191. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/e2e_operator.py +0 -0
  192. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/email_live_to_turns.py +0 -0
  193. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/eval_memorymaster.py +0 -0
  194. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/generate_drill_signoff.py +0 -0
  195. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/git_to_turns.py +0 -0
  196. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/github_live_to_turns.py +0 -0
  197. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/gitnexus_to_claims.py +0 -0
  198. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/ingest_planning_docs.py +0 -0
  199. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/jira_live_to_turns.py +0 -0
  200. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/llm_benchmark.py +0 -0
  201. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/messages_to_turns.py +0 -0
  202. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/operator_metrics.py +0 -0
  203. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/recurring_incident_drill.py +0 -0
  204. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/release_readiness.py +0 -0
  205. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/run_codex_autologger.py +0 -0
  206. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/run_incident_drill.py +0 -0
  207. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/scheduled_ingest.py +0 -0
  208. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/setup-hooks.py +0 -0
  209. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/slack_live_to_turns.py +0 -0
  210. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/tickets_to_turns.py +0 -0
  211. {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/webhook_to_turns.py +0 -0
  212. {memorymaster-3.4.0 → memorymaster-3.5.0}/setup.cfg +0 -0
  213. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/conftest.py +0 -0
  214. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_access_control.py +0 -0
  215. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_auto_extractor.py +0 -0
  216. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_auto_resolver.py +0 -0
  217. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_auto_validate.py +0 -0
  218. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_claim_links.py +0 -0
  219. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_claude_to_turns.py +0 -0
  220. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_cli_json_flag.py +0 -0
  221. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_cli_ready.py +0 -0
  222. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_cli_review_queue.py +0 -0
  223. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_cli_subcommands.py +0 -0
  224. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_compact_summaries.py +0 -0
  225. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_compaction_trace.py +0 -0
  226. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_config.py +0 -0
  227. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_conflict_resolver.py +0 -0
  228. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_confusion_matrix_eval.py +0 -0
  229. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_connection_retry.py +0 -0
  230. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_connectors.py +0 -0
  231. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_context_hook.py +0 -0
  232. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_context_optimizer.py +0 -0
  233. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_conversation_to_turns.py +0 -0
  234. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_dashboard.py +0 -0
  235. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_dedup.py +0 -0
  236. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_deterministic_predicates.py +0 -0
  237. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_embeddings_coverage.py +0 -0
  238. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_entity_graph.py +0 -0
  239. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_events_schema.py +0 -0
  240. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_feedback.py +0 -0
  241. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_fts5_search.py +0 -0
  242. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_handler_regressions.py +0 -0
  243. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_human_id.py +0 -0
  244. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_incident_drill_runner.py +0 -0
  245. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_integration_workflows.py +0 -0
  246. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_lifecycle.py +0 -0
  247. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_llm_steward_coverage.py +0 -0
  248. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_llm_steward_key_rotation.py +0 -0
  249. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_metrics_exporter.py +0 -0
  250. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_obsidian_mind_patterns.py +0 -0
  251. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_operator_queue.py +0 -0
  252. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_perf_smoke_config.py +0 -0
  253. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_plugins.py +0 -0
  254. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_policy_coverage.py +0 -0
  255. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_postgres_parity.py +0 -0
  256. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_qdrant_backend.py +0 -0
  257. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_qmd_bridge.py +0 -0
  258. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_query_classifier.py +0 -0
  259. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_reliability_hardening.py +0 -0
  260. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_review.py +0 -0
  261. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_rl_trainer.py +0 -0
  262. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_scheduler.py +0 -0
  263. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_schema.py +0 -0
  264. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_security_access.py +0 -0
  265. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_service_coverage.py +0 -0
  266. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_session_tracker.py +0 -0
  267. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_snapshot.py +0 -0
  268. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_sqlite_core.py +0 -0
  269. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_staleness.py +0 -0
  270. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_stealth_mode.py +0 -0
  271. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_steward.py +0 -0
  272. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_steward_resolution_parity.py +0 -0
  273. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_store_factory.py +0 -0
  274. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_tenant_isolation.py +0 -0
  275. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_turn_schema.py +0 -0
  276. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_vault_exporter.py +0 -0
  277. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_webhook.py +0 -0
  278. {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_wiki_binding.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memorymaster
3
- Version: 3.4.0
3
+ Version: 3.5.0
4
4
  Summary: Production-grade memory reliability system for AI coding agents. Lifecycle-managed claims with citations, conflict detection, steward governance, and MCP integration.
5
5
  Author: wolverin0
6
6
  License: MIT
@@ -27,11 +27,19 @@ Provides-Extra: gemini
27
27
  Requires-Dist: google-genai>=1.0; extra == "gemini"
28
28
  Provides-Extra: qdrant
29
29
  Requires-Dist: httpx>=0.27; extra == "qdrant"
30
+ Provides-Extra: vector
31
+ Requires-Dist: sentence-transformers>=3.0; extra == "vector"
32
+ Requires-Dist: qdrant-client>=1.9; extra == "vector"
33
+ Provides-Extra: graph
34
+ Requires-Dist: kuzu>=0.4; extra == "graph"
30
35
  Provides-Extra: dev
31
36
  Requires-Dist: pytest>=8.2; extra == "dev"
32
37
  Requires-Dist: pytest-cov>=6.0; extra == "dev"
33
38
  Provides-Extra: mcp
34
39
  Requires-Dist: mcp>=1.2; extra == "mcp"
40
+ Provides-Extra: ml
41
+ Requires-Dist: scikit-learn>=1.3; extra == "ml"
42
+ Requires-Dist: joblib>=1.3; extra == "ml"
35
43
  Dynamic: license-file
36
44
 
37
45
  # MemoryMaster
@@ -143,6 +151,48 @@ MemoryMaster gives AI coding agents **persistent, verifiable memory** with a ful
143
151
  - **Obsidian 1.6+** with the **Bases** core plugin — only if you want to browse the wiki visually
144
152
  - **Docker** — only if you want Qdrant for hybrid vector search (SQLite FTS5 is the default and works out of the box)
145
153
 
154
+ ## Setup
155
+
156
+ A minimal path for new users. Every env var mentioned here is documented in [`.env.example`](.env.example) — copy that file and uncomment the lines you need.
157
+
158
+ ### 1. Minimum viable setup
159
+
160
+ ```bash
161
+ pip install "memorymaster[mcp]"
162
+ python -m memorymaster --db memorymaster.db init-db
163
+ cp .env.example .env
164
+ # Then set ONE of:
165
+ # GEMINI_API_KEY=... (free from https://aistudio.google.com)
166
+ # OPENAI_API_KEY=...
167
+ # ANTHROPIC_API_KEY=...
168
+ # Or run Ollama locally (no key needed) — see below.
169
+ ```
170
+
171
+ That's enough to use the CLI, the MCP server, and the auto-ingest Stop hook.
172
+
173
+ ### 2. Pick your LLM provider
174
+
175
+ | Provider | Env vars | Model (default) | Cost |
176
+ |----------|----------|-----------------|------|
177
+ | Google Gemini (default) | `MEMORYMASTER_LLM_PROVIDER=google` + `GEMINI_API_KEY=...` | `gemini-3.1-flash-lite-preview` | ~free |
178
+ | OpenAI | `MEMORYMASTER_LLM_PROVIDER=openai` + `OPENAI_API_KEY=...` | `gpt-4o-mini` | ~$0.001/call |
179
+ | Anthropic | `MEMORYMASTER_LLM_PROVIDER=anthropic` + `ANTHROPIC_API_KEY=...` | `claude-haiku-4-5-20251001` | ~$0.001/call |
180
+ | Ollama (local) | `MEMORYMASTER_LLM_PROVIDER=ollama` + `OLLAMA_URL=http://localhost:11434` | `llama3.2:3b` | free |
181
+
182
+ For zero-cost offline use, install [Ollama](https://ollama.com), `ollama pull llama3.2:3b`, and set `MEMORYMASTER_LLM_PROVIDER=ollama`. No API key required.
183
+
184
+ ### 3. Enable the v3 classifier + cadence policy (optional)
185
+
186
+ The v3 statistical classifier + cadence policy are off by default so fresh installs behave like legacy steward. To opt in, set `MEMORYMASTER_STEWARD_CLASSIFIER_ENABLED=1` (or point `MEMORYMASTER_STEWARD_CLASSIFIER_PATH` at a trained `.pkl`) and `MEMORYMASTER_POLICY_MODE=cadence`. Full details, the training workflow, and the back-test harness live in [`docs/enabling-v2-systems.md`](docs/enabling-v2-systems.md).
187
+
188
+ ### 4. First run
189
+
190
+ ```bash
191
+ python -m memorymaster --db memorymaster.db run-cycle
192
+ ```
193
+
194
+ Expect output summarising `ingest / validate / decay / supersession / archive` counts. A fresh DB prints all zeroes — that's normal. After one or two sessions of the auto-ingest hook feeding candidates, the next cycle starts promoting `candidate` → `confirmed`.
195
+
146
196
  ## Install via Agent (One-Prompt) ⚡
147
197
 
148
198
  **The fastest way to install MemoryMaster end-to-end is to let an AI agent do it.** Open Claude Code, Codex, Cursor, or any agent with shell access in the project directory you want to instrument, and paste the prompt below. The agent handles pip install, MCP wiring, all 7 hooks, steward cron, LLM provider selection, and verification — you only approve steps and provide an API key when asked.
@@ -1,39 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: memorymaster
3
- Version: 3.4.0
4
- Summary: Production-grade memory reliability system for AI coding agents. Lifecycle-managed claims with citations, conflict detection, steward governance, and MCP integration.
5
- Author: wolverin0
6
- License: MIT
7
- Keywords: memory,ai-agents,claims,lifecycle,mcp,sqlite,postgres,coding-agents
8
- Classifier: Development Status :: 5 - Production/Stable
9
- Classifier: Intended Audience :: Developers
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Topic :: Software Development :: Libraries
16
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
- Requires-Python: >=3.10
18
- Description-Content-Type: text/markdown
19
- License-File: LICENSE
20
- Provides-Extra: postgres
21
- Requires-Dist: psycopg[binary]>=3.2; extra == "postgres"
22
- Provides-Extra: security
23
- Requires-Dist: cryptography>=42; extra == "security"
24
- Provides-Extra: embeddings
25
- Requires-Dist: sentence-transformers>=3.0; extra == "embeddings"
26
- Provides-Extra: gemini
27
- Requires-Dist: google-genai>=1.0; extra == "gemini"
28
- Provides-Extra: qdrant
29
- Requires-Dist: httpx>=0.27; extra == "qdrant"
30
- Provides-Extra: dev
31
- Requires-Dist: pytest>=8.2; extra == "dev"
32
- Requires-Dist: pytest-cov>=6.0; extra == "dev"
33
- Provides-Extra: mcp
34
- Requires-Dist: mcp>=1.2; extra == "mcp"
35
- Dynamic: license-file
36
-
37
1
  # MemoryMaster
38
2
 
39
3
  **Production-grade memory reliability system for AI coding agents.**
@@ -143,6 +107,48 @@ MemoryMaster gives AI coding agents **persistent, verifiable memory** with a ful
143
107
  - **Obsidian 1.6+** with the **Bases** core plugin — only if you want to browse the wiki visually
144
108
  - **Docker** — only if you want Qdrant for hybrid vector search (SQLite FTS5 is the default and works out of the box)
145
109
 
110
+ ## Setup
111
+
112
+ A minimal path for new users. Every env var mentioned here is documented in [`.env.example`](.env.example) — copy that file and uncomment the lines you need.
113
+
114
+ ### 1. Minimum viable setup
115
+
116
+ ```bash
117
+ pip install "memorymaster[mcp]"
118
+ python -m memorymaster --db memorymaster.db init-db
119
+ cp .env.example .env
120
+ # Then set ONE of:
121
+ # GEMINI_API_KEY=... (free from https://aistudio.google.com)
122
+ # OPENAI_API_KEY=...
123
+ # ANTHROPIC_API_KEY=...
124
+ # Or run Ollama locally (no key needed) — see below.
125
+ ```
126
+
127
+ That's enough to use the CLI, the MCP server, and the auto-ingest Stop hook.
128
+
129
+ ### 2. Pick your LLM provider
130
+
131
+ | Provider | Env vars | Model (default) | Cost |
132
+ |----------|----------|-----------------|------|
133
+ | Google Gemini (default) | `MEMORYMASTER_LLM_PROVIDER=google` + `GEMINI_API_KEY=...` | `gemini-3.1-flash-lite-preview` | ~free |
134
+ | OpenAI | `MEMORYMASTER_LLM_PROVIDER=openai` + `OPENAI_API_KEY=...` | `gpt-4o-mini` | ~$0.001/call |
135
+ | Anthropic | `MEMORYMASTER_LLM_PROVIDER=anthropic` + `ANTHROPIC_API_KEY=...` | `claude-haiku-4-5-20251001` | ~$0.001/call |
136
+ | Ollama (local) | `MEMORYMASTER_LLM_PROVIDER=ollama` + `OLLAMA_URL=http://localhost:11434` | `llama3.2:3b` | free |
137
+
138
+ For zero-cost offline use, install [Ollama](https://ollama.com), `ollama pull llama3.2:3b`, and set `MEMORYMASTER_LLM_PROVIDER=ollama`. No API key required.
139
+
140
+ ### 3. Enable the v3 classifier + cadence policy (optional)
141
+
142
+ The v3 statistical classifier + cadence policy are off by default so fresh installs behave like legacy steward. To opt in, set `MEMORYMASTER_STEWARD_CLASSIFIER_ENABLED=1` (or point `MEMORYMASTER_STEWARD_CLASSIFIER_PATH` at a trained `.pkl`) and `MEMORYMASTER_POLICY_MODE=cadence`. Full details, the training workflow, and the back-test harness live in [`docs/enabling-v2-systems.md`](docs/enabling-v2-systems.md).
143
+
144
+ ### 4. First run
145
+
146
+ ```bash
147
+ python -m memorymaster --db memorymaster.db run-cycle
148
+ ```
149
+
150
+ Expect output summarising `ingest / validate / decay / supersession / archive` counts. A fresh DB prints all zeroes — that's normal. After one or two sessions of the auto-ingest hook feeding candidates, the next cycle starts promoting `candidate` → `confirmed`.
151
+
146
152
  ## Install via Agent (One-Prompt) ⚡
147
153
 
148
154
  **The fastest way to install MemoryMaster end-to-end is to let an AI agent do it.** Open Claude Code, Codex, Cursor, or any agent with shell access in the project directory you want to instrument, and paste the prompt below. The agent handles pip install, MCP wiring, all 7 hooks, steward cron, LLM provider selection, and verification — you only approve steps and provide an API key when asked.
@@ -0,0 +1,309 @@
1
+ """One-off eval harness for roadmap 1.4 BM25 per-field weighting.
2
+
3
+ The shipped ``scripts/eval_recall_precision_at_5.py`` has its own inline
4
+ ``_score`` implementation that reads ``row["lexical_score"]`` (the FTS5 rank
5
+ from retrieval) — it does NOT exercise the BM25 rescorer that lives inside
6
+ ``context_hook.recall``. That's fine for the per-weight grid search it was
7
+ built for, but it makes it impossible to measure a BM25-internal change
8
+ (like per-field weighting) through that script.
9
+
10
+ This harness reuses the same candidate-collection path, then applies the
11
+ EXACT per-field BM25 rescorer from ``memorymaster.context_hook`` so each
12
+ config is a true end-to-end measurement. It writes ``row["lexical_score"]``
13
+ back with the per-field score before delegating to the eval's ``_evaluate``
14
+ helper so the rest of the pipeline (ranker weights, labels, p@5, MAP@5) is
15
+ identical across configs.
16
+
17
+ Run::
18
+
19
+ python artifacts/bm25-per-field-eval-harness.py [--prompts ...] [--db ...]
20
+
21
+ Does NOT modify the DB. Read-only, like the parent eval.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import math
27
+ import os
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ # Add repo root and scripts/ to path.
32
+ HERE = Path(__file__).resolve().parent
33
+ REPO = HERE.parent
34
+ sys.path.insert(0, str(REPO))
35
+ sys.path.insert(0, str(REPO / "scripts"))
36
+
37
+ # Import from the eval script (treat it as a module).
38
+ import importlib.util
39
+ spec = importlib.util.spec_from_file_location(
40
+ "eval_module", REPO / "scripts" / "eval_recall_precision_at_5.py"
41
+ )
42
+ assert spec is not None and spec.loader is not None
43
+ eval_module = importlib.util.module_from_spec(spec)
44
+ sys.modules["eval_module"] = eval_module # dataclass needs cls.__module__ resolvable
45
+ spec.loader.exec_module(eval_module)
46
+
47
+ from memorymaster.context_hook import (
48
+ _BM25_K1_DEFAULT,
49
+ _BM25_B_DEFAULT,
50
+ _BM25_W_SUBJECT_DEFAULT,
51
+ _BM25_W_TEXT_DEFAULT,
52
+ )
53
+ from memorymaster.recall_tokenizer import _candidate_tokens
54
+ from memorymaster.service import MemoryService
55
+
56
+
57
+ def _tokens(raw: str) -> list[str]:
58
+ if not isinstance(raw, str):
59
+ return []
60
+ return [t for t in _candidate_tokens(raw) if len(t) >= 3]
61
+
62
+
63
+ def _apply_per_field_bm25(
64
+ prompt: str,
65
+ rows: list[dict],
66
+ w_subject: float,
67
+ w_text: float,
68
+ k1: float = _BM25_K1_DEFAULT,
69
+ b: float = _BM25_B_DEFAULT,
70
+ ) -> None:
71
+ """Overwrite ``row["lexical_score"]`` with per-field BM25 for each row.
72
+
73
+ This replicates the logic in context_hook.recall() so the eval
74
+ harness measures the same scoring code as production.
75
+ """
76
+ # Per-field tokenisation + df.
77
+ subj_tok: dict[int, list[str]] = {}
78
+ text_tok: dict[int, list[str]] = {}
79
+ df_s: dict[str, int] = {}
80
+ df_t: dict[str, int] = {}
81
+ for r in rows:
82
+ c = r.get("claim")
83
+ cid = getattr(c, "id", None)
84
+ if cid is None or cid in subj_tok:
85
+ continue
86
+ st = _tokens(getattr(c, "subject", "") or "")
87
+ tt = _tokens(getattr(c, "text", "") or "")
88
+ subj_tok[cid] = st
89
+ text_tok[cid] = tt
90
+ for t in set(st):
91
+ df_s[t] = df_s.get(t, 0) + 1
92
+ for t in set(tt):
93
+ df_t[t] = df_t.get(t, 0) + 1
94
+
95
+ n_docs = len(subj_tok)
96
+ non_empty_s = [v for v in subj_tok.values() if v]
97
+ non_empty_t = [v for v in text_tok.values() if v]
98
+ avg_s = sum(len(v) for v in non_empty_s) / len(non_empty_s) if non_empty_s else 0.0
99
+ avg_t = sum(len(v) for v in non_empty_t) / len(non_empty_t) if non_empty_t else 0.0
100
+
101
+ q_tokens = [t for t in _candidate_tokens(prompt) if len(t) >= 3]
102
+
103
+ def field_score(toks: list[str], df: dict[str, int], avg: float) -> float:
104
+ if not toks or avg <= 0.0:
105
+ return 0.0
106
+ tf: dict[str, int] = {}
107
+ for t in toks:
108
+ tf[t] = tf.get(t, 0) + 1
109
+ dl = len(toks)
110
+ s = 0.0
111
+ for qt in q_tokens:
112
+ f = tf.get(qt, 0)
113
+ if f == 0:
114
+ continue
115
+ n_q = df.get(qt, 0)
116
+ idf = math.log(((n_docs - n_q + 0.5) / (n_q + 0.5)) + 1.0)
117
+ norm = 1.0 - b + b * (dl / avg)
118
+ s += idf * ((f * (k1 + 1.0)) / (f + k1 * norm))
119
+ return s
120
+
121
+ # Write per-field combined score into row["lexical_score"] so the
122
+ # downstream eval _score() sees it as the lexical signal. This is the
123
+ # one mutation; everything else is untouched.
124
+ scores: dict[int, float] = {}
125
+ if n_docs > 0 and q_tokens:
126
+ for cid in subj_tok:
127
+ ss = field_score(subj_tok[cid], df_s, avg_s)
128
+ ts = field_score(text_tok[cid], df_t, avg_t)
129
+ scores[cid] = w_subject * ss + w_text * ts
130
+
131
+ for r in rows:
132
+ c = r.get("claim")
133
+ cid = getattr(c, "id", None)
134
+ if cid is not None and cid in scores:
135
+ r["lexical_score"] = scores[cid]
136
+ else:
137
+ r["lexical_score"] = 0.0
138
+
139
+
140
+ def _apply_concat_bm25(
141
+ prompt: str,
142
+ rows: list[dict],
143
+ k1: float = _BM25_K1_DEFAULT,
144
+ b: float = _BM25_B_DEFAULT,
145
+ ) -> None:
146
+ """Replicate the pre-change concatenated BM25 scorer for an honest baseline.
147
+
148
+ Mirrors the block at context_hook.py commit 3a34b2d:529-582.
149
+ """
150
+ tok: dict[int, list[str]] = {}
151
+ df: dict[str, int] = {}
152
+ for r in rows:
153
+ c = r.get("claim")
154
+ cid = getattr(c, "id", None)
155
+ if cid is None or cid in tok:
156
+ continue
157
+ subject = getattr(c, "subject", "") or ""
158
+ text = getattr(c, "text", "") or ""
159
+ if not isinstance(subject, str):
160
+ subject = ""
161
+ if not isinstance(text, str):
162
+ text = ""
163
+ joined = f"{subject} {text}"
164
+ toks = [t for t in _candidate_tokens(joined) if len(t) >= 3]
165
+ tok[cid] = toks
166
+ for t in set(toks):
167
+ df[t] = df.get(t, 0) + 1
168
+ n_docs = len(tok)
169
+ avg = sum(len(v) for v in tok.values()) / n_docs if n_docs else 0.0
170
+ q_tokens = [t for t in _candidate_tokens(prompt) if len(t) >= 3]
171
+ scores: dict[int, float] = {}
172
+ if n_docs > 0 and avg > 0 and q_tokens:
173
+ for cid, toks in tok.items():
174
+ if not toks:
175
+ continue
176
+ tf: dict[str, int] = {}
177
+ for t in toks:
178
+ tf[t] = tf.get(t, 0) + 1
179
+ dl = len(toks)
180
+ s = 0.0
181
+ for qt in q_tokens:
182
+ f = tf.get(qt, 0)
183
+ if f == 0:
184
+ continue
185
+ n_q = df.get(qt, 0)
186
+ idf = math.log(((n_docs - n_q + 0.5) / (n_q + 0.5)) + 1.0)
187
+ norm = 1.0 - b + b * (dl / avg)
188
+ s += idf * ((f * (k1 + 1.0)) / (f + k1 * norm))
189
+ scores[cid] = s
190
+ for r in rows:
191
+ c = r.get("claim")
192
+ cid = getattr(c, "id", None)
193
+ if cid is not None and cid in scores:
194
+ r["lexical_score"] = scores[cid]
195
+ else:
196
+ r["lexical_score"] = 0.0
197
+
198
+
199
+ def run_config(
200
+ collected: list[tuple[str, list[dict], object]],
201
+ label: str,
202
+ rescorer,
203
+ *rescorer_args,
204
+ min_overlap: int = 2,
205
+ ) -> tuple[float, float, int]:
206
+ # Fresh copies per config (rescorer mutates lexical_score).
207
+ import copy
208
+ rescored = []
209
+ for prompt, rows, svc_tokens in collected:
210
+ fresh = [dict(r) for r in rows]
211
+ rescorer(prompt, fresh, *rescorer_args)
212
+ rescored.append((prompt, fresh, svc_tokens))
213
+ p5, m5, hits = eval_module._evaluate(
214
+ rescored, eval_module.W0, min_overlap=min_overlap
215
+ )
216
+ return p5, m5, hits
217
+
218
+
219
+ def main() -> int:
220
+ ap = argparse.ArgumentParser(description=__doc__)
221
+ ap.add_argument(
222
+ "--prompts",
223
+ default=str(REPO.parent.parent.parent / "artifacts" / "real-prompts.jsonl"),
224
+ )
225
+ ap.add_argument(
226
+ "--db",
227
+ default=str(REPO.parent.parent.parent / "memorymaster.db"),
228
+ )
229
+ ap.add_argument("--top-k", type=int, default=20)
230
+ ap.add_argument("--min-overlap", type=int, default=2)
231
+ args = ap.parse_args()
232
+
233
+ prompts_path = Path(args.prompts)
234
+ db_path = Path(args.db)
235
+ if not prompts_path.exists() or not db_path.exists():
236
+ print(f"ERROR missing: prompts={prompts_path} db={db_path}")
237
+ return 2
238
+
239
+ prompts = eval_module._load_prompts(prompts_path)
240
+ svc = MemoryService(db_target=str(db_path), workspace_root=REPO)
241
+ svc._record_accesses = lambda *a, **k: None # type: ignore[assignment]
242
+ if hasattr(svc, "store") and hasattr(svc.store, "record_accesses_batch"):
243
+ svc.store.record_accesses_batch = lambda *a, **k: None # type: ignore[assignment]
244
+
245
+ print(f"Loaded {len(prompts)} prompts, collecting top-{args.top_k} candidates...")
246
+ collected = eval_module._collect_candidates(
247
+ prompts, svc, str(db_path), top_k=args.top_k,
248
+ include_entity_fanout=True, include_vector_fallback=False,
249
+ )
250
+ cand_counts = [len(r) for _, r, _ in collected]
251
+ print(f" mean candidates/prompt: {sum(cand_counts) / max(1, len(cand_counts)):.1f} "
252
+ f"(min={min(cand_counts, default=0)}, max={max(cand_counts, default=0)})")
253
+
254
+ configs = [
255
+ ("A concat baseline ", _apply_concat_bm25, ()),
256
+ ("B per-field W_S=2.0 W_T=1.0 ", _apply_per_field_bm25, (2.0, 1.0)),
257
+ ("C per-field W_S=3.0 W_T=1.0 ", _apply_per_field_bm25, (3.0, 1.0)),
258
+ ("D per-field W_S=1.5 W_T=1.0 ", _apply_per_field_bm25, (1.5, 1.0)),
259
+ ("E per-field W_S=5.0 W_T=1.0 ", _apply_per_field_bm25, (5.0, 1.0)),
260
+ ("F per-field W_S=10.0 W_T=0.0 ", _apply_per_field_bm25, (10.0, 0.0)),
261
+ ("G per-field W_S=0.0 W_T=10.0 ", _apply_per_field_bm25, (0.0, 10.0)),
262
+ ("H per-field W_S=1.0 W_T=1.0 ", _apply_per_field_bm25, (1.0, 1.0)),
263
+ ]
264
+
265
+ print("\n{:<34} {:>10} {:>10} {:>12}".format(
266
+ "config", "p@5", "MAP@5", "non_empty"))
267
+ print("-" * 70)
268
+ results = []
269
+ for label, fn, args_tuple in configs:
270
+ p5, m5, hits = run_config(collected, label, fn, *args_tuple,
271
+ min_overlap=args.min_overlap)
272
+ print(f"{label} {p5:>8.3f} {m5:>8.3f} {hits:>3}/{len(prompts)}")
273
+ results.append((label, p5, m5, hits))
274
+
275
+ # Sample drill-down: find a prompt where concat (A) and per-field
276
+ # H=(1.0, 1.0) give a DIFFERENT top-1, and print both top-5 lists.
277
+ for prompt, rows, _ in collected:
278
+ if len(rows) < 5:
279
+ continue
280
+ rows_concat = [dict(r) for r in rows]
281
+ rows_pf = [dict(r) for r in rows]
282
+ _apply_concat_bm25(prompt, rows_concat)
283
+ _apply_per_field_bm25(prompt, rows_pf, 1.0, 1.0)
284
+ # Rank by the hook's real _relevance proxy (W0).
285
+ top5_concat = eval_module._rank(rows_concat, eval_module.W0)[:5]
286
+ top5_pf = eval_module._rank(rows_pf, eval_module.W0)[:5]
287
+ id0_c = getattr(top5_concat[0].get("claim"), "id", None)
288
+ id0_p = getattr(top5_pf[0].get("claim"), "id", None)
289
+ if id0_c != id0_p:
290
+ print("\n--- sample prompt where top-1 differs ---")
291
+ print(f"PROMPT: {prompt[:120]!r}")
292
+ print("concat baseline top-5:")
293
+ for row in top5_concat:
294
+ c = row.get("claim")
295
+ print(f" cid={getattr(c, 'id', '?')!s:>6} "
296
+ f"subj={str(getattr(c, 'subject', ''))[:40]!r} "
297
+ f"text={str(getattr(c, 'text', ''))[:70]!r}")
298
+ print("per-field (1.0, 1.0) top-5:")
299
+ for row in top5_pf:
300
+ c = row.get("claim")
301
+ print(f" cid={getattr(c, 'id', '?')!s:>6} "
302
+ f"subj={str(getattr(c, 'subject', ''))[:40]!r} "
303
+ f"text={str(getattr(c, 'text', ''))[:70]!r}")
304
+ break
305
+ return 0
306
+
307
+
308
+ if __name__ == "__main__":
309
+ raise SystemExit(main())
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "3.4.0"
5
+ __version__ = "3.4.1"
@@ -298,6 +298,14 @@ def build_parser() -> argparse.ArgumentParser:
298
298
  )
299
299
  wiki_backfill.add_argument("--output", default="obsidian-vault", help="Wiki directory to scan")
300
300
 
301
+ wiki_freshness = sub.add_parser(
302
+ "wiki-freshness",
303
+ help="Report per-article freshness (Option A — absorb recency)",
304
+ )
305
+ wiki_freshness.add_argument("--vault", default="obsidian-vault/wiki", help="Wiki root (defaults to obsidian-vault/wiki)")
306
+ wiki_freshness.add_argument("--below", type=float, default=None, help="Only show articles with freshness_score below this threshold (0-1)")
307
+ wiki_freshness.add_argument("--threshold-days", type=int, default=None, help="Only show articles older than N days since last absorb (alias for --below)")
308
+
301
309
  mine_cmd = sub.add_parser("mine-transcript", help="Parse Claude Code transcripts into claims")
302
310
  mine_cmd.add_argument("--input", required=True, help="JSONL transcript file or directory")
303
311
  mine_cmd.add_argument("--scope", default="project", help="Scope for ingested claims")
@@ -407,7 +415,7 @@ def main(argv: list[str] | None = None) -> int:
407
415
  effective_db = _resolve_db_path(args)
408
416
 
409
417
  # Commands that don't need MemoryService run first; service is lazy-created once for all others.
410
- _NO_SERVICE_COMMANDS = {"stealth-status", "export-metrics"}
418
+ _NO_SERVICE_COMMANDS = {"stealth-status", "export-metrics", "wiki-freshness"}
411
419
 
412
420
  try:
413
421
  handler = COMMAND_HANDLERS.get(args.command)
@@ -107,6 +107,16 @@ def _handle_lint_vault(args: argparse.Namespace, service, parser: argparse.Argum
107
107
  print(f"\n Stale claims ({len(report['stale'])}):")
108
108
  for s in report["stale"][:10]:
109
109
  print(f" #{s['id']} ({s['age_days']}d old, conf={s['confidence']:.2f}) {s['text'][:50]}")
110
+ stale_articles = report.get("stale_articles") or []
111
+ if stale_articles:
112
+ print(f"\n Stale articles ({len(stale_articles)}):")
113
+ for a in stale_articles[:10]:
114
+ scope = a.get("scope") or ""
115
+ title = a.get("title") or ""
116
+ print(
117
+ f" [{scope}] {title} — {a['days_since_absorb']:.0f}d "
118
+ f"(freshness={a['freshness_score']:.2f})"
119
+ )
110
120
  return 0
111
121
 
112
122
 
@@ -217,6 +227,65 @@ def _handle_bases_generate(args: argparse.Namespace, service, parser: argparse.A
217
227
  return 0
218
228
 
219
229
 
230
+ def _handle_wiki_freshness(args: argparse.Namespace, service, parser: argparse.ArgumentParser, effective_db: str) -> int:
231
+ """Print per-article freshness scores (Option A — absorb recency).
232
+
233
+ Service is unused; the metric is a pure filesystem read over the vault.
234
+ """
235
+ from memorymaster.wiki_freshness import (
236
+ as_jsonable,
237
+ bucket_distribution,
238
+ scan_vault,
239
+ )
240
+
241
+ vault_root = Path(args.vault)
242
+ t0 = time.perf_counter()
243
+ snapshots = scan_vault(vault_root)
244
+ elapsed_ms = (time.perf_counter() - t0) * 1000
245
+
246
+ # Optional filters.
247
+ threshold_score: float | None = None
248
+ if args.below is not None:
249
+ threshold_score = float(args.below)
250
+ if args.threshold_days is not None:
251
+ import math as _math
252
+ # Convert the day threshold into the equivalent score cut-off using the
253
+ # same decay curve as wiki_freshness.FRESHNESS_SCALE_DAYS.
254
+ equivalent = _math.exp(-float(args.threshold_days) / 30.0)
255
+ threshold_score = equivalent if threshold_score is None else min(threshold_score, equivalent)
256
+
257
+ filtered = snapshots
258
+ if threshold_score is not None:
259
+ filtered = [s for s in snapshots if s.freshness_score < threshold_score]
260
+
261
+ dist = bucket_distribution(snapshots)
262
+
263
+ if args.json_output:
264
+ payload = {
265
+ "vault": str(vault_root),
266
+ "total_articles": len(snapshots),
267
+ "distribution": dist,
268
+ "threshold_score": threshold_score,
269
+ "articles": as_jsonable(filtered),
270
+ }
271
+ print(_json_envelope(payload, total=len(filtered), query_ms=elapsed_ms))
272
+ return 0
273
+
274
+ print(f"wiki-freshness: {len(snapshots)} articles scanned in {elapsed_ms:.0f}ms")
275
+ print(f" fresh (>=0.5): {dist['fresh']} mid (0.2-0.5): {dist['mid']} stale (<0.2): {dist['stale']}")
276
+ if threshold_score is not None:
277
+ print(f" filter: freshness_score < {threshold_score:.3f} -> {len(filtered)} matching")
278
+ if not filtered:
279
+ return 0
280
+ print()
281
+ print(f" {'score':>6} {'days':>6} {'scope':<28} {'title'}")
282
+ for snap in filtered:
283
+ title = snap.title[:50]
284
+ scope = (snap.scope or "")[:28]
285
+ print(f" {snap.freshness_score:>6.3f} {snap.days_since_absorb:>6.1f} {scope:<28} {title}")
286
+ return 0
287
+
288
+
220
289
  def _handle_wiki_cleanup(args: argparse.Namespace, service, parser: argparse.ArgumentParser, effective_db: str) -> int:
221
290
  from memorymaster.wiki_engine import cleanup
222
291
  t0 = time.perf_counter()
@@ -599,6 +668,7 @@ COMMAND_HANDLERS: dict[str, object] = {
599
668
  "wiki-cleanup": _handle_wiki_cleanup,
600
669
  "wiki-breakdown": _handle_wiki_breakdown,
601
670
  "wiki-backfill-bindings": _handle_wiki_backfill_bindings,
671
+ "wiki-freshness": _handle_wiki_freshness,
602
672
  "bases-generate": _handle_bases_generate,
603
673
  "mine-transcript": _handle_mine_transcript,
604
674
  "verify-claims": _handle_verify_claims,