From 96387898d7725681bd734dbfb6c87fb2c919d2ec Mon Sep 17 00:00:00 2001 From: Autismo Date: Thu, 10 Sep 2026 22:23:01 +0200 Subject: [PATCH] Port Go searchConsistently cases into a C++ snapshot self-test Lift before/after search gates out of rag-service so ctest can assert stable snapshot, partial interleave, retry-fresh, rebuild bound, and hybrid identity without Mongo. --- godbrain_core/cpp_memory_store/CMakeLists.txt | 1 + godbrain_core/cpp_memory_store/README.md | 7 +- .../godbrain/memory_store/snapshot.hpp | 64 +++++ .../cpp_memory_store/src/rag_engine.cpp | 101 ++++---- .../cpp_memory_store/src/rag_snapshot.cpp | 243 ++++++++++++++++++ .../cpp_memory_store/tests/tests.cpp | 7 +- 6 files changed, 363 insertions(+), 60 deletions(-) create mode 100644 godbrain_core/cpp_memory_store/include/godbrain/memory_store/snapshot.hpp create mode 100644 godbrain_core/cpp_memory_store/src/rag_snapshot.cpp diff --git a/godbrain_core/cpp_memory_store/CMakeLists.txt b/godbrain_core/cpp_memory_store/CMakeLists.txt index 8dd87f4..270529c 100644 --- a/godbrain_core/cpp_memory_store/CMakeLists.txt +++ b/godbrain_core/cpp_memory_store/CMakeLists.txt @@ -23,6 +23,7 @@ add_library(cpp_memory_store_core src/state_machine.cpp src/store.cpp src/embedding.cpp + src/rag_snapshot.cpp ) target_include_directories(cpp_memory_store_core PUBLIC include diff --git a/godbrain_core/cpp_memory_store/README.md b/godbrain_core/cpp_memory_store/README.md index 27afabc..00fa79e 100644 --- a/godbrain_core/cpp_memory_store/README.md +++ b/godbrain_core/cpp_memory_store/README.md @@ -1,4 +1,4 @@ -# C++ Alexandria Memory Store (cut 12) +# C++ Alexandria Memory Store (cut 13) Same stdin JSON door as Go `memory-store.exe`. Go under `godbrain_core/memory_store/` stays as rollback. Start/Heal prefer `build/cpp_memory_store/Release` @@ -60,6 +60,11 @@ Cut 12: `rag-service` search retries once if generation/health moved mid-query (Go `searchConsistently`). Unready or hybrid-without-embeddings still fail closed on the first look. +Cut 13: offline snapshot self-test ports Go `http_test.go` searchConsistently +cases (stable snapshot, partial interleave, retry-fresh, rebuild bound, hybrid +identity). `search_before_gate` / `search_after_gate` are shared with +`rag-service`. + Start/Heal prefer this tree's Release exes, then Go. ```powershell diff --git a/godbrain_core/cpp_memory_store/include/godbrain/memory_store/snapshot.hpp b/godbrain_core/cpp_memory_store/include/godbrain/memory_store/snapshot.hpp new file mode 100644 index 0000000..c48e6e0 --- /dev/null +++ b/godbrain_core/cpp_memory_store/include/godbrain/memory_store/snapshot.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "godbrain/memory_store/embedding.hpp" + +#include +#include + +namespace godbrain::memory { + +constexpr int kMaxSearchAttempts = 2; + +struct SearchHealthSnap { + bool ready = false; + std::string mongo = "unavailable"; + std::string active_generation; + std::string building_generation; + std::string projection_version; + std::string projection_schema; + std::string indexer_version; + std::string retrieval_mode = "lexical"; + bool semantic_configured = false; + bool semantic_available = false; + bool semantic_required = false; + std::string degradation; + bool has_embedding = false; + EmbeddingIdentity embedding; + int64_t committed_runs = 0; + int64_t committed_nodes = 0; + int64_t committed_links = 0; + int64_t projected_nodes = 0; + int64_t projected_links = 0; + int64_t projected_embeddings = 0; + bool latest_committed = false; + bool latest_projected = false; + bool latest_embedded = false; + int64_t committed_at = 0; + int64_t projected_at = 0; + int64_t embedded_at = 0; +}; + +enum class BeforeSearch { Proceed, Unready, SemanticUnavailable }; +enum class AfterSearch { Accept, Retry }; + +BeforeSearch search_before_gate(const SearchHealthSnap& before, const std::string& requested_mode); + +bool same_search_snapshot( + const SearchHealthSnap& before, + const SearchHealthSnap& after, + const std::string& retrieval, + bool hybrid, + const std::string& degradation, + const EmbeddingIdentity* response_embedding); + +AfterSearch search_after_gate( + const SearchHealthSnap& before, + const SearchHealthSnap& after, + const std::string& retrieval, + bool hybrid, + const std::string& degradation, + const EmbeddingIdentity* response_embedding); + +int run_snapshot_self_test(); + +} // namespace godbrain::memory diff --git a/godbrain_core/cpp_memory_store/src/rag_engine.cpp b/godbrain_core/cpp_memory_store/src/rag_engine.cpp index 83af7ac..3721a47 100644 --- a/godbrain_core/cpp_memory_store/src/rag_engine.cpp +++ b/godbrain_core/cpp_memory_store/src/rag_engine.cpp @@ -2,6 +2,7 @@ #include "godbrain/memory_store/json.hpp" #include "godbrain/memory_store/protocol.hpp" +#include "godbrain/memory_store/snapshot.hpp" #include #include @@ -702,59 +703,35 @@ HttpResponse handle_health(RagEngine* e) { return json_status(h.ready ? 200 : 503, health_json(h)); } -bool same_corpus_counts(const RagCorpusCounts& a, const RagCorpusCounts& b) { - return a.committed_runs == b.committed_runs && a.committed_nodes == b.committed_nodes && - a.committed_links == b.committed_links && a.projected_nodes == b.projected_nodes && - a.projected_links == b.projected_links && a.projected_embeddings == b.projected_embeddings; -} - -bool same_semantic_capability(const HealthSnap& a, const HealthSnap& b) { - if (a.semantic_configured != b.semantic_configured || a.semantic_available != b.semantic_available || - a.semantic_required != b.semantic_required || a.degradation != b.degradation) { - return false; - } - if (a.meta.has_embedding != b.meta.has_embedding) return false; - if (!a.meta.has_embedding) return true; - return embedding_identity_equal(a.meta.embedding, b.meta.embedding); -} - -bool same_optional_millis(bool has_a, int64_t a, bool has_b, int64_t b) { - if (has_a != has_b) return false; - return !has_a || a == b; -} - -bool valid_response_capability( - const HealthSnap& health, const std::string& retrieval, bool hybrid, const std::string& degradation) { - if (retrieval == "lexical") return !hybrid; - if (retrieval == "hybrid") { - return health.semantic_available && health.meta.has_embedding && hybrid && degradation.empty(); - } - return false; -} - -bool same_search_snapshot( - const HealthSnap& before, - const HealthSnap& after, - const std::string& retrieval, - bool hybrid, - const std::string& degradation) { - return before.ready && after.ready && before.mongo == "ok" && after.mongo == "ok" && - !before.meta.active_generation.empty() && - before.meta.active_generation == after.meta.active_generation && - before.meta.building_generation == after.meta.building_generation && - !before.meta.projection_version.empty() && - before.meta.projection_version == after.meta.projection_version && - before.meta.projection_schema == after.meta.projection_schema && - before.meta.indexer_version == after.meta.indexer_version && - before.retrieval_mode == after.retrieval_mode && same_semantic_capability(before, after) && - same_corpus_counts(before.counts, after.counts) && - same_optional_millis( - before.latest_committed, before.committed_at, after.latest_committed, after.committed_at) && - same_optional_millis( - before.latest_projected, before.projected_at, after.latest_projected, after.projected_at) && - same_optional_millis( - before.latest_embedded, before.embedded_at, after.latest_embedded, after.embedded_at) && - valid_response_capability(before, retrieval, hybrid, degradation); +SearchHealthSnap to_search_snap(const HealthSnap& h) { + SearchHealthSnap s; + s.ready = h.ready; + s.mongo = h.mongo; + s.active_generation = h.meta.active_generation; + s.building_generation = h.meta.building_generation; + s.projection_version = h.meta.projection_version; + s.projection_schema = h.meta.projection_schema; + s.indexer_version = h.meta.indexer_version; + s.retrieval_mode = h.retrieval_mode; + s.semantic_configured = h.semantic_configured; + s.semantic_available = h.semantic_available; + s.semantic_required = h.semantic_required; + s.degradation = h.degradation; + s.has_embedding = h.meta.has_embedding; + s.embedding = h.meta.embedding; + s.committed_runs = h.counts.committed_runs; + s.committed_nodes = h.counts.committed_nodes; + s.committed_links = h.counts.committed_links; + s.projected_nodes = h.counts.projected_nodes; + s.projected_links = h.counts.projected_links; + s.projected_embeddings = h.counts.projected_embeddings; + s.latest_committed = h.latest_committed; + s.latest_projected = h.latest_projected; + s.latest_embedded = h.latest_embedded; + s.committed_at = h.committed_at; + s.projected_at = h.projected_at; + s.embedded_at = h.embedded_at; + return s; } std::string utf8_snip(const std::string& content, const std::vector& tokens, int max_bytes) { @@ -1108,12 +1085,18 @@ HttpResponse handle_search(RagEngine* e, const HttpRequest& req) { } // Do not return hits if generation or corpus counts moved before after-health. - for (int attempt = 0; attempt < 2; ++attempt) { + for (int attempt = 0; attempt < kMaxSearchAttempts; ++attempt) { HealthSnap before; std::string herr; - if (!fill_health(e, &before, &herr) || !before.ready) { - if (mode == "hybrid" && !before.semantic_available) return api_error(503, "semantic_unavailable"); - return api_error(503, "search_unavailable"); + if (!fill_health(e, &before, &herr)) return api_error(503, "search_unavailable"); + switch (search_before_gate(to_search_snap(before), mode)) { + case BeforeSearch::Proceed: + break; + case BeforeSearch::SemanticUnavailable: + return api_error(503, "semantic_unavailable"); + case BeforeSearch::Unready: + default: + return api_error(503, "search_unavailable"); } std::string retrieval = "lexical"; @@ -1409,7 +1392,9 @@ HttpResponse handle_search(RagEngine* e, const HttpRequest& req) { HealthSnap after; std::string aerr; if (!fill_health(e, &after, &aerr)) return api_error(503, "search_unavailable"); - if (same_search_snapshot(before, after, retrieval, hybrid, degradation)) { + const EmbeddingIdentity* resp_emb = hybrid ? &before.meta.embedding : nullptr; + if (search_after_gate(to_search_snap(before), to_search_snap(after), retrieval, hybrid, degradation, resp_emb) == + AfterSearch::Accept) { return json_status(200, o.str()); } } diff --git a/godbrain_core/cpp_memory_store/src/rag_snapshot.cpp b/godbrain_core/cpp_memory_store/src/rag_snapshot.cpp new file mode 100644 index 0000000..f77d4a8 --- /dev/null +++ b/godbrain_core/cpp_memory_store/src/rag_snapshot.cpp @@ -0,0 +1,243 @@ +#include "godbrain/memory_store/snapshot.hpp" + +#include +#include + +namespace godbrain::memory { +namespace { + +bool same_counts(const SearchHealthSnap& a, const SearchHealthSnap& b) { + return a.committed_runs == b.committed_runs && a.committed_nodes == b.committed_nodes && + a.committed_links == b.committed_links && a.projected_nodes == b.projected_nodes && + a.projected_links == b.projected_links && a.projected_embeddings == b.projected_embeddings; +} + +bool same_semantic(const SearchHealthSnap& a, const SearchHealthSnap& b) { + if (a.semantic_configured != b.semantic_configured || a.semantic_available != b.semantic_available || + a.semantic_required != b.semantic_required || a.degradation != b.degradation) { + return false; + } + if (a.has_embedding != b.has_embedding) return false; + if (!a.has_embedding) return true; + return embedding_identity_equal(a.embedding, b.embedding); +} + +bool same_optional_millis(bool has_a, int64_t a, bool has_b, int64_t b) { + if (has_a != has_b) return false; + return !has_a || a == b; +} + +bool valid_response_capability( + const SearchHealthSnap& health, + const std::string& retrieval, + bool hybrid, + const std::string& degradation, + const EmbeddingIdentity* response_embedding) { + if (retrieval == "lexical") return !hybrid && response_embedding == nullptr; + if (retrieval == "hybrid") { + return health.semantic_available && health.has_embedding && hybrid && degradation.empty() && + response_embedding != nullptr && embedding_identity_equal(health.embedding, *response_embedding); + } + return false; +} + +SearchHealthSnap ready_health(const char* generation, int64_t nodes, int64_t links) { + SearchHealthSnap h; + h.ready = true; + h.mongo = "ok"; + h.active_generation = generation; + h.projection_version = "hybrid-v1"; + h.projection_schema = "rag-document-v2"; + h.indexer_version = "mongodb-text-v1"; + h.retrieval_mode = "lexical"; + h.degradation = "embedding_provider_disabled"; + h.committed_runs = nodes; + h.committed_nodes = nodes; + h.committed_links = links; + h.projected_nodes = nodes; + h.projected_links = links; + return h; +} + +struct Scripted { + std::vector healths; + std::vector snippets; + std::string mode = "auto"; + std::string retrieval = "lexical"; + bool hybrid = false; + std::string degradation = "embedding_provider_disabled"; +}; + +struct Outcome { + bool ok = false; + bool semantic = false; + std::string snippet; + int health_calls = 0; + int search_calls = 0; +}; + +Outcome run_scripted(const Scripted& sc) { + Outcome o; + size_t hi = 0; + for (int attempt = 0; attempt < kMaxSearchAttempts; ++attempt) { + if (hi >= sc.healths.size()) return o; + const SearchHealthSnap& before = sc.healths[hi++]; + ++o.health_calls; + const BeforeSearch gate = search_before_gate(before, sc.mode); + if (gate == BeforeSearch::SemanticUnavailable) { + o.semantic = true; + return o; + } + if (gate != BeforeSearch::Proceed) return o; + if (static_cast(o.search_calls) >= sc.snippets.size()) return o; + const std::string snip = sc.snippets[static_cast(o.search_calls)]; + ++o.search_calls; + if (hi >= sc.healths.size()) return o; + const SearchHealthSnap& after = sc.healths[hi++]; + ++o.health_calls; + const EmbeddingIdentity* emb = sc.hybrid ? &before.embedding : nullptr; + if (search_after_gate(before, after, sc.retrieval, sc.hybrid, sc.degradation, emb) == AfterSearch::Accept) { + o.ok = true; + o.snippet = snip; + return o; + } + } + return o; +} + +} // namespace + +BeforeSearch search_before_gate(const SearchHealthSnap& before, const std::string& requested_mode) { + if (before.ready) return BeforeSearch::Proceed; + if (requested_mode == "hybrid" && !before.semantic_available) return BeforeSearch::SemanticUnavailable; + return BeforeSearch::Unready; +} + +bool same_search_snapshot( + const SearchHealthSnap& before, + const SearchHealthSnap& after, + const std::string& retrieval, + bool hybrid, + const std::string& degradation, + const EmbeddingIdentity* response_embedding) { + return before.ready && after.ready && before.mongo == "ok" && after.mongo == "ok" && + !before.active_generation.empty() && before.active_generation == after.active_generation && + before.building_generation == after.building_generation && !before.projection_version.empty() && + before.projection_version == after.projection_version && + before.projection_schema == after.projection_schema && before.indexer_version == after.indexer_version && + before.retrieval_mode == after.retrieval_mode && same_semantic(before, after) && same_counts(before, after) && + same_optional_millis( + before.latest_committed, before.committed_at, after.latest_committed, after.committed_at) && + same_optional_millis( + before.latest_projected, before.projected_at, after.latest_projected, after.projected_at) && + same_optional_millis( + before.latest_embedded, before.embedded_at, after.latest_embedded, after.embedded_at) && + valid_response_capability(before, retrieval, hybrid, degradation, response_embedding); +} + +AfterSearch search_after_gate( + const SearchHealthSnap& before, + const SearchHealthSnap& after, + const std::string& retrieval, + bool hybrid, + const std::string& degradation, + const EmbeddingIdentity* response_embedding) { + if (same_search_snapshot(before, after, retrieval, hybrid, degradation, response_embedding)) { + return AfterSearch::Accept; + } + return AfterSearch::Retry; +} + +int run_snapshot_self_test() { + int failed = 0; + auto check = [&](bool ok, const char* name) { + if (!ok) { + std::fprintf(stderr, "FAIL %s\n", name); + ++failed; + } + }; + + const SearchHealthSnap stable = ready_health("generation-a", 1, 1); + { + Scripted sc; + sc.healths = {stable, stable}; + sc.snippets = {""}; + const Outcome o = run_scripted(sc); + check(o.ok && o.health_calls == 2 && o.search_calls == 1, "stable-snapshot"); + } + + { + SearchHealthSnap partial = ready_health("generation-a", 2, 2); + partial.projected_links = 1; + partial.ready = false; + Scripted sc; + sc.healths = {stable, partial, partial}; + sc.snippets = {"partial projection must not escape"}; + const Outcome o = run_scripted(sc); + check(!o.ok && o.search_calls == 1 && o.health_calls == 3 && o.snippet.empty(), "partial-interleave"); + } + + { + const SearchHealthSnap older = ready_health("generation-a", 1, 1); + const SearchHealthSnap newer = ready_health("generation-a", 2, 2); + Scripted sc; + sc.healths = {older, newer, newer, newer}; + sc.snippets = {"discard me", "return me"}; + const Outcome o = run_scripted(sc); + check(o.ok && o.snippet == "return me" && o.health_calls == 4 && o.search_calls == 2, "retry-fresh"); + } + + { + const SearchHealthSnap a = ready_health("generation-a", 1, 1); + const SearchHealthSnap b = ready_health("generation-b", 1, 1); + const SearchHealthSnap c = ready_health("generation-c", 1, 1); + Scripted sc; + sc.healths = {a, b, b, c}; + sc.snippets = {"", ""}; + const Outcome o = run_scripted(sc); + check(!o.ok && o.health_calls == 4 && o.search_calls == kMaxSearchAttempts, "rebuild-bound"); + } + + { + SearchHealthSnap unready; + unready.ready = false; + Scripted sc; + sc.healths = {unready}; + sc.snippets = {"must not search"}; + const Outcome o = run_scripted(sc); + check(!o.ok && o.search_calls == 0 && !o.semantic, "unready-first"); + } + + { + SearchHealthSnap h; + h.ready = false; + h.semantic_available = false; + Scripted sc; + sc.mode = "hybrid"; + sc.healths = {h}; + sc.snippets = {"must not search"}; + const Outcome o = run_scripted(sc); + check(!o.ok && o.semantic && o.search_calls == 0, "hybrid-semantic-unavail"); + } + + { + EmbeddingIdentity id = embedding_fake_identity(32); + SearchHealthSnap health = ready_health("generation-a", 1, 1); + health.retrieval_mode = "hybrid"; + health.semantic_configured = true; + health.semantic_available = true; + health.degradation.clear(); + health.has_embedding = true; + health.embedding = id; + health.projected_embeddings = 1; + check(same_search_snapshot(health, health, "hybrid", true, "", &id), "hybrid-identity-ok"); + EmbeddingIdentity mismatched = id; + mismatched.model_revision = "other"; + check(!same_search_snapshot(health, health, "hybrid", true, "", &mismatched), "hybrid-identity-mismatch"); + } + + if (failed == 0) std::fprintf(stderr, "cpp_memory_store snapshot self-test ok\n"); + return failed == 0 ? 0 : 1; +} + +} // namespace godbrain::memory diff --git a/godbrain_core/cpp_memory_store/tests/tests.cpp b/godbrain_core/cpp_memory_store/tests/tests.cpp index fdae8e4..bcb5299 100644 --- a/godbrain_core/cpp_memory_store/tests/tests.cpp +++ b/godbrain_core/cpp_memory_store/tests/tests.cpp @@ -1,3 +1,8 @@ #include "godbrain/memory_store/protocol.hpp" +#include "godbrain/memory_store/snapshot.hpp" -int main() { return godbrain::memory::run_self_test(); } +int main() { + const int protocol = godbrain::memory::run_self_test(); + const int snapshot = godbrain::memory::run_snapshot_self_test(); + return (protocol == 0 && snapshot == 0) ? 0 : 1; +}