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.
- {memorymaster-3.26.0/memorymaster.egg-info → memorymaster-3.28.0}/PKG-INFO +2 -2
- {memorymaster-3.26.0 → memorymaster-3.28.0}/README.md +1 -1
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/__init__.py +1 -1
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_read.py +82 -40
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_schema.py +73 -4
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_sources.py +12 -1
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/auto_resolver.py +34 -7
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/candidate_dedupe.py +17 -4
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/cli.py +27 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/cli_handlers_basic.py +94 -1
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/cli_handlers_curation.py +46 -2
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config.py +0 -22
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-steward-cycle.py +5 -1
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/conflict_resolver.py +15 -8
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/context_hook.py +187 -30
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/contradiction_probe.py +20 -4
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/dashboard.py +30 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/db_merge.py +166 -18
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/dream_bridge.py +11 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/embeddings.py +19 -1
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/entity_graph.py +65 -27
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/entity_registry.py +32 -14
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/llm_provider.py +89 -15
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/llm_rerank.py +19 -34
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/llm_steward.py +172 -38
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/mcp_server.py +222 -135
- memorymaster-3.28.0/memorymaster/migrations/0006_verbatim_session_content_index.py +59 -0
- memorymaster-3.28.0/memorymaster/migrations/0007_rule_stats.py +54 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/postgres_store.py +15 -2
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/query_classifier.py +11 -7
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/recall_tokenizer.py +27 -3
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/retrieval.py +102 -9
- memorymaster-3.28.0/memorymaster/rule_export.py +157 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/rule_miner.py +134 -5
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/schema_postgres.sql +21 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/security.py +171 -2
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/service.py +333 -5
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/steward.py +21 -8
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/storage.py +6 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_linter.py +11 -13
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_query_capture.py +4 -3
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/verbatim_cleanup.py +46 -23
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/verbatim_store.py +13 -6
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/webhook.py +11 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/wiki_engine.py +72 -11
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/wiki_validate.py +2 -2
- {memorymaster-3.26.0 → memorymaster-3.28.0/memorymaster.egg-info}/PKG-INFO +2 -2
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster.egg-info/SOURCES.txt +31 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/pyproject.toml +1 -1
- memorymaster-3.28.0/scripts/archive_watchkeeper_heartbeats.py +81 -0
- memorymaster-3.28.0/tests/test_claim_paths.py +306 -0
- memorymaster-3.28.0/tests/test_conflict_resolver_extra.py +415 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_contradiction_probe.py +58 -0
- memorymaster-3.28.0/tests/test_db_merge_audit_fixes.py +184 -0
- memorymaster-3.28.0/tests/test_delta_sync_extra.py +392 -0
- memorymaster-3.28.0/tests/test_dream_bridge_subject_sensitivity.py +83 -0
- memorymaster-3.28.0/tests/test_embeddings_degraded_signal.py +53 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_graph.py +100 -0
- memorymaster-3.28.0/tests/test_entity_registry_merge_atomicity.py +94 -0
- memorymaster-3.28.0/tests/test_graph_harvest.py +348 -0
- memorymaster-3.28.0/tests/test_lifecycle_extra.py +314 -0
- memorymaster-3.28.0/tests/test_llm_concurrency_fixes.py +253 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_fallback.py +20 -5
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_provider_extra.py +53 -10
- memorymaster-3.28.0/tests/test_llm_steward_scope.py +191 -0
- memorymaster-3.28.0/tests/test_mcp_server_security_cluster.py +187 -0
- memorymaster-3.28.0/tests/test_misc_correctness_cluster.py +92 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_postgres_parity.py +110 -0
- memorymaster-3.28.0/tests/test_read_visibility_filter.py +156 -0
- memorymaster-3.28.0/tests/test_recall_analysis.py +306 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_tokenizer.py +70 -2
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_rrf_auto_gate.py +139 -0
- memorymaster-3.28.0/tests/test_rule_confidence_bootstrap.py +210 -0
- memorymaster-3.28.0/tests/test_rules_export.py +152 -0
- memorymaster-3.28.0/tests/test_service_embedding_toctou.py +109 -0
- memorymaster-3.28.0/tests/test_steward_resolvers_audit_fixes.py +236 -0
- memorymaster-3.28.0/tests/test_storage_concurrency.py +112 -0
- memorymaster-3.28.0/tests/test_storage_internals_audit.py +124 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_vault_linter_extra.py +17 -9
- memorymaster-3.28.0/tests/test_vault_linter_weak_link.py +43 -0
- memorymaster-3.28.0/tests/test_vault_query_capture_scope.py +38 -0
- memorymaster-3.28.0/tests/test_verbatim_perf_index.py +234 -0
- memorymaster-3.28.0/tests/test_wiki_backfill_bindings_backend.py +94 -0
- memorymaster-3.28.0/tests/test_wiki_engine_absorb_fixes.py +187 -0
- memorymaster-3.28.0/tests/test_wiki_validate_desc_bound.py +30 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/LICENSE +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/artifacts/bm25-per-field-eval-harness.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/benchmarks/longmemeval_runner.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/benchmarks/longmemeval_vector_runner.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/benchmarks/perf_smoke.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/__main__.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_lifecycle.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_shared.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/_storage_write_claims.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/access_control.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/action_exporters.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/action_extractor.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/atlas_claim_extractor.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/atlas_contract.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/auto_extractor.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/claim_edges.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/claim_verifier.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/cli_helpers.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/closets.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/claude-md-append.md +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/codex-agents-md-append.md +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-auto-ingest.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-classify.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-dream-sync.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-precompact.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-recall.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-session-start.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/config_templates/hooks/memorymaster-validate-wiki.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/connectors/__init__.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/connectors/whatsapp.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/context_optimizer.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/daily_notes.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/dashboard_auth.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/delta_sync.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/entity_extractor.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/federated_graphify.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/feedback.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/graph_store.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/hook_log.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/__init__.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/calibration.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/compact_summaries.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/compactor.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/daydream_ingest.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/decay.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/dedup.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/deterministic.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/entity_graph_export.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/extractor.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/staleness.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/jobs/validator.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/key_rotator.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/lifecycle.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/llm_budget.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/mcp_path_policy.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/mcp_usage.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/media_processing.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/media_providers.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/metrics_exporter.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/0001_initial.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/0002_miner_state.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/0003_contradiction_verdicts.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/0004_query_cache.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/__init__.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/migrations/runner.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/models.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/observability.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/operator.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/operator_queue.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/plugins.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/policy.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/qdrant_backend.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/qdrant_recall_fallback.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/qmd_bridge.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/query_cache.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/query_expansion.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/recall_fusion.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/retry.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/review.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/rl_trainer.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/rules.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/scheduler.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/schema.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/schema.sql +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/scope_utils.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/session_tracker.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/setup_hooks.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/skill_evolver.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/snapshot.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/steward_classifier.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/steward_features.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/store_factory.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/transcript_miner.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/turn_schema.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_bases.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_curator.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_exporter.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_log.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/vault_synthesis.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/verbatim_recall.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/wiki_freshness.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/wiki_similarity.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster/wiki_suggest.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster.egg-info/dependency_links.txt +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster.egg-info/entry_points.txt +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster.egg-info/requires.txt +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/memorymaster.egg-info/top_level.txt +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/agg_recall_latency.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/alert_operator_metrics.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/audit_dedupe_precision.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/autoresearch_daemon.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/backfill_entity_extraction.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/backfill_graph_store.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/backfill_stop_hook_citations.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/backtest_steward_classifier.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/build_steward_training_set.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/check_hook_template_drift.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/claude_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/codex_live_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/compaction_edge_cases.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/compaction_trace_report.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/compaction_trace_validate.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/confusion_matrix_eval.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/conversation_importer.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/conversation_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/e2e_operator.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/email_live_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_bm25_sweep.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_classify_f1.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_memorymaster.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_recall_precision_at_5.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_recall_quality.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_steward_pareto.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/eval_verbatim_recall.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/expand_recall_eval.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/generate_drill_signoff.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/git_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/github_live_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/gitnexus_to_claims.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/grid_recall_weights.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/index_claims_to_qdrant.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/ingest_planning_docs.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/jira_live_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/label_prompts_with_judge.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/llm_benchmark.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/measure_dedupe_thresholds.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/merge_scope_variants.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/messages_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/operator_metrics.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/precompute_candidates.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/recurring_incident_drill.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/release_readiness.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/run_codex_autologger.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/run_incident_drill.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/scheduled_ingest.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/setup-hooks.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/slack_live_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/sync_hook_templates.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/tickets_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/train_steward_classifier.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/scripts/webhook_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/setup.cfg +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/bench_longmemeval.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/conftest.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/integration/test_extract_llm_ollama_live.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_access_control.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_action_exporters.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_action_extractor.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_atlas_claim_extractor.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_atlas_contract.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_atlas_source_schema.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_auto_extractor.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_auto_ingest_hook_citations.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_auto_ingest_hook_schema.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_auto_resolver.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_auto_validate.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_backend_parity.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_bench_answer_prompt.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_bm25_per_field.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_calibration.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_calibration_priors_applied.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_candidate_dedupe.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_claim_edges.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_claim_links.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_claim_type_ranking.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_classify_hook_f1.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_classify_hook_latency.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_claude_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_dry_run.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_handlers_extra.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_json_flag.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_ready.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_review_queue.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_cli_subcommands.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_closets.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_closets_recall_integration.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_compact_summaries.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_compact_summaries_sensitivity.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_compaction_trace.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_compactor_artifact_order.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_config.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_conflict_resolver.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_confusion_matrix_eval.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_connection_retry.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_connectors.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_context_hook.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_context_hook_extra.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_context_optimizer.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_context_optimizer_provider.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_conversation_to_turns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard_auth.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard_coverage.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard_latency.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard_lineage.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dashboard_review_queue.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_daydream_ingest.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_db_merge_confidence_conflict.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_db_merge_coverage_v2.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_db_merge_extra.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_decay_coverage.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_decay_respects_pinned.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dedup.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dedup_cli.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dedup_conflict_disambiguation.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_delta_sync.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_deterministic_predicates.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dream_bridge_coverage_v2.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dream_bridge_extra.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_dream_bridge_sensitivity.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_embeddings_coverage.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_extractor.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_extractor_llm.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_graph_export.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_new_kinds.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_regex_v3.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_registry.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_entity_registry_extra.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_eval_harness.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_events_schema.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_extract_llm_ollama.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_federated_graphify_mcp.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_federated_query_safety.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_feedback.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_floor_gate.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_fts5_search.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_graph_distance.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_graph_store.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_handler_regressions.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_hook_env_isolation.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_human_id.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_incident_drill_runner.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_integration_workflows.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_key_rotator.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_lifecycle.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_lifecycle_supersede_invariant.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_budget.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_provider_claude_cli.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_provider_key_rotation.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_steward_coverage.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_llm_steward_key_rotation.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_filter_bypass.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_helpers.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_path_policy.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_rate_limit.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_server_validation.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_mcp_usage.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_media_processing.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_meta_decisions.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_metrics_exporter.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_migrations.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_observability.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_observability_extra.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_obsidian_mind_patterns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_operator.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_operator_queue.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_perf_smoke_config.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_plugins.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_policy_coverage.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_policy_mode_env.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_qdrant_backend.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_qmd_bridge.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_qrels_regression.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_query_cache.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_query_classifier.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_query_expansion.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_entity_fanout.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_fusion.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_latency.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_precision_at_5.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_recall_vector_fallback.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_reliability_hardening.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_resolvers_concurrent_supersede.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_retrieval_extra.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_retrieval_profile.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_retrieval_profiles.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_retrieval_rrf_tiebreaker.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_retrieval_weights.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_review.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_rl_trainer.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_rule_claims.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_rule_miner.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_scheduler.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_schema.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_scope_boost.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_scope_utils.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_security_access.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_security_patterns.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_sensitivity_filter_adversarial.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_sensitivity_filter_adversarial_v2.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_sensitivity_filter_t07.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_service_coverage.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_session_tracker.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_snapshot.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_snapshot_roundtrip.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_sqlite_core.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_staleness.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_stealth_mode.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_classifier.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_contradiction_phase.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_daydream_hook.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_features.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_features_v3.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_steward_resolution_parity.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_storage_extra.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_storage_parity.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_store_factory.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_tenant_isolation.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_turn_schema.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_two_pass_recall.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_v311_fixes.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_v313_e2e.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_v313_run_cycle_dedupe.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_v390_e2e.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_v391_strict_warnings.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_vault_exporter.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_vault_linter_orphan.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_vector_search.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_verbatim_cleanup.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_verbatim_dedup.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_verbatim_recall.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_verbatim_store.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_verbatim_store_qdrant.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_webhook.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_webhook_hmac.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_whatsapp_importer.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_autopromote.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_binding.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_engine_extra.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_engine_idempotency.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_explored_and_contradictions.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_freshness.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_similarity_multiscope.py +0 -0
- {memorymaster-3.26.0 → memorymaster-3.28.0}/tests/test_wiki_suggest.py +0 -0
- {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.
|
|
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)
|
|
54
54
|
[](https://www.python.org/downloads/)
|
|
55
|
-
[]()
|
|
56
56
|
[]()
|
|
57
57
|
[]()
|
|
58
58
|
[](https://pypi.org/project/memorymaster/)
|
|
@@ -6,7 +6,7 @@ Lifecycle-managed claims with citations, conflict detection, steward governance,
|
|
|
6
6
|
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
[](https://www.python.org/downloads/)
|
|
9
|
-
[]()
|
|
10
10
|
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[](https://pypi.org/project/memorymaster/)
|
|
@@ -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
|
|
39
|
+
"SELECT * FROM claims WHERE idempotency_key = ?",
|
|
38
40
|
(normalized_key,),
|
|
39
41
|
).fetchone()
|
|
40
|
-
if existing_row is
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
677
|
+
frontier.append((neighbor_id, 1, [start_claim_id, neighbor_id], link_type))
|
|
640
678
|
|
|
641
679
|
results: list[dict] = []
|
|
642
|
-
while
|
|
643
|
-
cid,
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
406
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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()
|