memorymaster 3.26.0__tar.gz → 3.28.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 (441) hide show
  1. {memorymaster-3.26.0/memorymaster.egg-info → memorymaster-3.28.0}/PKG-INFO +2 -2
  2. {memorymaster-3.26.0 → memorymaster-3.28.0}/README.md +1 -1
  3. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/__init__.py +1 -1
  4. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_read.py +82 -40
  5. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_schema.py +73 -4
  6. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_sources.py +12 -1
  7. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/auto_resolver.py +34 -7
  8. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/candidate_dedupe.py +17 -4
  9. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/cli.py +27 -0
  10. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/cli_handlers_basic.py +94 -1
  11. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/cli_handlers_curation.py +46 -2
  12. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config.py +0 -22
  13. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-steward-cycle.py +5 -1
  14. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/conflict_resolver.py +15 -8
  15. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/context_hook.py +187 -30
  16. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/contradiction_probe.py +20 -4
  17. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/dashboard.py +30 -0
  18. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/db_merge.py +166 -18
  19. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/dream_bridge.py +11 -0
  20. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/embeddings.py +19 -1
  21. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/entity_graph.py +65 -27
  22. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/entity_registry.py +32 -14
  23. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/llm_provider.py +89 -15
  24. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/llm_rerank.py +19 -34
  25. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/llm_steward.py +172 -38
  26. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/mcp_server.py +222 -135
  27. memorymaster-3.28.0/memorymaster/migrations/0006_verbatim_session_content_index.py +59 -0
  28. memorymaster-3.28.0/memorymaster/migrations/0007_rule_stats.py +54 -0
  29. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/postgres_store.py +15 -2
  30. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/query_classifier.py +11 -7
  31. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/recall_tokenizer.py +27 -3
  32. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/retrieval.py +102 -9
  33. memorymaster-3.28.0/memorymaster/rule_export.py +157 -0
  34. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/rule_miner.py +134 -5
  35. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/schema_postgres.sql +21 -0
  36. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/security.py +171 -2
  37. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/service.py +333 -5
  38. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/steward.py +21 -8
  39. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/storage.py +6 -0
  40. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_linter.py +11 -13
  41. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_query_capture.py +4 -3
  42. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/verbatim_cleanup.py +46 -23
  43. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/verbatim_store.py +13 -6
  44. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/webhook.py +11 -0
  45. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/wiki_engine.py +72 -11
  46. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/wiki_validate.py +2 -2
  47. {memorymaster-3.26.0 → memorymaster-3.28.0/memorymaster.egg-info}/PKG-INFO +2 -2
  48. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster.egg-info/SOURCES.txt +31 -0
  49. {memorymaster-3.26.0 → memorymaster-3.28.0}/pyproject.toml +1 -1
  50. memorymaster-3.28.0/scripts/archive_watchkeeper_heartbeats.py +81 -0
  51. memorymaster-3.28.0/tests/test_claim_paths.py +306 -0
  52. memorymaster-3.28.0/tests/test_conflict_resolver_extra.py +415 -0
  53. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_contradiction_probe.py +58 -0
  54. memorymaster-3.28.0/tests/test_db_merge_audit_fixes.py +184 -0
  55. memorymaster-3.28.0/tests/test_delta_sync_extra.py +392 -0
  56. memorymaster-3.28.0/tests/test_dream_bridge_subject_sensitivity.py +83 -0
  57. memorymaster-3.28.0/tests/test_embeddings_degraded_signal.py +53 -0
  58. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_graph.py +100 -0
  59. memorymaster-3.28.0/tests/test_entity_registry_merge_atomicity.py +94 -0
  60. memorymaster-3.28.0/tests/test_graph_harvest.py +348 -0
  61. memorymaster-3.28.0/tests/test_lifecycle_extra.py +314 -0
  62. memorymaster-3.28.0/tests/test_llm_concurrency_fixes.py +253 -0
  63. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_fallback.py +20 -5
  64. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_provider_extra.py +53 -10
  65. memorymaster-3.28.0/tests/test_llm_steward_scope.py +191 -0
  66. memorymaster-3.28.0/tests/test_mcp_server_security_cluster.py +187 -0
  67. memorymaster-3.28.0/tests/test_misc_correctness_cluster.py +92 -0
  68. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_postgres_parity.py +110 -0
  69. memorymaster-3.28.0/tests/test_read_visibility_filter.py +156 -0
  70. memorymaster-3.28.0/tests/test_recall_analysis.py +306 -0
  71. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_tokenizer.py +70 -2
  72. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_rrf_auto_gate.py +139 -0
  73. memorymaster-3.28.0/tests/test_rule_confidence_bootstrap.py +210 -0
  74. memorymaster-3.28.0/tests/test_rules_export.py +152 -0
  75. memorymaster-3.28.0/tests/test_service_embedding_toctou.py +109 -0
  76. memorymaster-3.28.0/tests/test_steward_resolvers_audit_fixes.py +236 -0
  77. memorymaster-3.28.0/tests/test_storage_concurrency.py +112 -0
  78. memorymaster-3.28.0/tests/test_storage_internals_audit.py +124 -0
  79. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_vault_linter_extra.py +17 -9
  80. memorymaster-3.28.0/tests/test_vault_linter_weak_link.py +43 -0
  81. memorymaster-3.28.0/tests/test_vault_query_capture_scope.py +38 -0
  82. memorymaster-3.28.0/tests/test_verbatim_perf_index.py +234 -0
  83. memorymaster-3.28.0/tests/test_wiki_backfill_bindings_backend.py +94 -0
  84. memorymaster-3.28.0/tests/test_wiki_engine_absorb_fixes.py +187 -0
  85. memorymaster-3.28.0/tests/test_wiki_validate_desc_bound.py +30 -0
  86. {memorymaster-3.26.0 → memorymaster-3.28.0}/LICENSE +0 -0
  87. {memorymaster-3.26.0 → memorymaster-3.28.0}/artifacts/bm25-per-field-eval-harness.py +0 -0
  88. {memorymaster-3.26.0 → memorymaster-3.28.0}/benchmarks/longmemeval_runner.py +0 -0
  89. {memorymaster-3.26.0 → memorymaster-3.28.0}/benchmarks/longmemeval_vector_runner.py +0 -0
  90. {memorymaster-3.26.0 → memorymaster-3.28.0}/benchmarks/perf_smoke.py +0 -0
  91. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/__main__.py +0 -0
  92. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_lifecycle.py +0 -0
  93. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_shared.py +0 -0
  94. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_write_claims.py +0 -0
  95. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/access_control.py +0 -0
  96. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/action_exporters.py +0 -0
  97. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/action_extractor.py +0 -0
  98. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/atlas_claim_extractor.py +0 -0
  99. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/atlas_contract.py +0 -0
  100. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/auto_extractor.py +0 -0
  101. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/claim_edges.py +0 -0
  102. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/claim_verifier.py +0 -0
  103. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/cli_helpers.py +0 -0
  104. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/closets.py +0 -0
  105. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/claude-md-append.md +0 -0
  106. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/codex-agents-md-append.md +0 -0
  107. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-auto-ingest.py +0 -0
  108. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-classify.py +0 -0
  109. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-dream-sync.py +0 -0
  110. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-precompact.py +0 -0
  111. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-recall.py +0 -0
  112. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-session-start.py +0 -0
  113. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-validate-wiki.py +0 -0
  114. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/connectors/__init__.py +0 -0
  115. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/connectors/whatsapp.py +0 -0
  116. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/context_optimizer.py +0 -0
  117. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/daily_notes.py +0 -0
  118. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/dashboard_auth.py +0 -0
  119. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/delta_sync.py +0 -0
  120. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/entity_extractor.py +0 -0
  121. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/federated_graphify.py +0 -0
  122. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/feedback.py +0 -0
  123. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/graph_store.py +0 -0
  124. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/hook_log.py +0 -0
  125. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/__init__.py +0 -0
  126. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/calibration.py +0 -0
  127. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/compact_summaries.py +0 -0
  128. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/compactor.py +0 -0
  129. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/daydream_ingest.py +0 -0
  130. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/decay.py +0 -0
  131. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/dedup.py +0 -0
  132. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/deterministic.py +0 -0
  133. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/entity_graph_export.py +0 -0
  134. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/extractor.py +0 -0
  135. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/staleness.py +0 -0
  136. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/validator.py +0 -0
  137. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/key_rotator.py +0 -0
  138. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/lifecycle.py +0 -0
  139. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/llm_budget.py +0 -0
  140. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/mcp_path_policy.py +0 -0
  141. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/mcp_usage.py +0 -0
  142. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/media_processing.py +0 -0
  143. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/media_providers.py +0 -0
  144. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/metrics_exporter.py +0 -0
  145. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/0001_initial.py +0 -0
  146. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/0002_miner_state.py +0 -0
  147. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/0003_contradiction_verdicts.py +0 -0
  148. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/0004_query_cache.py +0 -0
  149. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/__init__.py +0 -0
  150. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/runner.py +0 -0
  151. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/models.py +0 -0
  152. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/observability.py +0 -0
  153. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/operator.py +0 -0
  154. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/operator_queue.py +0 -0
  155. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/plugins.py +0 -0
  156. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/policy.py +0 -0
  157. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/qdrant_backend.py +0 -0
  158. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/qdrant_recall_fallback.py +0 -0
  159. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/qmd_bridge.py +0 -0
  160. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/query_cache.py +0 -0
  161. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/query_expansion.py +0 -0
  162. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/recall_fusion.py +0 -0
  163. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/retry.py +0 -0
  164. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/review.py +0 -0
  165. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/rl_trainer.py +0 -0
  166. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/rules.py +0 -0
  167. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/scheduler.py +0 -0
  168. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/schema.py +0 -0
  169. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/schema.sql +0 -0
  170. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/scope_utils.py +0 -0
  171. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/session_tracker.py +0 -0
  172. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/setup_hooks.py +0 -0
  173. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/skill_evolver.py +0 -0
  174. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/snapshot.py +0 -0
  175. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/steward_classifier.py +0 -0
  176. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/steward_features.py +0 -0
  177. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/store_factory.py +0 -0
  178. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/transcript_miner.py +0 -0
  179. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/turn_schema.py +0 -0
  180. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_bases.py +0 -0
  181. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_curator.py +0 -0
  182. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_exporter.py +0 -0
  183. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_log.py +0 -0
  184. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_synthesis.py +0 -0
  185. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/verbatim_recall.py +0 -0
  186. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/wiki_freshness.py +0 -0
  187. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/wiki_similarity.py +0 -0
  188. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/wiki_suggest.py +0 -0
  189. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster.egg-info/dependency_links.txt +0 -0
  190. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster.egg-info/entry_points.txt +0 -0
  191. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster.egg-info/requires.txt +0 -0
  192. {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster.egg-info/top_level.txt +0 -0
  193. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/agg_recall_latency.py +0 -0
  194. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/alert_operator_metrics.py +0 -0
  195. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/audit_dedupe_precision.py +0 -0
  196. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/autoresearch_daemon.py +0 -0
  197. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/backfill_entity_extraction.py +0 -0
  198. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/backfill_graph_store.py +0 -0
  199. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/backfill_stop_hook_citations.py +0 -0
  200. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/backtest_steward_classifier.py +0 -0
  201. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/build_steward_training_set.py +0 -0
  202. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/check_hook_template_drift.py +0 -0
  203. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/claude_to_turns.py +0 -0
  204. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/codex_live_to_turns.py +0 -0
  205. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/compaction_edge_cases.py +0 -0
  206. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/compaction_trace_report.py +0 -0
  207. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/compaction_trace_validate.py +0 -0
  208. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/confusion_matrix_eval.py +0 -0
  209. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/conversation_importer.py +0 -0
  210. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/conversation_to_turns.py +0 -0
  211. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/e2e_operator.py +0 -0
  212. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/email_live_to_turns.py +0 -0
  213. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_bm25_sweep.py +0 -0
  214. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_classify_f1.py +0 -0
  215. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_memorymaster.py +0 -0
  216. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_recall_precision_at_5.py +0 -0
  217. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_recall_quality.py +0 -0
  218. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_steward_pareto.py +0 -0
  219. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_verbatim_recall.py +0 -0
  220. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/expand_recall_eval.py +0 -0
  221. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/generate_drill_signoff.py +0 -0
  222. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/git_to_turns.py +0 -0
  223. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/github_live_to_turns.py +0 -0
  224. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/gitnexus_to_claims.py +0 -0
  225. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/grid_recall_weights.py +0 -0
  226. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/index_claims_to_qdrant.py +0 -0
  227. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/ingest_planning_docs.py +0 -0
  228. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/jira_live_to_turns.py +0 -0
  229. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/label_prompts_with_judge.py +0 -0
  230. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/llm_benchmark.py +0 -0
  231. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/measure_dedupe_thresholds.py +0 -0
  232. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/merge_scope_variants.py +0 -0
  233. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/messages_to_turns.py +0 -0
  234. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/operator_metrics.py +0 -0
  235. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/precompute_candidates.py +0 -0
  236. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/recurring_incident_drill.py +0 -0
  237. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/release_readiness.py +0 -0
  238. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/run_codex_autologger.py +0 -0
  239. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/run_incident_drill.py +0 -0
  240. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/scheduled_ingest.py +0 -0
  241. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/setup-hooks.py +0 -0
  242. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/slack_live_to_turns.py +0 -0
  243. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/sync_hook_templates.py +0 -0
  244. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/tickets_to_turns.py +0 -0
  245. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/train_steward_classifier.py +0 -0
  246. {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/webhook_to_turns.py +0 -0
  247. {memorymaster-3.26.0 → memorymaster-3.28.0}/setup.cfg +0 -0
  248. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/bench_longmemeval.py +0 -0
  249. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/conftest.py +0 -0
  250. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/integration/test_extract_llm_ollama_live.py +0 -0
  251. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_access_control.py +0 -0
  252. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_action_exporters.py +0 -0
  253. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_action_extractor.py +0 -0
  254. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_atlas_claim_extractor.py +0 -0
  255. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_atlas_contract.py +0 -0
  256. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_atlas_source_schema.py +0 -0
  257. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_auto_extractor.py +0 -0
  258. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_auto_ingest_hook_citations.py +0 -0
  259. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_auto_ingest_hook_schema.py +0 -0
  260. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_auto_resolver.py +0 -0
  261. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_auto_validate.py +0 -0
  262. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_backend_parity.py +0 -0
  263. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_bench_answer_prompt.py +0 -0
  264. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_bm25_per_field.py +0 -0
  265. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_calibration.py +0 -0
  266. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_calibration_priors_applied.py +0 -0
  267. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_candidate_dedupe.py +0 -0
  268. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_claim_edges.py +0 -0
  269. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_claim_links.py +0 -0
  270. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_claim_type_ranking.py +0 -0
  271. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_classify_hook_f1.py +0 -0
  272. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_classify_hook_latency.py +0 -0
  273. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_claude_to_turns.py +0 -0
  274. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_dry_run.py +0 -0
  275. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_handlers_extra.py +0 -0
  276. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_json_flag.py +0 -0
  277. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_ready.py +0 -0
  278. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_review_queue.py +0 -0
  279. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_subcommands.py +0 -0
  280. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_closets.py +0 -0
  281. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_closets_recall_integration.py +0 -0
  282. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_compact_summaries.py +0 -0
  283. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_compact_summaries_sensitivity.py +0 -0
  284. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_compaction_trace.py +0 -0
  285. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_compactor_artifact_order.py +0 -0
  286. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_config.py +0 -0
  287. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_conflict_resolver.py +0 -0
  288. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_confusion_matrix_eval.py +0 -0
  289. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_connection_retry.py +0 -0
  290. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_connectors.py +0 -0
  291. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_context_hook.py +0 -0
  292. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_context_hook_extra.py +0 -0
  293. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_context_optimizer.py +0 -0
  294. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_context_optimizer_provider.py +0 -0
  295. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_conversation_to_turns.py +0 -0
  296. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard.py +0 -0
  297. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard_auth.py +0 -0
  298. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard_coverage.py +0 -0
  299. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard_latency.py +0 -0
  300. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard_lineage.py +0 -0
  301. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard_review_queue.py +0 -0
  302. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_daydream_ingest.py +0 -0
  303. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_db_merge_confidence_conflict.py +0 -0
  304. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_db_merge_coverage_v2.py +0 -0
  305. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_db_merge_extra.py +0 -0
  306. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_decay_coverage.py +0 -0
  307. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_decay_respects_pinned.py +0 -0
  308. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dedup.py +0 -0
  309. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dedup_cli.py +0 -0
  310. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dedup_conflict_disambiguation.py +0 -0
  311. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_delta_sync.py +0 -0
  312. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_deterministic_predicates.py +0 -0
  313. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dream_bridge_coverage_v2.py +0 -0
  314. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dream_bridge_extra.py +0 -0
  315. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dream_bridge_sensitivity.py +0 -0
  316. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_embeddings_coverage.py +0 -0
  317. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_extractor.py +0 -0
  318. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_extractor_llm.py +0 -0
  319. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_graph_export.py +0 -0
  320. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_new_kinds.py +0 -0
  321. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_regex_v3.py +0 -0
  322. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_registry.py +0 -0
  323. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_registry_extra.py +0 -0
  324. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_eval_harness.py +0 -0
  325. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_events_schema.py +0 -0
  326. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_extract_llm_ollama.py +0 -0
  327. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_federated_graphify_mcp.py +0 -0
  328. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_federated_query_safety.py +0 -0
  329. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_feedback.py +0 -0
  330. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_floor_gate.py +0 -0
  331. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_fts5_search.py +0 -0
  332. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_graph_distance.py +0 -0
  333. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_graph_store.py +0 -0
  334. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_handler_regressions.py +0 -0
  335. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_hook_env_isolation.py +0 -0
  336. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_human_id.py +0 -0
  337. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_incident_drill_runner.py +0 -0
  338. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_integration_workflows.py +0 -0
  339. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_key_rotator.py +0 -0
  340. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_lifecycle.py +0 -0
  341. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_lifecycle_supersede_invariant.py +0 -0
  342. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_budget.py +0 -0
  343. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_provider_claude_cli.py +0 -0
  344. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_provider_key_rotation.py +0 -0
  345. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_steward_coverage.py +0 -0
  346. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_steward_key_rotation.py +0 -0
  347. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_filter_bypass.py +0 -0
  348. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_helpers.py +0 -0
  349. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_path_policy.py +0 -0
  350. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_rate_limit.py +0 -0
  351. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_server_validation.py +0 -0
  352. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_usage.py +0 -0
  353. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_media_processing.py +0 -0
  354. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_meta_decisions.py +0 -0
  355. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_metrics_exporter.py +0 -0
  356. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_migrations.py +0 -0
  357. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_observability.py +0 -0
  358. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_observability_extra.py +0 -0
  359. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_obsidian_mind_patterns.py +0 -0
  360. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_operator.py +0 -0
  361. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_operator_queue.py +0 -0
  362. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_perf_smoke_config.py +0 -0
  363. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_plugins.py +0 -0
  364. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_policy_coverage.py +0 -0
  365. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_policy_mode_env.py +0 -0
  366. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_qdrant_backend.py +0 -0
  367. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_qmd_bridge.py +0 -0
  368. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_qrels_regression.py +0 -0
  369. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_query_cache.py +0 -0
  370. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_query_classifier.py +0 -0
  371. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_query_expansion.py +0 -0
  372. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_entity_fanout.py +0 -0
  373. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_fusion.py +0 -0
  374. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_latency.py +0 -0
  375. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_precision_at_5.py +0 -0
  376. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_vector_fallback.py +0 -0
  377. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_reliability_hardening.py +0 -0
  378. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_resolvers_concurrent_supersede.py +0 -0
  379. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_retrieval_extra.py +0 -0
  380. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_retrieval_profile.py +0 -0
  381. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_retrieval_profiles.py +0 -0
  382. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_retrieval_rrf_tiebreaker.py +0 -0
  383. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_retrieval_weights.py +0 -0
  384. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_review.py +0 -0
  385. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_rl_trainer.py +0 -0
  386. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_rule_claims.py +0 -0
  387. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_rule_miner.py +0 -0
  388. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_scheduler.py +0 -0
  389. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_schema.py +0 -0
  390. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_scope_boost.py +0 -0
  391. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_scope_utils.py +0 -0
  392. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_security_access.py +0 -0
  393. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_security_patterns.py +0 -0
  394. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_sensitivity_filter_adversarial.py +0 -0
  395. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_sensitivity_filter_adversarial_v2.py +0 -0
  396. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_sensitivity_filter_t07.py +0 -0
  397. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_service_coverage.py +0 -0
  398. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_session_tracker.py +0 -0
  399. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_snapshot.py +0 -0
  400. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_snapshot_roundtrip.py +0 -0
  401. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_sqlite_core.py +0 -0
  402. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_staleness.py +0 -0
  403. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_stealth_mode.py +0 -0
  404. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward.py +0 -0
  405. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_classifier.py +0 -0
  406. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_contradiction_phase.py +0 -0
  407. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_daydream_hook.py +0 -0
  408. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_features.py +0 -0
  409. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_features_v3.py +0 -0
  410. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_resolution_parity.py +0 -0
  411. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_storage_extra.py +0 -0
  412. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_storage_parity.py +0 -0
  413. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_store_factory.py +0 -0
  414. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_tenant_isolation.py +0 -0
  415. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_turn_schema.py +0 -0
  416. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_two_pass_recall.py +0 -0
  417. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_v311_fixes.py +0 -0
  418. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_v313_e2e.py +0 -0
  419. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_v313_run_cycle_dedupe.py +0 -0
  420. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_v390_e2e.py +0 -0
  421. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_v391_strict_warnings.py +0 -0
  422. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_vault_exporter.py +0 -0
  423. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_vault_linter_orphan.py +0 -0
  424. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_vector_search.py +0 -0
  425. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_verbatim_cleanup.py +0 -0
  426. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_verbatim_dedup.py +0 -0
  427. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_verbatim_recall.py +0 -0
  428. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_verbatim_store.py +0 -0
  429. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_verbatim_store_qdrant.py +0 -0
  430. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_webhook.py +0 -0
  431. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_webhook_hmac.py +0 -0
  432. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_whatsapp_importer.py +0 -0
  433. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_autopromote.py +0 -0
  434. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_binding.py +0 -0
  435. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_engine_extra.py +0 -0
  436. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_engine_idempotency.py +0 -0
  437. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_explored_and_contradictions.py +0 -0
  438. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_freshness.py +0 -0
  439. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_similarity_multiscope.py +0 -0
  440. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_suggest.py +0 -0
  441. {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_validate_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memorymaster
3
- Version: 3.26.0
3
+ Version: 3.28.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
@@ -52,7 +52,7 @@ Lifecycle-managed claims with citations, conflict detection, steward governance,
52
52
 
53
53
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
54
54
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
55
- [![Tests](https://img.shields.io/badge/tests-2514-green.svg)]()
55
+ [![Tests](https://img.shields.io/badge/tests-2732-green.svg)]()
56
56
  [![MCP Tools](https://img.shields.io/badge/MCP%20tools-24-purple.svg)]()
57
57
  [![CLI Commands](https://img.shields.io/badge/CLI%20commands-86-orange.svg)]()
58
58
  [![PyPI](https://img.shields.io/pypi/v/memorymaster.svg)](https://pypi.org/project/memorymaster/)
@@ -6,7 +6,7 @@ Lifecycle-managed claims with citations, conflict detection, steward governance,
6
6
 
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
8
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
9
- [![Tests](https://img.shields.io/badge/tests-2514-green.svg)]()
9
+ [![Tests](https://img.shields.io/badge/tests-2732-green.svg)]()
10
10
  [![MCP Tools](https://img.shields.io/badge/MCP%20tools-24-purple.svg)]()
11
11
  [![CLI Commands](https://img.shields.io/badge/CLI%20commands-86-orange.svg)]()
12
12
  [![PyPI](https://img.shields.io/pypi/v/memorymaster.svg)](https://pypi.org/project/memorymaster/)
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "3.26.0"
5
+ __version__ = "3.28.0"
@@ -33,16 +33,23 @@ class _ReadMixin:
33
33
  normalized_key = (idempotency_key or "").strip() or None
34
34
  if normalized_key is None:
35
35
  return None
36
+ # Hydrate the full row from the already-open conn instead of paying
37
+ # a fresh get_claim() connection open on every duplicate re-ingest.
36
38
  existing_row = conn.execute(
37
- "SELECT id FROM claims WHERE idempotency_key = ?",
39
+ "SELECT * FROM claims WHERE idempotency_key = ?",
38
40
  (normalized_key,),
39
41
  ).fetchone()
40
- if existing_row is not None:
41
- existing = self.get_claim(int(existing_row["id"]))
42
- if existing is None:
43
- raise RuntimeError("Idempotency key matched missing claim.")
44
- return existing
45
- return None
42
+ if existing_row is None:
43
+ return None
44
+ claim = self._row_to_claim(existing_row)
45
+ claim.citations = [
46
+ self._row_to_citation(cit_row)
47
+ for cit_row in conn.execute(
48
+ "SELECT * FROM citations WHERE claim_id = ? ORDER BY id ASC",
49
+ (claim.id,),
50
+ ).fetchall()
51
+ ]
52
+ return claim
46
53
 
47
54
 
48
55
  def get_claim(self, claim_id: int, include_citations: bool = True) -> Claim | None:
@@ -172,7 +179,13 @@ class _ReadMixin:
172
179
  clauses.append(f"status IN ({placeholders})")
173
180
  params.extend(status_in)
174
181
 
175
- if not include_archived and status != "archived":
182
+ # Only apply the implicit archived-exclusion when the caller did not
183
+ # explicitly request archived claims via `status` or `status_in`.
184
+ # Otherwise list_claims(status_in=['archived']) would return zero rows.
185
+ explicitly_wants_archived = (
186
+ status == "archived" or (status is None and bool(status_in) and "archived" in status_in)
187
+ )
188
+ if not include_archived and not explicitly_wants_archived:
176
189
  clauses.append("status <> 'archived'")
177
190
 
178
191
  if scope_allowlist:
@@ -597,6 +610,45 @@ class _ReadMixin:
597
610
  ).fetchall()
598
611
  return [self._row_to_citation(row) for row in rows]
599
612
 
613
+ @staticmethod
614
+ def _batch_neighbors(
615
+ conn: sqlite3.Connection,
616
+ frontier_ids: list[int],
617
+ *,
618
+ direction: str,
619
+ link_types: list[str] | None,
620
+ ) -> dict[int, list[tuple[int, str]]]:
621
+ """Fetch all neighbors for a whole BFS frontier in one query per direction.
622
+
623
+ Returns a mapping ``{frontier_claim_id: [(neighbor_id, link_type), ...]}``.
624
+ Replaces the per-node N+1 pattern with one batched query per level.
625
+ """
626
+ if not frontier_ids:
627
+ return {}
628
+ result: dict[int, list[tuple[int, str]]] = {cid: [] for cid in frontier_ids}
629
+ placeholders = ",".join("?" for _ in frontier_ids)
630
+ if direction in ("outgoing", "both"):
631
+ sql = f"SELECT source_id, target_id, link_type FROM claim_links WHERE source_id IN ({placeholders})"
632
+ for row in conn.execute(sql, frontier_ids).fetchall():
633
+ if link_types is None or row["link_type"] in link_types:
634
+ result[int(row["source_id"])].append((int(row["target_id"]), str(row["link_type"])))
635
+ if direction in ("incoming", "both"):
636
+ sql = f"SELECT source_id, target_id, link_type FROM claim_links WHERE target_id IN ({placeholders})"
637
+ for row in conn.execute(sql, frontier_ids).fetchall():
638
+ if link_types is None or row["link_type"] in link_types:
639
+ result[int(row["target_id"])].append((int(row["source_id"]), str(row["link_type"])))
640
+ return result
641
+
642
+ def _hydrate_claims(self, conn: sqlite3.Connection, claim_ids: list[int]) -> dict[int, Claim]:
643
+ """Load a batch of claims (no citations) in a single SELECT from *conn*."""
644
+ if not claim_ids:
645
+ return {}
646
+ placeholders = ",".join("?" for _ in claim_ids)
647
+ rows = conn.execute(
648
+ f"SELECT * FROM claims WHERE id IN ({placeholders})", claim_ids
649
+ ).fetchall()
650
+ return {int(row["id"]): self._row_to_claim(row) for row in rows}
651
+
600
652
  def traverse_relationships(
601
653
  self,
602
654
  start_claim_id: int,
@@ -616,44 +668,34 @@ class _ReadMixin:
616
668
  """
617
669
  with self.connect() as conn:
618
670
  visited: set[int] = {start_claim_id}
619
- queue: list[tuple[int, int, list[int], str]] = [] # (claim_id, depth, path, via_link_type)
620
-
621
- # Seed with depth-0 neighbors
622
- def _get_neighbors(claim_id: int) -> list[tuple[int, str]]:
623
- neighbors: list[tuple[int, str]] = []
624
- if direction in ("outgoing", "both"):
625
- q = "SELECT target_id, link_type FROM claim_links WHERE source_id = ?"
626
- for row in conn.execute(q, (claim_id,)).fetchall():
627
- if link_types is None or row[1] in link_types:
628
- neighbors.append((row[0], row[1]))
629
- if direction in ("incoming", "both"):
630
- q = "SELECT source_id, link_type FROM claim_links WHERE target_id = ?"
631
- for row in conn.execute(q, (claim_id,)).fetchall():
632
- if link_types is None or row[1] in link_types:
633
- neighbors.append((row[0], row[1]))
634
- return neighbors
635
-
636
- for neighbor_id, link_type in _get_neighbors(start_claim_id):
671
+ # Frontier entries: (claim_id, depth, path, via_link_type)
672
+ frontier: list[tuple[int, int, list[int], str]] = []
673
+ seed = self._batch_neighbors(conn, [start_claim_id], direction=direction, link_types=link_types)
674
+ for neighbor_id, link_type in seed.get(start_claim_id, []):
637
675
  if neighbor_id not in visited:
638
676
  visited.add(neighbor_id)
639
- queue.append((neighbor_id, 1, [start_claim_id, neighbor_id], link_type))
677
+ frontier.append((neighbor_id, 1, [start_claim_id, neighbor_id], link_type))
640
678
 
641
679
  results: list[dict] = []
642
- while queue:
643
- cid, depth, path, via_type = queue.pop(0)
644
- claim = self.get_claim(cid, include_citations=False)
645
- if claim:
646
- results.append({
647
- "claim": claim,
648
- "depth": depth,
649
- "path": path,
650
- "link_type": via_type,
651
- })
652
- if depth < max_depth:
653
- for neighbor_id, link_type in _get_neighbors(cid):
680
+ while frontier:
681
+ level_ids = [cid for cid, _, _, _ in frontier]
682
+ claims = self._hydrate_claims(conn, level_ids)
683
+ next_frontier: list[tuple[int, int, list[int], str]] = []
684
+ depth = frontier[0][1] if frontier else 0
685
+ neighbor_map = (
686
+ self._batch_neighbors(conn, level_ids, direction=direction, link_types=link_types)
687
+ if depth < max_depth
688
+ else {}
689
+ )
690
+ for cid, cdepth, path, via_type in frontier:
691
+ claim = claims.get(cid)
692
+ if claim:
693
+ results.append({"claim": claim, "depth": cdepth, "path": path, "link_type": via_type})
694
+ for neighbor_id, link_type in neighbor_map.get(cid, []):
654
695
  if neighbor_id not in visited:
655
696
  visited.add(neighbor_id)
656
- queue.append((neighbor_id, depth + 1, path + [neighbor_id], link_type))
697
+ next_frontier.append((neighbor_id, cdepth + 1, path + [neighbor_id], link_type))
698
+ frontier = next_frontier
657
699
 
658
700
  return results
659
701
 
@@ -329,6 +329,10 @@ class _SchemaMixin:
329
329
  if not _SchemaMixin._fts5_available(conn):
330
330
  return
331
331
 
332
+ fts_existed = conn.execute(
333
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='claims_fts'"
334
+ ).fetchone() is not None
335
+
332
336
  conn.execute(
333
337
  """
334
338
  CREATE VIRTUAL TABLE IF NOT EXISTS claims_fts USING fts5(
@@ -401,9 +405,19 @@ class _SchemaMixin:
401
405
  """
402
406
  )
403
407
 
404
- # Backfill: rebuild FTS index from existing claims data.
405
- # The 'rebuild' command re-reads all rows from the content table.
406
- conn.execute("INSERT INTO claims_fts(claims_fts) VALUES ('rebuild')")
408
+ # Backfill: rebuild the FTS index from existing claims data only when
409
+ # necessary. A full 'rebuild' is O(n) over every claim and the sync
410
+ # triggers above already keep the index current, so on warm DBs an
411
+ # unconditional rebuild is pure waste (54k+ claims). Rebuild only when
412
+ # the FTS table was just created, or when a row-count mismatch between
413
+ # `claims` and `claims_fts` reveals the index drifted.
414
+ needs_rebuild = not fts_existed
415
+ if not needs_rebuild:
416
+ claims_count = conn.execute("SELECT COUNT(*) FROM claims").fetchone()[0]
417
+ fts_count = conn.execute("SELECT COUNT(*) FROM claims_fts").fetchone()[0]
418
+ needs_rebuild = claims_count != fts_count
419
+ if needs_rebuild:
420
+ conn.execute("INSERT INTO claims_fts(claims_fts) VALUES ('rebuild')")
407
421
 
408
422
 
409
423
  @staticmethod
@@ -486,12 +500,26 @@ class _SchemaMixin:
486
500
 
487
501
  @staticmethod
488
502
  def _backfill_human_ids(conn: sqlite3.Connection) -> int:
489
- """Assign human_id to all claims that lack one."""
503
+ """Assign human_id to all claims that lack one.
504
+
505
+ Guard: only the claims with a NULL human_id are selected, so a warm DB
506
+ (all ids assigned) returns immediately. When there are zero
507
+ ``derived_from`` links the whole batch is top-level, so we skip the
508
+ per-row claim_links JOIN and generate ids in-memory + executemany.
509
+ """
490
510
  rows = conn.execute(
491
511
  "SELECT id, subject, text FROM claims WHERE human_id IS NULL ORDER BY id ASC"
492
512
  ).fetchall()
493
513
  if not rows:
494
514
  return 0
515
+
516
+ has_derived_links = conn.execute(
517
+ "SELECT 1 FROM claim_links WHERE link_type = 'derived_from' LIMIT 1"
518
+ ).fetchone() is not None
519
+
520
+ if not has_derived_links:
521
+ return _SchemaMixin._backfill_human_ids_top_level(conn, rows)
522
+
495
523
  updated = 0
496
524
  for row in rows:
497
525
  claim_id = int(row["id"])
@@ -505,6 +533,33 @@ class _SchemaMixin:
505
533
  updated += 1
506
534
  return updated
507
535
 
536
+ @staticmethod
537
+ def _backfill_human_ids_top_level(conn: sqlite3.Connection, rows: list) -> int:
538
+ """Batch-assign top-level human_ids (no derived_from parents present).
539
+
540
+ Collisions are resolved in-memory against ids already present in the DB
541
+ plus ids minted within this batch, then written via a single executemany.
542
+ """
543
+ taken: set[str] = {
544
+ str(r[0])
545
+ for r in conn.execute(
546
+ "SELECT human_id FROM claims WHERE human_id IS NOT NULL"
547
+ ).fetchall()
548
+ }
549
+ updates: list[tuple[str, int]] = []
550
+ for row in rows:
551
+ claim_id = int(row["id"])
552
+ candidate = generate_top_level_human_id(row["subject"], str(row["text"]))
553
+ final = candidate
554
+ suffix = 1
555
+ while final in taken:
556
+ suffix += 1
557
+ final = f"{candidate}~{suffix}"
558
+ taken.add(final)
559
+ updates.append((final, claim_id))
560
+ conn.executemany("UPDATE claims SET human_id = ? WHERE id = ?", updates)
561
+ return len(updates)
562
+
508
563
 
509
564
  @staticmethod
510
565
  def _allocate_human_id(
@@ -675,6 +730,20 @@ class _SchemaMixin:
675
730
 
676
731
  @staticmethod
677
732
  def _backfill_event_chain(conn: sqlite3.Connection, *, rebuild_all: bool = False) -> int:
733
+ # Fast path: when not rebuilding everything, skip the full-table scan
734
+ # entirely if every event already has an event_hash. The cheap probe
735
+ # uses idx_events_event_hash, so the common warm-DB init does no work.
736
+ if not rebuild_all:
737
+ pending = conn.execute(
738
+ "SELECT 1 FROM events WHERE event_hash IS NULL LIMIT 1"
739
+ ).fetchone()
740
+ if pending is None:
741
+ return 0
742
+
743
+ # Materialize the rows before issuing UPDATEs: SQLite forbids mutating
744
+ # a table while a read cursor over it is still being iterated, so we
745
+ # fetchall() here rather than streaming. The NULL-hash guard above
746
+ # already makes the warm-DB common case do zero work.
678
747
  rows = conn.execute(
679
748
  """
680
749
  SELECT id, claim_id, event_type, from_status, to_status, details, payload_json, created_at, event_hash, hash_algo
@@ -633,6 +633,16 @@ class _SourceItemsMixin:
633
633
  return []
634
634
  now = utc_now()
635
635
  with self.connect() as conn:
636
+ # BEGIN IMMEDIATE takes the write lock up front so the SELECT and
637
+ # the claiming UPDATE are one atomic read-modify-write. Without it,
638
+ # two concurrent fetchers can SELECT the same pending rows, both
639
+ # UPDATE (double-incrementing attempt_count, emitting duplicate
640
+ # events) and both receive the same media_key — breaking the
641
+ # single-claimer guarantee. The loser waits on busy_timeout instead
642
+ # of failing. The "AND status = 'pending'" on the UPDATE is the
643
+ # second guard: a row already moved out of 'pending' is never
644
+ # re-claimed even if a stale id slips through.
645
+ conn.execute("BEGIN IMMEDIATE")
636
646
  rows = conn.execute(
637
647
  """
638
648
  SELECT id FROM media_retry_queue
@@ -654,7 +664,8 @@ class _SourceItemsMixin:
654
664
  SET status = 'retrying',
655
665
  attempt_count = attempt_count + 1,
656
666
  updated_at = ?
657
- WHERE id IN ({placeholders})
667
+ WHERE status = 'pending'
668
+ AND id IN ({placeholders})
658
669
  """,
659
670
  [now, *ids],
660
671
  )
@@ -158,21 +158,44 @@ def resolve_conflict_pair(
158
158
 
159
159
 
160
160
  def _resolve_group_pairs(store, claims: list[Claim], limit: int) -> tuple[int, int, int]:
161
- """Resolve all pairs within a conflict group. Returns (evaluated, resolved, failed)."""
161
+ """Resolve a conflict group down to a single survivor. Returns (evaluated, resolved, failed).
162
+
163
+ MED audit fix: the previous loop only compared ADJACENT claims
164
+ (i vs i+1). In a 3+ group that left non-adjacent claims still
165
+ 'conflicted' even though the whole group was reported resolved — a
166
+ claim that lost to its neighbour was never compared to the eventual
167
+ survivor. We instead carry a running winner and judge it against each
168
+ remaining conflicted claim, so every loser is superseded by the actual
169
+ group survivor and at most one 'conflicted' claim remains.
170
+ """
162
171
  evaluated = 0
163
172
  resolved = 0
164
173
  failed = 0
165
174
 
166
- for i in range(len(claims) - 1):
175
+ # Running winner: the first still-conflicted claim, refreshed from store.
176
+ winner: Claim | None = None
177
+
178
+ for claim in claims:
167
179
  if evaluated >= limit:
168
180
  break
169
- # Re-fetch to check if still conflicted (might have been resolved in earlier pair)
170
- a = store.get_claim(claims[i].id, include_citations=True)
171
- b = store.get_claim(claims[i + 1].id, include_citations=True)
172
- if a is None or b is None or a.status != "conflicted" or b.status != "conflicted":
181
+
182
+ contender = store.get_claim(claim.id, include_citations=True)
183
+ if contender is None or contender.status != "conflicted":
184
+ continue
185
+
186
+ if winner is None:
187
+ winner = contender
188
+ continue
189
+
190
+ # Re-fetch the running winner: an earlier pair (or another writer)
191
+ # may have changed its status/version since we last saw it.
192
+ winner = store.get_claim(winner.id, include_citations=True)
193
+ if winner is None or winner.status != "conflicted":
194
+ # Running winner is gone; the current contender becomes the new winner.
195
+ winner = contender
173
196
  continue
174
197
 
175
- result = resolve_conflict_pair(store, a, b)
198
+ result = resolve_conflict_pair(store, winner, contender)
176
199
  evaluated += 1
177
200
  if result.get("resolved"):
178
201
  resolved += 1
@@ -180,6 +203,10 @@ def _resolve_group_pairs(store, claims: list[Claim], limit: int) -> tuple[int, i
180
203
  "Resolved conflict: winner=%d, loser=%d (%s)",
181
204
  result["winner_id"], result["loser_id"], result["reason"],
182
205
  )
206
+ # The survivor (whichever side the LLM kept) carries forward.
207
+ survivor_id = result["winner_id"]
208
+ survivor = store.get_claim(survivor_id, include_citations=True)
209
+ winner = survivor if survivor is not None else contender
183
210
  else:
184
211
  failed += 1
185
212
 
@@ -90,6 +90,14 @@ def _has_fts5_table(conn: sqlite3.Connection) -> bool:
90
90
  return row is not None
91
91
 
92
92
 
93
+ # Statuses a claim may have and still be a valid "canonical" target to dedupe
94
+ # a fresh candidate against. A retired (archived), replaced (superseded), or
95
+ # contested (conflicted) claim must NOT be treated as the canonical survivor —
96
+ # archiving a newer candidate in favour of one of those drops live information
97
+ # in favour of a dead/contested row.
98
+ _CANONICAL_DEDUPE_STATUSES = ("confirmed", "candidate", "stale")
99
+
100
+
93
101
  def fts_candidates_in_scope(
94
102
  conn: sqlite3.Connection,
95
103
  *,
@@ -101,7 +109,11 @@ def fts_candidates_in_scope(
101
109
  """Return list of (id, text, status) candidate matches via FTS5 OR-query.
102
110
 
103
111
  Empty list if FTS5 isn't present, scope is empty, or there are no matches.
104
- Excludes archived claims and the candidate itself.
112
+ Excludes the candidate itself and any claim whose status is not a valid
113
+ canonical-dedupe target (archived / superseded / conflicted) — MED audit
114
+ fix: a fresh candidate must never be archived as a duplicate of a retired,
115
+ replaced, or contested claim, which would drop the possibly-newer candidate
116
+ in favour of a dead one.
105
117
  """
106
118
  if not text or not text.strip() or not scope:
107
119
  return []
@@ -109,19 +121,20 @@ def fts_candidates_in_scope(
109
121
  return []
110
122
 
111
123
  fts_query = _escape_fts5_query(text)
124
+ status_placeholders = ", ".join("?" for _ in _CANONICAL_DEDUPE_STATUSES)
112
125
  rows = conn.execute(
113
- """
126
+ f"""
114
127
  SELECT c.id, c.text, c.status
115
128
  FROM claims c
116
129
  JOIN claims_fts ON claims_fts.rowid = c.id
117
130
  WHERE claims_fts MATCH ?
118
131
  AND c.scope = ?
119
132
  AND c.id <> ?
120
- AND c.status <> 'archived'
133
+ AND c.status IN ({status_placeholders})
121
134
  ORDER BY bm25(claims_fts) ASC
122
135
  LIMIT ?
123
136
  """,
124
- (fts_query, scope, exclude_id, limit),
137
+ (fts_query, scope, exclude_id, *_CANONICAL_DEDUPE_STATUSES, limit),
125
138
  ).fetchall()
126
139
 
127
140
  return [(int(r[0]), r[1] or "", r[2] or "") for r in rows]
@@ -187,6 +187,19 @@ def build_parser() -> argparse.ArgumentParser:
187
187
  query.add_argument("--auto-classify", action="store_true", help="Auto-classify query type and use optimal retrieval mode")
188
188
  query.add_argument("--explain", action="store_true", help="Show per-stage score attribution (relevance vs. boosts, floor-gate status) for each result")
189
189
 
190
+ recall_analysis = sub.add_parser(
191
+ "recall-analysis",
192
+ help="Explain WHY claims ranked where they did (per-component score breakdown)",
193
+ )
194
+ recall_analysis.add_argument("--query", required=True, help="Query text to analyze")
195
+ recall_analysis.add_argument("--mode", choices=list(RETRIEVAL_MODES), default="hybrid", help="Retrieval mode (legacy or hybrid)")
196
+ recall_analysis.add_argument("--limit", type=int, default=10, help="Maximum rows")
197
+ recall_analysis.add_argument("--profile", choices=list(RETRIEVAL_PROFILES), default=None, help="Per-query hybrid retrieval profile")
198
+ recall_analysis.add_argument("--include-candidates", action="store_true", help="Also analyze candidate (unverified) claims")
199
+ recall_analysis.add_argument("--allow-sensitive", action="store_true", help="Include claims that look sensitive")
200
+ recall_analysis.add_argument("--scope-allowlist", default="", help="Comma-separated scopes to include")
201
+ # JSON output uses the global --json/-j flag (dest=json_output).
202
+
190
203
  context = sub.add_parser("context", help="Pack relevant claims into a token-budgeted context block for AI agents")
191
204
  context.add_argument("text", help="Query text describing what context is needed")
192
205
  context.add_argument("--budget", type=int, default=4000, help="Maximum token budget (default: 4000)")
@@ -361,6 +374,14 @@ def build_parser() -> argparse.ArgumentParser:
361
374
  links_cmd.add_argument("claim_id", help="Claim numeric id or human_id")
362
375
  links_cmd.add_argument("--type", dest="link_type", choices=list(CLAIM_LINK_TYPES), default=None, help="Filter by link type")
363
376
 
377
+ paths_cmd = sub.add_parser("query-paths", help="BFS path query over claim links (provenance/conflict/impact)")
378
+ paths_cmd.add_argument("--claim-id", dest="claim_id", required=True, help="Start claim numeric id or human_id")
379
+ paths_cmd.add_argument("--edge-type", dest="edge_type", choices=list(CLAIM_LINK_TYPES), default=None, help="Filter traversal to one link type (default: all)")
380
+ paths_cmd.add_argument("--direction", choices=["in", "out", "both"], default="both", help="in=provenance, out=impact, both (default)")
381
+ paths_cmd.add_argument("--max-hops", dest="max_hops", type=int, default=2, help="BFS depth, clamped to 5 (default: 2)")
382
+ paths_cmd.add_argument("--include-stale", dest="include_stale", action="store_true", help="Include stale claims in results")
383
+ paths_cmd.add_argument("--include-conflicted", dest="include_conflicted", action="store_true", help="Include conflicted claims in results")
384
+
364
385
  resolve_conflicts_cmd = sub.add_parser("resolve-conflicts", help="Detect and auto-resolve conflicting claims (same subject+predicate, different object_value)")
365
386
  resolve_conflicts_cmd.add_argument("--dry-run", action="store_true", help="Detect conflicts but do not apply transitions")
366
387
  resolve_conflicts_cmd.add_argument("--limit", type=int, default=500, help="Maximum claims to scan for conflicts")
@@ -465,6 +486,12 @@ def build_parser() -> argparse.ArgumentParser:
465
486
  mine_rules_cmd.add_argument("--provider", default="claude_cli", help="LLM provider for this run (default: claude_cli)")
466
487
  mine_rules_cmd.add_argument("--reset", action="store_true", help="Clear the stored watermark before running (re-scan from the start)")
467
488
 
489
+ export_rules_cmd = sub.add_parser("export-rules", help="Export mined rule-shaped claims as json/csv/markdown (v3.28)")
490
+ export_rules_cmd.add_argument("--format", choices=["json", "csv", "markdown"], default="json", help="Output format (default: json)")
491
+ export_rules_cmd.add_argument("--min-confidence", dest="min_confidence", type=float, default=0.0, help="Only export rules at/above this confidence (default: 0.0)")
492
+ export_rules_cmd.add_argument("--status", default=None, help="Only export rules with this claim status (default: all statuses)")
493
+ export_rules_cmd.add_argument("--limit", type=int, default=500, help="Max rules to export (default: 500)")
494
+
468
495
  verbatim_clean = sub.add_parser("verbatim-cleanup", help="Dedup the verbatim archive + optionally purge pre-#128 capture-bug junk (v3.23)")
469
496
  verbatim_clean.add_argument("--analyze-only", dest="analyze_only", action="store_true", help="Report composition only; do not delete")
470
497
  verbatim_clean.add_argument("--apply", action="store_true", help="Actually delete (default is dry-run)")
@@ -231,6 +231,34 @@ def _handle_link_commands(args: argparse.Namespace, service, parser: argparse.Ar
231
231
  return 0
232
232
 
233
233
 
234
+ def _handle_query_paths(args: argparse.Namespace, service, parser: argparse.ArgumentParser, effective_db: str = "") -> int:
235
+ """Handle the query-paths subcommand: BFS path query over claim links."""
236
+ t0 = time.perf_counter()
237
+ rows = service.query_claim_paths(
238
+ args.claim_id,
239
+ edge_type=getattr(args, "edge_type", None),
240
+ direction=getattr(args, "direction", "both"),
241
+ max_hops=getattr(args, "max_hops", 2),
242
+ include_stale=getattr(args, "include_stale", False),
243
+ include_conflicted=getattr(args, "include_conflicted", False),
244
+ )
245
+ elapsed_ms = (time.perf_counter() - t0) * 1000
246
+ if args.json_output:
247
+ print(_json_envelope({"rows": len(rows), "paths": rows}, total=len(rows), query_ms=elapsed_ms))
248
+ return 0
249
+ if not rows:
250
+ print(f"No paths found from claim {args.claim_id}.")
251
+ return 0
252
+ print(f"claim {args.claim_id} ({getattr(args, 'direction', 'both')}, max {getattr(args, 'max_hops', 2)} hops)")
253
+ for row in rows:
254
+ claim = row["claim"]
255
+ chain = " > ".join(row.get("edge_chain", [])) or "?"
256
+ text = str(claim.get("text", ""))[:80]
257
+ print(f"{' ' * row['depth']}|-[{chain}] #{claim.get('id')} "
258
+ f"(conf={row.get('path_confidence', 0.0):.2f}) {text}")
259
+ return 0
260
+
261
+
234
262
  def _handle_stealth_status(args: argparse.Namespace, service, parser: argparse.ArgumentParser, effective_db: str) -> int:
235
263
  t0 = time.perf_counter()
236
264
  stealth_path = Path.cwd() / STEALTH_DB_NAME
@@ -852,10 +880,19 @@ def _print_score_explanation(breakdown: dict | None) -> None:
852
880
 
853
881
 
854
882
  def _handle_query(args: argparse.Namespace, service, parser: argparse.ArgumentParser, effective_db: str) -> int:
855
- resolve_allow_sensitive_access(allow_sensitive=args.allow_sensitive, context="cli.query")
883
+ allow_sensitive = resolve_allow_sensitive_access(
884
+ allow_sensitive=args.allow_sensitive, context="cli.query"
885
+ )
856
886
  if getattr(args, "as_of", ""):
857
887
  t0 = time.perf_counter()
858
888
  claims = service.store.query_as_of(args.as_of)
889
+ # Parity with the non-as-of path: never surface sensitive-visibility
890
+ # claims in plaintext unless allow_sensitive was actually granted.
891
+ if not allow_sensitive:
892
+ claims = [
893
+ c for c in claims
894
+ if (getattr(c, "visibility", "public") or "public").strip().lower() != "sensitive"
895
+ ]
859
896
  elapsed_ms = (time.perf_counter() - t0) * 1000
860
897
  if args.json_output:
861
898
  print(_json_envelope([_claim_to_dict(c) for c in claims], total=len(claims), query_ms=elapsed_ms))
@@ -901,6 +938,62 @@ def _handle_query(args: argparse.Namespace, service, parser: argparse.ArgumentPa
901
938
  return 0
902
939
 
903
940
 
941
+ def _print_recall_analysis(analysis: dict) -> None:
942
+ """Render the recall-analysis breakdown for humans (non-JSON path)."""
943
+ w = analysis.get("weights", {})
944
+ rw = w.get("retrieval_weights", {})
945
+ print(f"query: {analysis.get('query', '')!r} mode={analysis.get('mode')} "
946
+ f"profile={analysis.get('profile')} rows={analysis.get('rows', 0)}")
947
+ print(f"weights(l,c,f,v)=({rw.get('lexical')},{rw.get('confidence')},"
948
+ f"{rw.get('freshness')},{rw.get('vector')}) "
949
+ f"floor_ratio={w.get('boost_floor_ratio')} pinned_bonus={w.get('pinned_bonus')}")
950
+ if w.get("profile_override"):
951
+ print(f"profile_override: {w['profile_override']}")
952
+ for rank, entry in enumerate(analysis.get("results", []), start=1):
953
+ bd = entry.get("breakdown") or {}
954
+ comp = bd.get("components", {})
955
+ contrib = bd.get("contributions", {})
956
+ gate = "GATED" if bd.get("floor_gated") else "applied"
957
+ print(f"#{rank} claim={entry.get('claim_id')} ({entry.get('human_id')}) "
958
+ f"score={entry.get('score', 0.0):.3f} tier={entry.get('tier')} "
959
+ f"pinned={int(entry.get('pinned', False))}")
960
+ print(f" components: lex={comp.get('lexical', 0.0):.3f} "
961
+ f"conf={comp.get('confidence', 0.0):.3f} fresh={comp.get('freshness', 0.0):.3f} "
962
+ f"vec={comp.get('vector', 0.0):.3f}")
963
+ print(f" contributions: lex={contrib.get('lexical', 0.0):+.3f} "
964
+ f"vec={contrib.get('vector', 0.0):+.3f} conf={contrib.get('confidence', 0.0):+.3f} "
965
+ f"fresh={contrib.get('freshness', 0.0):+.3f} "
966
+ f"tier={contrib.get('tier_bonus', 0.0):+.3f} pin={contrib.get('pinned_bonus', 0.0):+.3f}")
967
+ print(f" relevance={bd.get('relevance_subtotal', 0.0):.3f} "
968
+ f"boosts={bd.get('boosts_subtotal', 0.0):+.3f} [{gate}] "
969
+ f"-> final={bd.get('final_score', 0.0):.3f}")
970
+ rankings = analysis.get("component_rankings", {})
971
+ if rankings:
972
+ print("component rankings (best-first claim ids):")
973
+ for comp_name, ids in rankings.items():
974
+ print(f" {comp_name}: {ids}")
975
+
976
+
977
+ def _handle_recall_analysis(args: argparse.Namespace, service, parser: argparse.ArgumentParser, effective_db: str) -> int:
978
+ resolve_allow_sensitive_access(allow_sensitive=args.allow_sensitive, context="cli.recall-analysis")
979
+ t0 = time.perf_counter()
980
+ analysis = service.recall_analysis(
981
+ query_text=args.query,
982
+ limit=args.limit,
983
+ retrieval_mode=args.mode,
984
+ include_candidates=getattr(args, "include_candidates", False),
985
+ retrieval_profile=getattr(args, "profile", None),
986
+ allow_sensitive=args.allow_sensitive,
987
+ scope_allowlist=parse_scope_allowlist(args.scope_allowlist),
988
+ )
989
+ elapsed_ms = (time.perf_counter() - t0) * 1000
990
+ if args.json_output:
991
+ print(_json_envelope(analysis, total=analysis.get("rows", 0), query_ms=elapsed_ms))
992
+ else:
993
+ _print_recall_analysis(analysis)
994
+ return 0
995
+
996
+
904
997
  def _handle_context(args: argparse.Namespace, service, parser: argparse.ArgumentParser, effective_db: str) -> int:
905
998
  resolve_allow_sensitive_access(allow_sensitive=args.allow_sensitive, context="cli.context")
906
999
  t0 = time.perf_counter()