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.
- {memorymaster-3.4.0 → memorymaster-3.5.0}/PKG-INFO +51 -1
- memorymaster-3.4.0/memorymaster.egg-info/PKG-INFO → memorymaster-3.5.0/README.md +42 -36
- memorymaster-3.5.0/artifacts/bm25-per-field-eval-harness.py +309 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/__init__.py +1 -1
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/cli.py +9 -1
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/cli_handlers_curation.py +70 -0
- memorymaster-3.5.0/memorymaster/config_templates/hooks/memorymaster-auto-ingest.py +222 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/hooks/memorymaster-classify.py +98 -10
- memorymaster-3.5.0/memorymaster/config_templates/hooks/memorymaster-dream-sync.py +27 -0
- memorymaster-3.5.0/memorymaster/config_templates/hooks/memorymaster-observe.py +39 -0
- memorymaster-3.5.0/memorymaster/config_templates/hooks/memorymaster-precompact.py +76 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/hooks/memorymaster-recall.py +8 -3
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/hooks/memorymaster-session-start.py +23 -2
- memorymaster-3.5.0/memorymaster/config_templates/hooks/memorymaster-steward-cycle.py +49 -0
- memorymaster-3.5.0/memorymaster/context_hook.py +1561 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/embeddings.py +23 -5
- memorymaster-3.5.0/memorymaster/entity_extractor.py +690 -0
- memorymaster-3.5.0/memorymaster/entity_registry.py +456 -0
- memorymaster-3.5.0/memorymaster/graph_store.py +570 -0
- memorymaster-3.5.0/memorymaster/hook_log.py +65 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/validator.py +41 -1
- memorymaster-3.5.0/memorymaster/key_rotator.py +176 -0
- memorymaster-3.5.0/memorymaster/llm_provider.py +469 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/mcp_server.py +45 -2
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/policy.py +23 -0
- memorymaster-3.5.0/memorymaster/qdrant_recall_fallback.py +305 -0
- memorymaster-3.5.0/memorymaster/query_expansion.py +184 -0
- memorymaster-3.5.0/memorymaster/recall_fusion.py +79 -0
- memorymaster-3.5.0/memorymaster/recall_tokenizer.py +305 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/security.py +139 -7
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/service.py +35 -7
- memorymaster-3.5.0/memorymaster/steward_classifier.py +149 -0
- memorymaster-3.5.0/memorymaster/steward_features.py +345 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_linter.py +81 -4
- memorymaster-3.5.0/memorymaster/verbatim_recall.py +296 -0
- memorymaster-3.5.0/memorymaster/wiki_freshness.py +228 -0
- memorymaster-3.5.0/memorymaster/wiki_similarity.py +621 -0
- memorymaster-3.4.0/README.md → memorymaster-3.5.0/memorymaster.egg-info/PKG-INFO +86 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster.egg-info/SOURCES.txt +70 -1
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster.egg-info/requires.txt +11 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster.egg-info/top_level.txt +1 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/pyproject.toml +13 -1
- memorymaster-3.5.0/scripts/agg_recall_latency.py +184 -0
- memorymaster-3.5.0/scripts/backfill_entity_extraction.py +327 -0
- memorymaster-3.5.0/scripts/backfill_graph_store.py +264 -0
- memorymaster-3.5.0/scripts/backfill_stop_hook_citations.py +137 -0
- memorymaster-3.5.0/scripts/backtest_steward_classifier.py +757 -0
- memorymaster-3.5.0/scripts/build_steward_training_set.py +119 -0
- memorymaster-3.5.0/scripts/check_hook_template_drift.py +143 -0
- memorymaster-3.5.0/scripts/eval_bm25_sweep.py +432 -0
- memorymaster-3.5.0/scripts/eval_classify_f1.py +226 -0
- memorymaster-3.5.0/scripts/eval_recall_precision_at_5.py +468 -0
- memorymaster-3.5.0/scripts/eval_recall_quality.py +132 -0
- memorymaster-3.5.0/scripts/eval_steward_pareto.py +273 -0
- memorymaster-3.5.0/scripts/eval_verbatim_recall.py +216 -0
- memorymaster-3.5.0/scripts/expand_recall_eval.py +404 -0
- memorymaster-3.5.0/scripts/index_claims_to_qdrant.py +315 -0
- memorymaster-3.5.0/scripts/merge_scope_variants.py +216 -0
- memorymaster-3.5.0/scripts/run_longmemeval.py +997 -0
- memorymaster-3.5.0/scripts/sync_hook_templates.py +117 -0
- memorymaster-3.5.0/scripts/train_steward_classifier.py +457 -0
- memorymaster-3.5.0/tests/integration/test_extract_llm_ollama_live.py +49 -0
- memorymaster-3.5.0/tests/test_auto_ingest_hook_citations.py +147 -0
- memorymaster-3.5.0/tests/test_auto_ingest_hook_schema.py +145 -0
- memorymaster-3.5.0/tests/test_bm25_per_field.py +222 -0
- memorymaster-3.5.0/tests/test_classify_hook_f1.py +85 -0
- memorymaster-3.5.0/tests/test_classify_hook_latency.py +91 -0
- memorymaster-3.5.0/tests/test_entity_extractor.py +150 -0
- memorymaster-3.5.0/tests/test_entity_extractor_llm.py +296 -0
- memorymaster-3.5.0/tests/test_entity_new_kinds.py +241 -0
- memorymaster-3.5.0/tests/test_entity_registry.py +406 -0
- memorymaster-3.5.0/tests/test_eval_harness.py +291 -0
- memorymaster-3.5.0/tests/test_extract_llm_ollama.py +140 -0
- memorymaster-3.5.0/tests/test_graph_distance.py +233 -0
- memorymaster-3.5.0/tests/test_graph_store.py +321 -0
- memorymaster-3.5.0/tests/test_key_rotator.py +159 -0
- memorymaster-3.5.0/tests/test_llm_fallback.py +219 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_mcp_helpers.py +93 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_operator.py +6 -0
- memorymaster-3.5.0/tests/test_policy_mode_env.py +91 -0
- memorymaster-3.5.0/tests/test_query_expansion.py +264 -0
- memorymaster-3.5.0/tests/test_recall_entity_fanout.py +459 -0
- memorymaster-3.5.0/tests/test_recall_fusion.py +94 -0
- memorymaster-3.5.0/tests/test_recall_latency.py +275 -0
- memorymaster-3.5.0/tests/test_recall_precision_at_5.py +261 -0
- memorymaster-3.5.0/tests/test_recall_tokenizer.py +234 -0
- memorymaster-3.5.0/tests/test_recall_vector_fallback.py +306 -0
- memorymaster-3.5.0/tests/test_rrf_auto_gate.py +223 -0
- memorymaster-3.5.0/tests/test_scope_boost.py +298 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_security_patterns.py +4 -1
- memorymaster-3.5.0/tests/test_sensitivity_filter_adversarial.py +93 -0
- memorymaster-3.5.0/tests/test_sensitivity_filter_adversarial_v2.py +153 -0
- memorymaster-3.5.0/tests/test_steward_classifier.py +261 -0
- memorymaster-3.5.0/tests/test_steward_features.py +174 -0
- memorymaster-3.5.0/tests/test_steward_features_v3.py +254 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_vector_search.py +3 -1
- memorymaster-3.5.0/tests/test_verbatim_recall.py +321 -0
- memorymaster-3.5.0/tests/test_wiki_freshness.py +281 -0
- memorymaster-3.5.0/tests/test_wiki_similarity_multiscope.py +270 -0
- memorymaster-3.4.0/memorymaster/config_templates/hooks/memorymaster-auto-ingest.py +0 -126
- memorymaster-3.4.0/memorymaster/config_templates/hooks/memorymaster-precompact.py +0 -35
- memorymaster-3.4.0/memorymaster/config_templates/hooks/memorymaster-steward-cycle.py +0 -28
- memorymaster-3.4.0/memorymaster/context_hook.py +0 -230
- memorymaster-3.4.0/memorymaster/entity_registry.py +0 -255
- memorymaster-3.4.0/memorymaster/llm_provider.py +0 -229
- {memorymaster-3.4.0 → memorymaster-3.5.0}/LICENSE +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/benchmarks/longmemeval_runner.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/benchmarks/longmemeval_vector_runner.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/benchmarks/perf_smoke.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/__main__.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/_storage_lifecycle.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/_storage_read.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/_storage_schema.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/_storage_shared.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/_storage_write_claims.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/access_control.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/auto_extractor.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/auto_resolver.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/claim_verifier.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/cli_handlers_basic.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/cli_helpers.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/claude-md-append.md +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/codex-agents-md-append.md +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/config_templates/hooks/memorymaster-validate-wiki.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/conflict_resolver.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/context_optimizer.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/daily_notes.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/dashboard.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/db_merge.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/dream_bridge.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/entity_graph.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/feedback.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/__init__.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/compact_summaries.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/compactor.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/decay.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/dedup.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/deterministic.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/extractor.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/jobs/staleness.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/lifecycle.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/llm_steward.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/metrics_exporter.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/models.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/operator.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/operator_queue.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/plugins.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/postgres_store.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/qdrant_backend.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/qmd_bridge.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/query_classifier.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/retrieval.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/retry.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/review.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/rl_trainer.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/scheduler.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/schema.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/schema.sql +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/schema_postgres.sql +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/session_tracker.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/setup_hooks.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/skill_evolver.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/snapshot.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/steward.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/storage.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/store_factory.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/transcript_miner.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/turn_schema.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_bases.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_curator.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_exporter.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_log.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_query_capture.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/vault_synthesis.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/verbatim_store.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/webhook.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster/wiki_engine.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster.egg-info/dependency_links.txt +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/memorymaster.egg-info/entry_points.txt +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/alert_operator_metrics.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/autoresearch_daemon.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/claude_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/codex_live_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/compaction_edge_cases.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/compaction_trace_report.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/compaction_trace_validate.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/confusion_matrix_eval.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/conversation_importer.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/conversation_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/e2e_operator.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/email_live_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/eval_memorymaster.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/generate_drill_signoff.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/git_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/github_live_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/gitnexus_to_claims.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/ingest_planning_docs.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/jira_live_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/llm_benchmark.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/messages_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/operator_metrics.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/recurring_incident_drill.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/release_readiness.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/run_codex_autologger.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/run_incident_drill.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/scheduled_ingest.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/setup-hooks.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/slack_live_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/tickets_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/scripts/webhook_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/setup.cfg +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/conftest.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_access_control.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_auto_extractor.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_auto_resolver.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_auto_validate.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_claim_links.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_claude_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_cli_json_flag.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_cli_ready.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_cli_review_queue.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_cli_subcommands.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_compact_summaries.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_compaction_trace.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_config.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_conflict_resolver.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_confusion_matrix_eval.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_connection_retry.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_connectors.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_context_hook.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_context_optimizer.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_conversation_to_turns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_dashboard.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_dedup.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_deterministic_predicates.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_embeddings_coverage.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_entity_graph.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_events_schema.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_feedback.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_fts5_search.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_handler_regressions.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_human_id.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_incident_drill_runner.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_integration_workflows.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_lifecycle.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_llm_steward_coverage.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_llm_steward_key_rotation.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_metrics_exporter.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_obsidian_mind_patterns.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_operator_queue.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_perf_smoke_config.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_plugins.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_policy_coverage.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_postgres_parity.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_qdrant_backend.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_qmd_bridge.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_query_classifier.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_reliability_hardening.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_review.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_rl_trainer.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_scheduler.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_schema.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_security_access.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_service_coverage.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_session_tracker.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_snapshot.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_sqlite_core.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_staleness.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_stealth_mode.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_steward.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_steward_resolution_parity.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_store_factory.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_tenant_isolation.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_turn_schema.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_vault_exporter.py +0 -0
- {memorymaster-3.4.0 → memorymaster-3.5.0}/tests/test_webhook.py +0 -0
- {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.
|
|
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())
|
|
@@ -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,
|