yams/tests/unit/search/search_engine_topology_catch2_test.cpp
Trevon 3606914222
test(search): characterize the vector stage (#297)
Pin the current behaviour of the vector candidate stage in
search_engine.cpp before it is extracted into search_vector_stage.{h,cpp}.
A new catch2_search_vector_stage binary runs the matrix policy
{Shadow, Augment, Narrow commit, Narrow fallback} x backend
{SimeonPqAdc, ExactScan, Vec0L2} with stage trace off and on, plus one
executor-backed fanout case, and compares each cell against explicit
goldens: fused hash/rank/score and vector contribution, raw vector
ComponentResults (trace_component_hits_json), the stage's debug keys
without timings, certificate.work / admission.work via the
topology_route_work_* and admission keys, and the merged vector_search_*
diagnostics. Quirks are pinned as-is: relaxed_vector_primary_hit_count is
last-writer, Vec0 keeps global results unchanged under Shadow/Augment,
Narrow commits only on Simeon PQ because ExactScan and Vec0 never observe
an ANN candidate budget (work_observation_unavailable), and Vec0 leaks a
partial ann_candidate_budget_actual into the unobserved work record. A
hidden [.dump] case regenerates the goldens.

Move the shared topology fixtures (TopologySearchFixture,
buildTwoClusterTopologyBatch, topologyRoutingTestConfig,
runTopologySearch, configureCertifiedTopologyRoute, TopologyVectorEnvPin)
into tests/unit/search/topology_search_test_support.h so both tests use
the same corpus. runTopologySearch takes an optional executor and
configureCertifiedTopologyRoute an options struct; existing call sites are
unchanged. The portability allowlist entry for the raw YAMS_TEST_TMPDIR
read moves with the helper. No product behaviour changes.

Signed-off-by: trvon <me@trevon.dev>
2026-10-14 08:47:00 -06:00

2321 lines
110 KiB
C++

#include <nlohmann/json.hpp>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <yams/metadata/connection_pool.h>
#include <yams/metadata/knowledge_graph_store.h>
#include <yams/metadata/metadata_repository.h>
#include <yams/search/search_engine.h>
#include <yams/search/search_execution_context.h>
#include <yams/search/search_topology_stage.h>
#include <yams/search/topology_routing_session.h>
#include <yams/topology/topology_baseline.h>
#include <yams/topology/topology_metadata_store.h>
#include <yams/vector/embedding_generator.h>
#include <yams/vector/vector_database.h>
#include "src/search/search_vector_pipeline_internal.h"
#include "tests/common/test_helpers_catch2.h"
#include "tests/unit/search/topology_search_test_support.h"
using namespace yams;
using namespace yams::metadata;
using namespace yams::search;
using namespace yams::search::topology_test_support;
// Catch2 assertion macros expand through do/while and large generated test bodies.
// NOLINTBEGIN(cppcoreguidelines-avoid-do-while, readability-function-cognitive-complexity)
namespace {
class HashLookupCountingRepository : public MetadataRepository {
public:
explicit HashLookupCountingRepository(ConnectionPool& pool) : MetadataRepository(pool) {}
Result<std::optional<DocumentInfo>> getDocumentByHash(const std::string& hash) override {
++singleLookups;
return MetadataRepository::getDocumentByHash(hash);
}
Result<std::unordered_map<std::string, DocumentInfo>>
batchGetDocumentsByHash(const std::vector<std::string>& hashes) override {
const auto call = ++batchLookups;
if (failBatchLookups || (failBatchAt > 0 && call == failBatchAt)) {
return Error{ErrorCode::InternalError, "injected metadata lookup failure"};
}
return MetadataRepository::batchGetDocumentsByHash(hashes);
}
std::atomic<int> singleLookups{0};
std::atomic<int> batchLookups{0};
bool failBatchLookups{false};
int failBatchAt{0};
};
yams::topology::TopologyArtifactBatch buildMultiscaleOverlapTopologyBatch() {
auto batch = buildTwoClusterTopologyBatch();
auto fineIt = std::ranges::find_if(batch.clusters, [](const auto& cluster) {
return std::ranges::find(cluster.memberDocumentHashes, "y1") !=
cluster.memberDocumentHashes.end();
});
REQUIRE(fineIt != batch.clusters.end());
yams::topology::ClusterArtifact coarse = *fineIt;
coarse.clusterId += ":coarse";
coarse.level = 1;
coarse.persistenceScore = 0.70;
coarse.cohesionScore = 0.80;
coarse.bridgeMass = 0.30;
coarse.densityScore = 0.60;
coarse.parentClusterId.reset();
coarse.overlapClusterIds = {fineIt->clusterId};
if (coarse.medoid.has_value()) {
coarse.medoid->clusterId = coarse.clusterId;
}
fineIt->parentClusterId = coarse.clusterId;
fineIt->overlapClusterIds = {coarse.clusterId};
for (auto& membership : batch.memberships) {
if (membership.documentHash != "y1" && membership.documentHash != "y2") {
continue;
}
membership.parentClusterId = coarse.clusterId;
membership.overlapClusterIds = {coarse.clusterId};
membership.bridgeScore = membership.documentHash == "y1" ? 0.60 : 0.20;
}
batch.clusters.push_back(std::move(coarse));
return batch;
}
void seedTopologyWithStaleClusterMember(TopologySearchFixture& fix) {
auto topologyBatch = buildTwoClusterTopologyBatch();
REQUIRE(!topologyBatch.clusters.empty());
topologyBatch.clusters.front().memberDocumentHashes.push_back("missing-hash");
topologyBatch.clusters.front().memberCount =
topologyBatch.clusters.front().memberDocumentHashes.size();
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(topologyBatch).has_value());
}
} // namespace
TEST_CASE("SearchEngine topology refuses uncertified weak-query narrowing",
"[search][topology][admission][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::Vec0L2};
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
fix.addDocument("x2", "alpha two", {0.9F, 0.1F});
fix.addDocument("y1", "omega one", {0.0F, 1.0F});
fix.addDocument("y2", "omega two", {0.1F, 0.9F});
yams::topology::ConnectedComponentTopologyEngine topologyEngine;
yams::topology::TopologyBuildConfig topologyConfig;
topologyConfig.reciprocalOnly = true;
topologyConfig.minEdgeScore = 0.5;
std::vector<yams::topology::TopologyDocumentInput> topologyDocs{
{.documentHash = "x1",
.filePath = "/tmp/x1.txt",
.embedding = {1.0F, 0.0F},
.neighbors = {{.documentHash = "x2", .score = 0.9F, .reciprocal = true}}},
{.documentHash = "x2",
.filePath = "/tmp/x2.txt",
.embedding = {0.9F, 0.1F},
.neighbors = {{.documentHash = "x1", .score = 0.9F, .reciprocal = true}}},
{.documentHash = "y1",
.filePath = "/tmp/y1.txt",
.embedding = {0.0F, 1.0F},
.neighbors = {{.documentHash = "y2", .score = 0.9F, .reciprocal = true}}},
{.documentHash = "y2",
.filePath = "/tmp/y2.txt",
.embedding = {0.1F, 0.9F},
.neighbors = {{.documentHash = "y1", .score = 0.9F, .reciprocal = true}}},
};
auto topologyBatch = topologyEngine.buildArtifacts(topologyDocs, topologyConfig);
REQUIRE(topologyBatch.has_value());
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(topologyBatch.value()).has_value());
vector::EmbeddingConfig embeddingConfig;
embeddingConfig.embedding_dim = 2;
auto generator = std::make_shared<vector::EmbeddingGenerator>(
std::make_unique<FixedEmbeddingBackend>(std::vector<float>{0.0F, 1.0F}), embeddingConfig);
REQUIRE(generator->initialize());
SearchEngineConfig config;
config.includeDebugInfo = true;
config.enableParallelExecution = true;
config.textWeight = 1.0F;
config.simeonTextWeight = 0.0F;
config.pathTreeWeight = 0.0F;
config.kgWeight = 0.0F;
config.vectorWeight = 1.0F;
config.entityVectorWeight = 0.0F;
config.tagWeight = 0.0F;
config.metadataWeight = 0.0F;
config.vectorOnlyThreshold = 0.0F;
config.vectorOnlyPenalty = 1.0F;
config.vectorMaxResults = 4;
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Narrow;
config.topologyMaxClusters = 1;
config.topologyMaxDocs = 2;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("unmatched query", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("search_pipeline_name") == "topology_evidence"));
REQUIRE(debug.contains("topology_weak_query_applied"));
CHECK((debug.at("topology_weak_query_load_attempted") == "1"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "1"));
CHECK(debug.at("topology_weak_query_skip_reason").empty());
CHECK((debug.at("topology_weak_query_applied") == "1"));
CHECK((debug.at("topology_route_narrow_proposed") == "1"));
CHECK((debug.at("topology_route_admission_eligible") == "0"));
CHECK((debug.at("topology_route_admission_denial_reason") ==
"protected_fibers_representation_unavailable"));
CHECK((debug.at("topology_weak_query_narrow_applied") == "0"));
CHECK((debug.at("topology_weak_query_allowed_candidates") == "2"));
CHECK((debug.at("topology_vector_filter_applied") == "0"));
CHECK((debug.at("topology_vector_filter_fallback") == "0"));
CHECK((debug.at("topology_vector_allowed_set_ann_applied") == "0"));
CHECK((debug.at("topology_vector_allowed_set_ann_fallback") == "0"));
CHECK_FALSE(debug.contains("topology_vector_partition_ann_applied"));
CHECK((debug.at("topology_vector_filter_matched") == "0"));
CHECK((debug.at("topology_vector_filter_removed") == "0"));
CHECK_FALSE(debug.contains("topology_vector_scores_reused"));
CHECK_FALSE(debug.contains("topology_vector_scores_reused_count"));
CHECK_FALSE(debug.contains("topology_member_rerank_rows_visited_actual"));
CHECK_FALSE(debug.contains("topology_member_rerank_distance_evaluations_actual"));
CHECK(debug.at("vector_search_rows_visited_status") == "unavailable");
CHECK(debug.at("vector_search_exact_distance_evaluations_status") == "unavailable");
CHECK(debug.at("vector_search_ann_candidate_budget_status") == "unavailable");
CHECK_FALSE(debug.contains("vector_search_rows_visited_actual"));
CHECK_FALSE(debug.contains("vector_search_exact_distance_evaluations_actual"));
CHECK_FALSE(debug.contains("vector_search_ann_candidate_budget_actual"));
CHECK((debug.at("topology_weak_query_routed_clusters") == "1"));
CHECK((debug.at("topology_weak_query_added_candidates") == "0"));
CHECK((debug.at("topology_weak_query_total_candidates") == "0"));
CHECK((debug.at("topology_snapshot_cache_hit") == "0"));
auto cachedResponse = engine.searchWithResponse("unmatched query", params);
REQUIRE(cachedResponse.has_value());
CHECK((cachedResponse.value().debugStats.at("topology_snapshot_cache_hit") == "1"));
}
TEST_CASE("SearchEngine keeps Simeon PQ global while route admission is unavailable",
"[search][topology][spq][admission][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.1F, 0.9F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.topologyMaxDocs = 1;
auto response = runTopologySearch(fix, generator, config, 4);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_weak_query_allowed_candidates") == "2");
CHECK(debug.at("topology_route_narrow_proposed") == "1");
CHECK(debug.at("topology_route_admission_eligible") == "0");
CHECK(debug.at("topology_route_admission_denial_reason") ==
"protected_fibers_representation_unavailable");
CHECK(debug.at("topology_vector_allowed_set_ann_applied") == "0");
CHECK(debug.at("topology_vector_allowed_set_ann_fallback") == "0");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("vector_search_rows_visited_status") == "observed");
CHECK(debug.at("vector_search_exact_distance_evaluations_status") == "observed");
CHECK(debug.at("vector_search_ann_candidate_budget_status") == "observed");
CHECK(debug.at("vector_search_ann_candidate_budget_actual") == "4");
CHECK(debug.at("vector_search_rows_visited_actual") == "4");
CHECK(debug.at("vector_search_exact_distance_evaluations_actual") == "4");
REQUIRE_FALSE(response.value().results.empty());
}
TEST_CASE("SearchEngine commits hard narrowing when every theorem certificate passes",
"[search][topology][admission][work][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(batch).has_value());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
configureCertifiedTopologyRoute(config, batch);
auto response = runTopologySearch(fix, generator, config, 4, "omega");
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_coordinate_space_identity") == "test-space-v1");
CHECK(debug.at("topology_route_coordinate_space_alignment_status") == "satisfied");
CHECK(debug.at("topology_route_protected_relation_coverage_status") == "satisfied");
CHECK(debug.at("topology_route_protected_fibers_represented_status") == "satisfied");
CHECK(debug.at("topology_route_certificate_saturates_protected_fibers_status") == "satisfied");
CHECK(debug.at("topology_route_risk_status") == "satisfied");
CHECK(debug.at("topology_route_work_status") == "satisfied");
CHECK(debug.at("topology_route_cover_materialization_status") == "satisfied");
CHECK(debug.at("topology_route_admission_eligible") == "1");
CHECK(debug.at("topology_weak_query_narrow_applied") == "1");
CHECK(debug.at("topology_vector_allowed_set_ann_applied") == "1");
CHECK(debug.at("topology_vector_global_fill_count") == "0");
REQUIRE(response.value().results.size() == 2U);
CHECK(std::ranges::all_of(response.value().results, [](const auto& result) {
return result.document.sha256Hash == "y1" || result.document.sha256Hash == "y2";
}));
}
TEST_CASE("SearchEngine discards a trial route whose observed vector work exceeds its budget",
"[search][topology][admission][work][fallback][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(batch).has_value());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
configureCertifiedTopologyRoute(config, batch, 1);
auto response = runTopologySearch(fix, generator, config, 4, "omega");
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_work_status") == "violated");
CHECK(debug.at("topology_route_admission_eligible") == "0");
CHECK(debug.at("topology_route_admission_denial_reason") == "work_budget_violated");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_vector_filter_fallback") == "1");
CHECK(response.value().results.size() == 4U);
}
TEST_CASE("SearchEngine rejects a relation cover that cuts an observed protected pair",
"[search][topology][admission][pairs][fallback][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
auto selectedChart = std::ranges::find_if(batch.clusters, [](const auto& cluster) {
return std::ranges::find(cluster.memberDocumentHashes, "y1") !=
cluster.memberDocumentHashes.end();
});
REQUIRE(selectedChart != batch.clusters.end());
REQUIRE(selectedChart->protectedPairCount > 0);
selectedChart->preservedProtectedPairCount = 0;
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(batch).has_value());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
configureCertifiedTopologyRoute(config, batch);
auto response = runTopologySearch(fix, generator, config, 4, "omega");
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_protected_relation_coverage_status") == "violated");
CHECK(debug.at("topology_route_admission_denial_reason") ==
"protected_relation_coverage_violated");
CHECK(debug.at("topology_route_work_status") == "unavailable");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(response.value().results.size() == 4U);
}
TEST_CASE("SearchEngine rejects route calibration from a different topology construction",
"[search][topology][admission][risk][fallback][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(batch).has_value());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
configureCertifiedTopologyRoute(config, batch);
config.topologyRouteRiskCalibration.constructionFingerprint = "different-construction";
auto response = runTopologySearch(fix, generator, config, 4, "omega");
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_protected_relation_coverage_status") == "satisfied");
CHECK(debug.at("topology_route_risk_status") == "unavailable");
CHECK(debug.at("topology_route_admission_denial_reason") ==
"protected_fibers_representation_unavailable");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(response.value().results.size() == 4U);
}
TEST_CASE("SearchEngine does not infer protected fiber representation from bounded miss risk",
"[search][topology][admission][fiber][fallback][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(batch).has_value());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
configureCertifiedTopologyRoute(config, batch);
config.topologyRouteRiskCalibration.missedProtectedCandidates = 1;
auto response = runTopologySearch(fix, generator, config, 4, "omega");
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_risk_status") == "satisfied");
CHECK(debug.at("topology_route_protected_fibers_represented_status") == "violated");
CHECK(debug.at("topology_route_admission_denial_reason") == "protected_fibers_unrepresented");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
}
TEST_CASE("SearchEngine keeps multi-route proposals global until admitted",
"[search][topology][spq][balance][admission][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.topologyMinClusters = 2;
config.topologyMaxClusters = 2;
config.topologyMaxDocs = 2;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 2);
REQUIRE(response.has_value());
REQUIRE(response.value().results.size() == 2U);
std::unordered_set<std::string> resultHashes;
for (const auto& result : response.value().results) {
resultHashes.insert(result.document.sha256Hash);
}
CHECK_FALSE((resultHashes.contains("x1") || resultHashes.contains("x2")));
CHECK((resultHashes.contains("y1") || resultHashes.contains("y2")));
CHECK(response.value().debugStats.at("topology_route_narrow_proposed") == "1");
CHECK(response.value().debugStats.at("topology_route_admission_eligible") == "0");
}
TEST_CASE("SearchEngine does not bound Simeon to an uncertified route",
"[search][topology][spq][fill][admission][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.topologyMinClusters = 1;
config.topologyMaxClusters = 1;
config.topologyMaxDocs = 4;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 4);
REQUIRE(response.has_value());
CHECK(response.value().results.size() == 4U);
CHECK(response.value().debugStats.at("topology_vector_global_fill_count") == "0");
CHECK(response.value().debugStats.at("vector_search_ann_candidate_budget_actual") == "4");
CHECK(response.value().debugStats.at("topology_route_admission_eligible") == "0");
}
TEST_CASE("SearchEngine uses global Vec0 directly for an uncertified route",
"[search][topology][vec0][fill][admission][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::Vec0L2};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.topologyMinClusters = 1;
config.topologyMaxClusters = 1;
config.topologyMaxDocs = 4;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 4);
REQUIRE(response.has_value());
CHECK(response.value().results.size() == 4U);
CHECK(response.value().debugStats.at("topology_vector_global_fill_count") == "0");
CHECK(response.value().debugStats.at("topology_route_admission_eligible") == "0");
}
TEST_CASE("SearchEngine topology abstains from narrowing at an ambiguous route boundary",
"[search][topology][spq][confidence][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({1.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologySparseDenseAlpha = 0.0F;
config.topologyMinClusters = 1;
config.topologyMaxClusters = 1;
config.topologyNarrowMinBoundaryMargin = 0.20F;
config.topologyMaxDocs = 2;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 4);
REQUIRE(response.has_value());
CHECK(response.value().results.size() == 4U);
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_confidence_abstained") == "1");
CHECK(debug.at("topology_weak_query_skip_reason") == "route_low_confidence");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_vector_allowed_set_ann_applied") == "0");
CHECK(debug.at("vector_search_ann_candidate_budget_actual") == "4");
}
TEST_CASE("SearchEngine topology shadow leaves global ranking and scores unchanged",
"[search][topology][shadow][catch2]") {
yams::test::ScopedEnvVar stageTrace("YAMS_SEARCH_STAGE_TRACE", std::optional<std::string>("1"));
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
SearchExecutionContextGuard contextGuard(context);
auto globalConfig = topologyRoutingTestConfig(false);
SearchEngine globalEngine(fix.repo, fix.vectorDb, generator, fix.kgStore, globalConfig);
auto shadowConfig = topologyRoutingTestConfig(true);
shadowConfig.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
shadowConfig.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Shadow;
shadowConfig.topologySparseDenseAlpha = 0.0F;
shadowConfig.topologyNarrowMinBoundaryMargin = 0.0F;
shadowConfig.topologyRouteWorkBudget.maxRowsVisited = 4;
shadowConfig.topologyRouteWorkBudget.maxExactDistanceEvaluations = 4;
shadowConfig.topologyRouteWorkBudget.maxAnnCandidates = 4;
SearchEngine shadowEngine(fix.repo, fix.vectorDb, generator, fix.kgStore, shadowConfig);
SearchParams params;
params.limit = 4;
auto global = globalEngine.searchWithResponse("unmatched query", params);
auto shadow = shadowEngine.searchWithResponse("unmatched query", params);
REQUIRE(global.has_value());
REQUIRE(shadow.has_value());
REQUIRE(shadow.value().results.size() == global.value().results.size());
bool scoreChanged = false;
for (std::size_t index = 0; index < global.value().results.size(); ++index) {
CHECK(shadow.value().results[index].document.sha256Hash ==
global.value().results[index].document.sha256Hash);
scoreChanged = scoreChanged ||
shadow.value().results[index].score != global.value().results[index].score;
}
CHECK_FALSE(scoreChanged);
const auto& debug = shadow.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "shadow");
CHECK(debug.at("topology_shadow_evaluated") == "1");
CHECK(debug.at("topology_shadow_proposed_action") == "narrow");
CHECK(debug.at("topology_shadow_retained_candidates") == "2");
CHECK(debug.at("topology_shadow_removed_candidates") == "2");
CHECK(debug.at("topology_shadow_retained_candidate_doc_ids") == "y1\ty2");
CHECK(debug.at("topology_shadow_removed_candidate_doc_ids") == "x1\tx2");
CHECK(debug.at("topology_route_work_status") == "satisfied");
CHECK(debug.at("topology_route_work_observed") == "1");
CHECK(debug.at("topology_route_work_rows_visited_actual") == "2");
CHECK(debug.at("topology_route_work_exact_distance_evaluations_actual") == "2");
CHECK(debug.at("topology_route_work_ann_candidate_budget_actual") == "2");
CHECK(debug.at("topology_weak_query_applied") == "0");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_weak_query_added_candidates") == "0");
CHECK(debug.at("topology_vector_filter_applied") == "0");
CHECK(debug.at("topology_vector_allowed_set_ann_applied") == "0");
}
TEST_CASE("SearchEngine topology augmentation materializes query-scored rescue candidates",
"[search][topology][candidate-rescue][catch2]") {
yams::test::ScopedEnvVar stageTrace("YAMS_SEARCH_STAGE_TRACE", std::optional<std::string>("1"));
for (const auto searchEngine :
{vector::VectorSearchEngine::SimeonPqAdc, vector::VectorSearchEngine::ExactScan}) {
CAPTURE(searchEngine);
TopologySearchFixture fix{searchEngine};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Augment;
config.topologySparseDenseAlpha = 1.0F;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.textMaxResults = 1;
config.vectorMaxResults = 1;
config.enableWeakQueryFanoutBoost = false;
config.topologyMaxDocs = 2;
config.topologyExpansionOutputLimit = 2;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 4, "alpha one");
REQUIRE(response.has_value());
std::vector<std::string> resultHashes;
for (const auto& result : response.value().results) {
resultHashes.push_back(result.document.sha256Hash);
}
CHECK(std::ranges::find(resultHashes, "x2") != resultHashes.end());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "augment");
CHECK(debug.at("topology_expansion_output_budget") == "2");
CHECK(debug.at("vector_search_result_budget") == "1");
CHECK(debug.at("topology_candidate_rescue_attempted") == "1");
CHECK(debug.at("topology_candidate_rescue_applied") == "1");
CHECK(debug.at("topology_candidate_rescue_added_candidates") == "2");
CHECK(debug.at("topology_candidate_rescue_novel_candidates") == "1");
CHECK(debug.at("topology_candidate_rescue_evidence_rescues") == "1");
CHECK(debug.at("topology_candidate_rescue_added_candidate_hashes") == "x2\tx1");
CHECK(debug.at("topology_candidate_rescue_added_candidate_doc_ids") == "x2\tx1");
CHECK(debug.at("topology_candidate_rescue_novel_candidate_doc_ids") == "x2");
CHECK(debug.at("topology_candidate_rescue_evidence_rescue_doc_ids") == "x1");
}
}
TEST_CASE("Topology selector resolves candidate hashes to metadata IDs, not trace IDs",
"[search][topology][candidate-rescue][simeon][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
fix.addDocument("31715818", "alpha one", {1.0F, 0.0F});
fix.addDocument("30655442", "beta two", {0.0F, 1.0F});
const std::vector<std::string> hashes = {"30655442", "missing", "31715818"};
auto resolved = yams::search::detail::resolveMetadataDocumentIdsByHash(fix.repo, hashes);
REQUIRE(resolved.has_value());
REQUIRE(resolved.value().size() == 2U);
auto first = fix.repo->getDocumentByHash("30655442");
auto second = fix.repo->getDocumentByHash("31715818");
REQUIRE(first.has_value());
REQUIRE(first.value().has_value());
REQUIRE(second.has_value());
REQUIRE(second.value().has_value());
CHECK(resolved.value().at(0).first == "30655442");
CHECK(resolved.value().at(0).second == first.value()->id);
CHECK(resolved.value().at(1).first == "31715818");
CHECK(resolved.value().at(1).second == second.value()->id);
}
TEST_CASE("SearchEngine shadow policy measures candidate rescue without changing results",
"[search][topology][candidate-rescue][shadow][catch2]") {
yams::test::ScopedEnvVar stageTrace("YAMS_SEARCH_STAGE_TRACE", std::optional<std::string>("1"));
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Shadow;
config.topologySparseDenseAlpha = 1.0F;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.textMaxResults = 1;
config.vectorMaxResults = 1;
config.topologyMaxDocs = 2;
config.topologyExpansionOutputLimit = 2;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 4, "alpha one");
REQUIRE(response.has_value());
std::vector<std::string> resultHashes;
for (const auto& result : response.value().results) {
resultHashes.push_back(result.document.sha256Hash);
}
CHECK(std::ranges::find(resultHashes, "x2") == resultHashes.end());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "shadow");
CHECK(debug.at("topology_candidate_rescue_attempted") == "1");
CHECK(debug.at("topology_candidate_rescue_applied") == "0");
CHECK(debug.at("topology_candidate_rescue_added_candidates") == "2");
CHECK(debug.at("topology_candidate_rescue_novel_candidates") == "1");
CHECK(debug.at("topology_candidate_rescue_evidence_rescues") == "1");
CHECK(debug.at("topology_candidate_rescue_added_candidate_hashes") == "x2\tx1");
CHECK(debug.at("topology_candidate_rescue_added_candidate_doc_ids") == "x2\tx1");
CHECK(debug.at("topology_candidate_rescue_novel_candidate_doc_ids") == "x2");
CHECK(debug.at("topology_candidate_rescue_evidence_rescue_doc_ids") == "x1");
CHECK(debug.at("topology_candidate_rescue_exact_scored_candidate_doc_ids") == "x1\tx2");
const auto trace =
nlohmann::json::parse(debug.at("topology_candidate_rescue_exact_scored_candidate_rows"));
REQUIRE(trace.contains("vector"));
const auto& hits = trace.at("vector").at("top_hits");
REQUIRE(hits.size() == 2U);
CHECK(hits.at(0).at("doc_id") == "x2");
CHECK(hits.at(0).at("rank") == 1U);
CHECK(hits.at(0).at("score").get<double>() < 0.5);
CHECK(hits.at(1).at("doc_id") == "x1");
CHECK(debug.at("topology_candidate_rescue_exact_candidate_hashes_requested") == "2");
CHECK(debug.at("topology_candidate_rescue_exact_rows_visited_status") == "observed");
CHECK(debug.at("topology_candidate_rescue_exact_rows_visited") == "2");
CHECK(debug.at("topology_candidate_rescue_exact_distance_evaluations_status") == "observed");
CHECK(debug.at("topology_candidate_rescue_exact_distance_evaluations") == "2");
CHECK(debug.at("topology_candidate_rescue_exact_returned_rows") == "2");
CHECK(debug.at("topology_candidate_rescue_exact_matched_documents") == "2");
CHECK(debug.at("topology_candidate_rescue_exact_matched_document_hashes") == "x1\tx2");
CHECK(debug.at("topology_candidate_rescue_exact_matched_document_doc_ids") == "x1\tx2");
}
TEST_CASE("SearchEngine topology final-window rescue promotes a topology-scored candidate",
"[search][topology][candidate-rescue][final-rescue][catch2]") {
yams::test::ScopedEnvVar stageTrace("YAMS_SEARCH_STAGE_TRACE", std::optional<std::string>("1"));
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
// x2 is topology-reachable from x1 (see seedTwoClusterTopology) but its real vector embedding
// is deliberately dissimilar to the query, mirroring the q0/q37 boundary: relation-reachable
// and document-scored, but losing on raw fused score alone.
fix.addDocument("x2", "alpha two", {0.1F, 0.9F});
// x3 is a strong organic match (like x1) but has no topology relation at all, so it competes
// for the same final-window slot as x2 without ever receiving topology evidence.
fix.addDocument("x3", "alpha one", {0.95F, 0.05F});
fix.addDocument("y1", "omega one", {0.0F, 1.0F});
fix.addDocument("y2", "omega two", {0.1F, 0.9F});
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({1.0F, 0.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Augment;
config.topologySparseDenseAlpha = 1.0F;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.textMaxResults = 2;
config.vectorMaxResults = 2;
config.enableWeakQueryFanoutBoost = false;
config.topologyMaxDocs = 2;
config.topologyExpansionOutputLimit = 2;
config.similarityThreshold = -1.0F;
// Default: no final-window rescue. The window's 2 slots go to the strong organic matches
// (x1, x3); the weak, topology-only x2 is excluded even though it was admitted and scored as
// a candidate.
auto baseline = runTopologySearch(fix, generator, config, 2, "alpha one");
REQUIRE(baseline.has_value());
REQUIRE(baseline.value().results.size() == 2);
std::vector<std::string> baselineHashes;
for (const auto& r : baseline.value().results) {
baselineHashes.push_back(r.document.sha256Hash);
}
CHECK(std::ranges::find(baselineHashes, "x1") != baselineHashes.end());
CHECK(std::ranges::find(baselineHashes, "x3") != baselineHashes.end());
CHECK(std::ranges::find(baselineHashes, "x2") == baselineHashes.end());
CHECK(baseline.value().debugStats.at("topology_final_rescue_enabled") == "0");
// Enabling the topology final-window rescue seam and requiring both window slots to carry
// topology evidence promotes x2 (which has it) over x3 (which does not), independent of the
// generic evidence-rescue family (fusionEvidenceRescueSlots), which stays disabled here.
config.topologyFinalRescueSlots = 2;
auto rescued = runTopologySearch(fix, generator, config, 2, "alpha one");
REQUIRE(rescued.has_value());
REQUIRE(rescued.value().results.size() == 2);
std::vector<std::string> rescuedHashes;
for (const auto& r : rescued.value().results) {
rescuedHashes.push_back(r.document.sha256Hash);
}
CHECK(std::ranges::find(rescuedHashes, "x1") != rescuedHashes.end());
CHECK(std::ranges::find(rescuedHashes, "x2") != rescuedHashes.end());
CHECK(std::ranges::find(rescuedHashes, "x3") == rescuedHashes.end());
const auto& debug = rescued.value().debugStats;
CHECK(debug.at("topology_final_rescue_enabled") == "1");
CHECK(debug.at("topology_final_rescue_slots") == "2");
CHECK(debug.at("topology_final_rescue_promoted_doc_ids") == "x2");
CHECK(debug.at("topology_final_rescue_displaced_doc_ids") == "x3");
CHECK(debug.at("topology_final_rescue_eligible_window_doc_ids").empty());
CHECK(debug.at("topology_final_rescue_eligible_tail_doc_ids") == "x2");
// x1 is organically found (not a novel rescue) but still shares the topology route with x2,
// so it also receives nonzero topologyScore -- a magnitude-only gate would have considered it
// just as eligible as x2. It must never appear as promoted/displaced: the eligibility set is
// scoped to novel candidates only, not every topology-touched one.
const auto x1Result = std::ranges::find_if(
rescued.value().results, [](const auto& r) { return r.document.sha256Hash == "x1"; });
REQUIRE(x1Result != rescued.value().results.end());
CHECK(x1Result->topologyScore.has_value());
CHECK(debug.at("topology_final_rescue_promoted_doc_ids").find("x1") == std::string::npos);
CHECK(debug.at("topology_final_rescue_displaced_doc_ids").find("x1") == std::string::npos);
}
namespace {
// x2 shares a topology route with the lexical seed x1, but lexical Tier-1 keeps only x1
// (textMaxResults=1) and the global vector top-1 is y1 (query embedding {0,1}). x2 is therefore
// reachable only through topology routing.
SearchEngineConfig routedOnlyCandidateConfig(SearchEngineConfig::TopologyVectorPolicy policy) {
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyVectorPolicy = policy;
config.topologySparseDenseAlpha = 1.0F;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.textMaxResults = 1;
config.vectorMaxResults = 1;
config.enableWeakQueryFanoutBoost = false;
config.topologyMaxDocs = 2;
config.topologyExpansionOutputLimit = 2;
config.similarityThreshold = -1.0F;
return config;
}
TopologyAssistStageResult
runRoutedOnlyAssistStage(TopologySearchFixture& fix,
SearchEngineConfig::TopologyVectorPolicy policy) {
TopologyAssistStageRequest request;
request.query = "alpha one";
request.config = routedOnlyCandidateConfig(policy);
request.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.weakTier1Query = true;
request.tier1SeedHashes = {"x1"};
request.tier1SeedEvidence = {{.documentHash = "x1", .weight = 1.0F}};
request.existingCandidateHashes = {"x1"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.queryEmbeddingSpaceIdentity = "test-space-v1";
request.metadataRepo = fix.repo;
request.kgStore = fix.kgStore;
return runTopologyAssistStage(request);
}
} // namespace
TEST_CASE("Topology assist stage reports routed members added under augment only",
"[search][topology][augment][catch2]") {
const TopologyVectorEnvPin vectorEnv;
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
SECTION("augment counts allowed members that are new to the candidate pool") {
const auto stage =
runRoutedOnlyAssistStage(fix, SearchEngineConfig::TopologyVectorPolicy::Augment);
const auto& session = stage.session;
REQUIRE(session.applied);
CHECK(stage.skipReason.empty());
CHECK(session.certificate.allowedDocumentHashes ==
std::unordered_set<std::string>{"x1", "x2"});
CHECK(session.routedDocs == 2U);
CHECK(session.addedCandidates == 1U);
CHECK(session.duplicateCandidates == 1U);
CHECK(session.addedCandidateHashes == std::vector<std::string>{"x2"});
}
for (const auto policy : {SearchEngineConfig::TopologyVectorPolicy::Narrow,
SearchEngineConfig::TopologyVectorPolicy::Shadow}) {
DYNAMIC_SECTION("policy " << SearchEngineConfig::topologyVectorPolicyToString(policy)
<< " keeps the allowed set without claiming additions") {
const auto stage = runRoutedOnlyAssistStage(fix, policy);
const auto& session = stage.session;
REQUIRE(session.applied);
CHECK(session.certificate.allowedDocumentHashes ==
std::unordered_set<std::string>{"x1", "x2"});
CHECK(session.routedDocs == 2U);
CHECK(session.addedCandidates == 0U);
CHECK(session.duplicateCandidates == 0U);
CHECK(session.addedCandidateHashes.empty());
}
}
}
TEST_CASE("SearchEngine topology augment adds a routed-only document to the fused results",
"[search][topology][augment][catch2]") {
const TopologyVectorEnvPin vectorEnv;
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto disabledConfig =
routedOnlyCandidateConfig(SearchEngineConfig::TopologyVectorPolicy::Shadow);
disabledConfig.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::Disabled;
auto disabled = runTopologySearch(fix, generator, disabledConfig, 4, "alpha one");
REQUIRE(disabled.has_value());
const auto disabledHashes = resultHashes(disabled.value());
// Precondition: without topology the routed-only document is unreachable.
REQUIRE(std::ranges::find(disabledHashes, "x2") == disabledHashes.end());
SECTION("augment") {
auto response = runTopologySearch(
fix, generator,
routedOnlyCandidateConfig(SearchEngineConfig::TopologyVectorPolicy::Augment), 4,
"alpha one");
REQUIRE(response.has_value());
const auto routed = std::ranges::find_if(
response.value().results, [](const auto& r) { return r.document.sha256Hash == "x2"; });
REQUIRE(routed != response.value().results.end());
CHECK(routed->score > 0.0);
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "augment");
CHECK(debug.at("topology_weak_query_applied") == "1");
CHECK(debug.at("topology_weak_query_skip_reason").empty());
CHECK(debug.at("topology_weak_query_added_candidates") == "1");
CHECK(debug.at("topology_weak_query_duplicate_candidates") == "1");
CHECK(debug.at("topology_weak_query_total_candidates") == "2");
CHECK(debug.at("topology_candidate_rescue_applied") == "1");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
}
SECTION("shadow matches topology-disabled results") {
auto response = runTopologySearch(
fix, generator,
routedOnlyCandidateConfig(SearchEngineConfig::TopologyVectorPolicy::Shadow), 4,
"alpha one");
REQUIRE(response.has_value());
REQUIRE(response.value().results.size() == disabled.value().results.size());
for (std::size_t i = 0; i < response.value().results.size(); ++i) {
CHECK(response.value().results[i].document.sha256Hash ==
disabled.value().results[i].document.sha256Hash);
CHECK(response.value().results[i].score == disabled.value().results[i].score);
}
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_shadow_evaluated") == "1");
CHECK(debug.at("topology_weak_query_applied") == "0");
CHECK(debug.at("topology_weak_query_added_candidates") == "0");
CHECK(debug.at("topology_weak_query_duplicate_candidates") == "0");
CHECK(debug.at("topology_weak_query_total_candidates") == "1");
}
SECTION("narrow without a certificate stays global") {
auto response = runTopologySearch(
fix, generator,
routedOnlyCandidateConfig(SearchEngineConfig::TopologyVectorPolicy::Narrow), 4,
"alpha one");
REQUIRE(response.has_value());
const auto hashes = resultHashes(response.value());
CHECK(std::ranges::find(hashes, "x2") == hashes.end());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "narrow");
CHECK(debug.at("topology_weak_query_applied") == "1");
CHECK(debug.at("topology_route_admission_eligible") == "0");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_weak_query_added_candidates") == "0");
CHECK(debug.at("topology_weak_query_duplicate_candidates") == "0");
CHECK(debug.at("topology_weak_query_total_candidates") == "1");
CHECK(debug.at("topology_candidate_rescue_attempted") == "0");
}
}
TEST_CASE("Evidence pipeline receives no topology adjustments under a shadow policy",
"[search][topology][evidence_pipeline][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::Vec0L2};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
SearchExecutionContextGuard contextGuard(context);
auto globalConfig = topologyRoutingTestConfig(false);
SearchEngine globalEngine(fix.repo, fix.vectorDb, generator, fix.kgStore, globalConfig);
auto evidenceConfig = topologyRoutingTestConfig(true);
evidenceConfig.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
evidenceConfig.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Shadow;
evidenceConfig.topologySparseDenseAlpha = 0.0F;
evidenceConfig.topologyNarrowMinBoundaryMargin = 0.0F;
SearchEngine evidenceEngine(fix.repo, fix.vectorDb, generator, fix.kgStore, evidenceConfig);
SearchParams params;
params.limit = 4;
auto global = globalEngine.searchWithResponse("unmatched query", params);
auto evidence = evidenceEngine.searchWithResponse("unmatched query", params);
REQUIRE(global.has_value());
REQUIRE(evidence.has_value());
std::vector<std::string> globalHashes;
std::vector<std::string> evidenceHashes;
for (const auto& result : global.value().results) {
globalHashes.push_back(result.document.sha256Hash);
}
for (const auto& result : evidence.value().results) {
evidenceHashes.push_back(result.document.sha256Hash);
}
std::ranges::sort(globalHashes);
std::ranges::sort(evidenceHashes);
CHECK(evidenceHashes == globalHashes);
const auto& debug = evidence.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "shadow");
CHECK(debug.at("topology_weak_query_applied") == "0");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_vector_filter_applied") == "0");
CHECK(debug.at("topology_vector_allowed_set_ann_applied") == "0");
CHECK_FALSE(debug.contains("topology_shadow_retained_candidate_doc_ids"));
CHECK_FALSE(debug.contains("topology_shadow_removed_candidate_doc_ids"));
CHECK_FALSE(debug.contains("trace_stage_summary_json"));
CHECK_FALSE(debug.contains("trace_fusion_source_summary_json"));
CHECK(debug.at("candidate_pipeline_topology_annotated_candidates") == "0");
}
TEST_CASE("Hybrid topology keeps lexical hits while uncertified narrowing falls back",
"[search][topology][narrowing][admission][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
fix.addDocument("lexical-only", "unique needle", {1.0F, 0.0F});
seedTwoClusterTopology(fix);
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologySparseDenseAlpha = 0.0F;
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("unique needle", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
REQUIRE(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_route_narrow_proposed") == "1");
CHECK(debug.at("topology_route_admission_eligible") == "0");
CHECK(debug.at("topology_weak_query_allowed_candidates") == "2");
CHECK(debug.at("topology_vector_filter_applied") == "0");
CHECK_FALSE(debug.contains("topology_vector_scores_reused"));
CHECK_FALSE(debug.contains("topology_member_rerank_distance_evaluations_actual"));
const auto lexical = std::find_if(
response.value().results.begin(), response.value().results.end(),
[](const auto& result) { return result.document.sha256Hash == "lexical-only"; });
REQUIRE(lexical != response.value().results.end());
}
TEST_CASE("SearchEngine topology routing loads stored artifacts without freshness readiness",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(true);
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = false;
context.freshness.topologyArtifactsFresh = false;
context.freshness.topologyEpoch = 0;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("zz", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_routing_freshness_gate") == "1");
CHECK((debug.at("topology_routing_mode") == "weak_query_only"));
CHECK((debug.at("topology_weak_query_enabled") == "1"));
CHECK((debug.at("topology_weak_query_load_attempted") == "1"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "1"));
CHECK((debug.at("topology_ready") == "1"));
CHECK((debug.at("topology_artifacts_fresh") == "1"));
CHECK(debug.contains("topology_epoch"));
CHECK(debug.at("topology_weak_query_skip_reason").empty());
CHECK((debug.at("topology_weak_query_applied") == "1"));
CHECK((debug.at("topology_weak_query_added_candidates") == "0"));
}
TEST_CASE("SearchEngine abstains from topology routing when daemon artifacts are stale",
"[search][topology][freshness][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(true);
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyStatusKnown = true;
context.freshness.topologyEpoch = 7;
context.freshness.topologyReady = false;
context.freshness.topologyArtifactsFresh = false;
context.freshness.topologyDirtyDocuments = 1;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("zz", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_routing_freshness_gate") == "0");
CHECK(debug.at("topology_routing_mode") == "disabled");
CHECK(debug.at("topology_weak_query_load_attempted") == "0");
CHECK(debug.at("topology_weak_query_load_succeeded") == "0");
CHECK(debug.at("topology_weak_query_skip_reason") == "artifacts_not_fresh");
CHECK(debug.at("topology_weak_query_applied") == "0");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
}
TEST_CASE("SearchEngine rejects inconsistent topology artifacts before routing",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTopologyWithStaleClusterMember(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(true);
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
context.freshness.topologyArtifactsFresh = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("zz", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("topology_weak_query_load_attempted") == "1"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "1"));
CHECK((debug.at("topology_artifact_admitted") == "0"));
CHECK((debug.at("topology_weak_query_skip_reason").rfind("invalid_artifact:", 0) == 0));
CHECK((debug.at("topology_weak_query_applied") == "0"));
CHECK((debug.at("topology_weak_query_added_candidates") == "0"));
}
TEST_CASE("SearchEngine disabled topology routing does not load or add candidates",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(false);
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("zz", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("topology_routing_mode") == "disabled"));
CHECK((debug.at("topology_weak_query_enabled") == "0"));
CHECK((debug.at("topology_weak_query_load_attempted") == "0"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "0"));
CHECK((debug.at("topology_weak_query_skip_reason") == "disabled"));
CHECK((debug.at("topology_weak_query_applied") == "0"));
CHECK((debug.at("topology_weak_query_narrow_applied") == "0"));
CHECK((debug.at("topology_weak_query_added_candidates") == "0"));
}
TEST_CASE("SearchEngine weak-only topology routing skips strong tier-1 queries",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(true);
config.weakQueryMinTextHits = 1;
config.weakQueryMinTopTextScore = 0.0F;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
context.freshness.topologyArtifactsFresh = true;
context.freshness.topologyEpoch = 7;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("topology_routing_mode") == "weak_query_only"));
CHECK((debug.at("topology_weak_query_enabled") == "1"));
CHECK((debug.at("topology_weak_query_load_attempted") == "0"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "0"));
CHECK((debug.at("topology_weak_query_skip_reason") == "strong_tier1_query"));
CHECK((debug.at("topology_weak_query_applied") == "0"));
CHECK((debug.at("topology_weak_query_added_candidates") == "0"));
}
TEST_CASE("SearchEngine global fallback vector-scores outside-route lexical hits",
"[search][topology][admission][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
fix.addDocument("z1", "alpha outside route", {0.0F, 1.0F});
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.weakQueryMinTextHits = 1;
config.weakQueryMinTopTextScore = 0.0F;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("topology_routing_mode") == "hybrid_assist"));
CHECK((debug.at("topology_weak_query_load_attempted") == "1"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "1"));
CHECK(debug.at("topology_weak_query_skip_reason").empty());
CHECK((debug.at("topology_weak_query_applied") == "1"));
CHECK((debug.at("topology_weak_query_narrow_applied") == "0"));
CHECK((debug.at("topology_route_narrow_proposed") == "1"));
CHECK((debug.at("topology_route_admission_eligible") == "0"));
CHECK((debug.at("topology_weak_query_allowed_candidates") == "2"));
const auto outsideRoute =
std::find_if(response.value().results.begin(), response.value().results.end(),
[](const auto& result) { return result.document.sha256Hash == "z1"; });
REQUIRE((outsideRoute != response.value().results.end()));
CHECK(outsideRoute->vectorScore.has_value());
const bool hasTopologyVectorScore = std::any_of(
response.value().results.begin(), response.value().results.end(), [](const auto& result) {
return result.graphVectorScore.has_value() && result.graphVectorScore.value() > 0.0;
});
CHECK_FALSE(hasTopologyVectorScore);
}
TEST_CASE("SearchEngine stage trace reports reranker flags as booleans",
"[search][trace][rerank][catch2]") {
yams::test::ScopedEnvVar stageTrace("YAMS_SEARCH_STAGE_TRACE", std::optional<std::string>("1"));
yams::test::ScopedEnvVar traceTopN("YAMS_SEARCH_STAGE_TRACE_TOP_N",
std::optional<std::string>("4"));
TopologySearchFixture fix;
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
fix.addDocument("x2", "alpha two", {0.9F, 0.1F});
SearchEngineConfig config;
config.includeDebugInfo = true;
config.enableParallelExecution = false;
config.textWeight = 1.0F;
config.simeonTextWeight = 0.0F;
config.pathTreeWeight = 0.0F;
config.kgWeight = 0.0F;
config.vectorWeight = 0.0F;
config.entityVectorWeight = 0.0F;
config.tagWeight = 0.0F;
config.metadataWeight = 0.0F;
config.enableGraphRerank = false;
config.enableReranking = true;
config.rerankTopK = 5;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, nullptr, fix.kgStore, config);
SearchParams params;
params.limit = 2;
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
REQUIRE(debug.contains("trace_enabled"));
CHECK((debug.at("trace_enabled") == "1"));
REQUIRE(debug.contains("trace_cross_rerank_applied"));
CHECK((debug.at("trace_cross_rerank_applied") == "0"));
REQUIRE(debug.contains("trace_rerank_guard_score_gap"));
REQUIRE(debug.contains("trace_stage_summary_json"));
REQUIRE(debug.contains("trace_fusion_source_summary_json"));
CHECK((debug.at("trace_rerank_guard_score_gap") == "0.000000"));
}
TEST_CASE("SearchEngine exposes the topology evidence pipeline as its only frontend",
"[search][evidence_pipeline][catch2]") {
TopologySearchFixture fix;
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
fix.addDocument("x2", "alpha two", {0.9F, 0.1F});
SearchEngineConfig config;
config.includeDebugInfo = true;
config.enableParallelExecution = false;
config.textWeight = 1.0F;
config.simeonTextWeight = 0.0F;
config.pathTreeWeight = 0.0F;
config.kgWeight = 0.0F;
config.vectorWeight = 0.0F;
config.entityVectorWeight = 0.0F;
config.tagWeight = 0.0F;
config.metadataWeight = 0.0F;
config.enableGraphRerank = false;
config.enableReranking = false;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, nullptr, fix.kgStore, config);
SearchParams params;
params.limit = 2;
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response);
REQUIRE(response.value().results.size() == 2);
const auto& debug = response.value().debugStats;
CHECK(debug.at("search_pipeline_name") == "topology_evidence");
CHECK(std::stoull(debug.at("candidate_pipeline_input_components")) > 0);
CHECK(std::stoull(debug.at("candidate_pipeline_aggregated_candidates")) >=
response.value().results.size());
CHECK(debug.at("candidate_pipeline_topology_annotated_candidates") == "0");
}
TEST_CASE("SearchEngine cross reranker promotes lower fused candidate",
"[search][rerank][catch2]") {
TopologySearchFixture fix;
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
fix.addDocument("x2", "alpha two", {0.9F, 0.1F});
SearchEngineConfig config;
config.includeDebugInfo = true;
config.enableParallelExecution = false;
config.textWeight = 1.0F;
config.simeonTextWeight = 0.0F;
config.pathTreeWeight = 0.0F;
config.kgWeight = 0.0F;
config.vectorWeight = 0.0F;
config.entityVectorWeight = 0.0F;
config.tagWeight = 0.0F;
config.metadataWeight = 0.0F;
config.enableGraphRerank = false;
config.enableReranking = true;
config.rerankTopK = 2;
config.rerankReplaceScores = true;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, nullptr, fix.kgStore, config);
engine.setCrossReranker(
[](const std::string&,
const std::vector<std::string>& documents) -> Result<std::vector<float>> {
std::vector<float> scores;
scores.reserve(documents.size());
for (const auto& doc : documents) {
scores.push_back(doc.find("x2") != std::string::npos ? 1.0F : 0.1F);
}
return scores;
});
SearchParams params;
params.limit = 2;
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
REQUIRE((response.value().results.size() == 2));
CHECK((response.value().results.front().document.sha256Hash == "x2"));
const auto& debug = response.value().debugStats;
CHECK((debug.at("cross_rerank_available") == "1"));
CHECK(debug.at("cross_rerank_skip_reason").empty());
CHECK((debug.at("cross_rerank_replace_scores") == "1"));
}
TEST_CASE("SearchEngine reports the scoring mode of the wired cross reranker",
"[search][rerank][catch2]") {
TopologySearchFixture fix;
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
fix.addDocument("x2", "alpha two", {0.9F, 0.1F});
SearchEngineConfig config;
config.includeDebugInfo = true;
config.enableParallelExecution = false;
config.enableGraphRerank = false;
config.enableReranking = true;
config.rerankTopK = 2;
// The daemon resolves the scoring mode from reranker policy, which can differ from the
// engine config; the debug stat must describe the scorer that actually ran.
config.simeonRerankOuterMaxSim = false;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, nullptr, fix.kgStore, config);
auto scorer = [](const std::string&,
const std::vector<std::string>& documents) -> Result<std::vector<float>> {
return std::vector<float>(documents.size(), 0.5F);
};
SearchParams params;
params.limit = 2;
SECTION("mode supplied by the wiring wins over the engine config") {
engine.setCrossReranker(scorer, "simeon_outer_maxsim");
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("cross_rerank_scoring_mode") == "simeon_outer_maxsim"));
CHECK((debug.at("cross_rerank_simeon_outer_maxsim") == "1"));
}
SECTION("an unlabelled scorer reports an unknown mode") {
engine.setCrossReranker(scorer);
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("cross_rerank_scoring_mode") == "unknown"));
CHECK((debug.at("cross_rerank_simeon_outer_maxsim") == "0"));
}
}
// NOLINTEND(cppcoreguidelines-avoid-do-while, readability-function-cognitive-complexity)
TEST_CASE("Topology routing options preserve the typed product configuration",
"[search][topology][options][catch2]") {
SearchEngineConfig config;
config.topologyRouteScoringMode = SearchEngineConfig::TopologyRouteScoringMode::SeedCoverage;
config.topologyExpansionSource = SearchEngineConfig::TopologyExpansionSource::GraphNeighbors;
config.topologyMinClusters = 2;
config.topologyMaxClusters = 5;
config.topologyRoutingRepresentativeLimit = 3;
config.topologyRoutingAnnCandidateLimit = 19;
config.topologyAdaptiveProbeScoreGap = 0.07F;
config.topologyNarrowMinBoundaryMargin = 0.11F;
config.topologyMaxDocs = 17;
config.topologySparseDenseAlpha = 0.65F;
config.topologyMinRouteScore = 0.22F;
config.topologyRouteRiskCalibration.constructionFingerprint = "atlas-fingerprint";
config.topologyRouteWorkBudget.maxRowsVisited = 23;
config.topologyGraphNeighborMinScore = 0.31F;
config.topologyGraphNeighborReciprocalOnly = false;
config.topologyGraphWeightedSeedRanking = true;
const auto options =
makeTopologyRoutingOptions(config, SearchEngineConfig::TopologyRoutingMode::WeakQueryOnly,
/*weakTier1Query=*/true, /*collectRouteMembership=*/true);
CHECK(options.routingMode == SearchEngineConfig::TopologyRoutingMode::WeakQueryOnly);
CHECK(options.routeScoringMode == SearchEngineConfig::TopologyRouteScoringMode::SeedCoverage);
CHECK(options.expansionSource == SearchEngineConfig::TopologyExpansionSource::GraphNeighbors);
CHECK(options.weakTier1Query);
CHECK(options.minClusters == 2U);
CHECK(options.maxClusters == 5U);
CHECK(options.representativeLimit == 3U);
CHECK(options.denseAnnCandidateLimit == 19U);
CHECK(options.adaptiveProbeScoreGap == Catch::Approx(0.07F));
CHECK(options.narrowMinBoundaryMargin == Catch::Approx(0.11F));
CHECK(options.maxDocs == 17U);
CHECK(options.sparseDenseAlpha == Catch::Approx(0.65F));
CHECK(options.minRouteScore == Catch::Approx(0.22F));
CHECK(options.routeRiskCalibration.constructionFingerprint == "atlas-fingerprint");
CHECK(options.routeWorkBudget.maxRowsVisited == 23U);
CHECK(options.collectRouteMembership);
CHECK(options.graphNeighborMinScore == Catch::Approx(0.31F));
CHECK_FALSE(options.graphNeighborReciprocalOnly);
CHECK(options.graphWeightedSeedRanking);
// Default (shadow) policy reports no additions; only augment counts route members.
CHECK_FALSE(options.countRouteMemberAdditions);
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Narrow;
CHECK_FALSE(makeTopologyRoutingOptions(config, options.routingMode, true, true)
.countRouteMemberAdditions);
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Augment;
const auto augment = makeTopologyRoutingOptions(config, options.routingMode, true, true);
CHECK(augment.countRouteMemberAdditions);
CHECK_FALSE(makeTopologyRoutingOptions(config, options.routingMode, true, false)
.countRouteMemberAdditions);
// Accounting does not change the allowed-set policy identity used by route calibration.
CHECK(topologyRoutingPolicyFingerprint("representation", augment) ==
topologyRoutingPolicyFingerprint("representation", options));
}
TEST_CASE("Topology route admission requires the protected-relation proof obligations",
"[search][topology][admission][catch2]") {
TopologyRouteAdmission admission;
CHECK_FALSE(admission.eligibleForNarrowing());
CHECK(admission.denialReason() == "protected_relation_coverage_unavailable");
admission.protectedRelationCoverage = TopologyProofObligationStatus::Satisfied;
admission.coordinateSpaceAlignment = TopologyProofObligationStatus::Satisfied;
admission.protectedFibersRepresented = TopologyProofObligationStatus::Satisfied;
admission.certificateSaturatesProtectedFibers = TopologyProofObligationStatus::Satisfied;
admission.routeRisk = TopologyProofObligationStatus::Satisfied;
admission.work = TopologyProofObligationStatus::Satisfied;
admission.coverMaterialization = TopologyProofObligationStatus::Satisfied;
admission.selectedCoverDocuments = 2;
admission.materializedCoverDocuments = 2;
CHECK(admission.eligibleForTrialNarrowing());
CHECK(admission.eligibleForNarrowing());
CHECK(admission.denialReason().empty());
admission.protectedRelationCoverage = TopologyProofObligationStatus::Violated;
CHECK_FALSE(admission.eligibleForTrialNarrowing());
CHECK_FALSE(admission.eligibleForNarrowing());
CHECK(admission.denialReason() == "protected_relation_coverage_violated");
admission.protectedRelationCoverage = TopologyProofObligationStatus::Satisfied;
admission.protectedFibersRepresented = TopologyProofObligationStatus::Unavailable;
CHECK(admission.denialReason() == "protected_fibers_representation_unavailable");
admission.protectedFibersRepresented = TopologyProofObligationStatus::Satisfied;
admission.certificateSaturatesProtectedFibers = TopologyProofObligationStatus::Violated;
CHECK(admission.denialReason() == "protected_fiber_saturation_violated");
}
TEST_CASE("Topology route certificate owns the theorem action boundary",
"[search][topology][certificate][catch2]") {
TopologyRouteCertificate certificate;
CHECK(certificate.action() == TopologyRouteAction::Global);
CHECK_FALSE(certificate.eligibleForTrialNarrowing());
certificate.allowedDocumentHashes.insert("document");
CHECK(certificate.action() == TopologyRouteAction::Augment);
auto& admission = certificate.admission;
admission.coordinateSpaceAlignment = TopologyProofObligationStatus::Satisfied;
admission.protectedRelationCoverage = TopologyProofObligationStatus::Satisfied;
admission.protectedFibersRepresented = TopologyProofObligationStatus::Satisfied;
admission.certificateSaturatesProtectedFibers = TopologyProofObligationStatus::Satisfied;
admission.routeRisk = TopologyProofObligationStatus::Satisfied;
admission.coverMaterialization = TopologyProofObligationStatus::Satisfied;
admission.selectedCoverDocuments = 1;
admission.materializedCoverDocuments = 1;
CHECK_FALSE(certificate.isConstructionBound());
CHECK_FALSE(certificate.eligibleForTrialNarrowing());
certificate.constructionFingerprint = "construction";
certificate.selectedCoverIds = {"cover"};
certificate.selectedProtectedRelationFiberIds = {"fiber"};
CHECK(certificate.isConstructionBound());
CHECK(certificate.eligibleForTrialNarrowing());
CHECK(certificate.action() == TopologyRouteAction::Augment);
admission.work = TopologyProofObligationStatus::Satisfied;
CHECK(certificate.action() == TopologyRouteAction::Narrow);
}
TEST_CASE("Topology routing session reports route confidence and seed coverage",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y1", "not-in-any-cluster"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 2;
request.options.maxDocs = 8;
request.options.minRouteScore = 0.0F;
const auto session = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(session.artifactAdmitted);
REQUIRE((session.acceptedRoutes >= 1));
CHECK((session.bestRouteScore > 0.0F));
CHECK((session.meanAcceptedRouteScore > 0.0F));
CHECK((session.meanAcceptedRouteScore <= session.bestRouteScore));
CHECK((session.seedCount == 2));
CHECK((session.seedsInRoutedClusters == 1));
}
TEST_CASE("Topology routing session excludes min-score-rejected routes from confidence aggregates",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y1"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 2;
request.options.maxDocs = 8;
request.options.minRouteScore = 1000000.0F;
const auto session = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK((session.routesRejected >= 1));
CHECK((session.acceptedRoutes == 0));
CHECK((session.bestRouteScore == 0.0F));
CHECK((session.meanAcceptedRouteScore == 0.0F));
CHECK((session.seedsInRoutedClusters == 0));
CHECK((session.addedCandidates == 0));
}
TEST_CASE("Topology routing expands through query-independent medoids",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y1"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 2;
request.options.maxDocs = 16;
const auto session = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(session.artifactAdmitted);
CHECK((session.addedCandidates <= session.acceptedRoutes));
CHECK((session.routedCandidateHashes.size() <= session.acceptedRoutes));
}
TEST_CASE("Topology routing refuses hard narrowing without complete theorem evidence",
"[search][topology][narrowing][admission][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y2"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 1;
request.options.maxDocs = 1;
request.options.collectRouteMembership = true;
const auto session = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(session.artifactAdmitted);
REQUIRE(session.certificate.hasUsefulRoute());
CHECK_FALSE(session.certificate.admission.eligibleForNarrowing());
CHECK(session.certificate.admission.coverMaterialization ==
TopologyProofObligationStatus::Violated);
CHECK(session.certificate.admission.certificateSaturatesProtectedFibers ==
TopologyProofObligationStatus::Violated);
CHECK(session.certificate.admission.selectedCoverDocuments == 2U);
CHECK(session.certificate.admission.materializedCoverDocuments == 1U);
CHECK(session.certificate.admission.denialReason() == "cover_materialization_incomplete");
CHECK(session.routedCandidateHashes.empty());
CHECK(session.routedDocs == 1U);
CHECK(session.certificate.allowedDocumentHashes.size() == 1U);
CHECK(session.addedCandidates == 0U);
CHECK(session.addedCandidateHashes.empty());
CHECK(session.certificate.allowedDocumentHashes.contains("y2"));
CHECK_FALSE(session.certificate.allowedDocumentHashes.contains("y1"));
CHECK(session.candidateStructureEvidence.size() == 1U);
REQUIRE(session.candidateStructureEvidence.contains("y2"));
CHECK(session.candidateStructureEvidence.at("y2").scaleAgreement == Catch::Approx(0.0F));
request.options.maxDocs = 0;
const auto unbounded = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(unbounded.certificate.hasUsefulRoute());
CHECK_FALSE(unbounded.certificate.admission.eligibleForNarrowing());
CHECK(unbounded.certificate.admission.coverMaterialization ==
TopologyProofObligationStatus::Satisfied);
CHECK(unbounded.certificate.admission.certificateSaturatesProtectedFibers ==
TopologyProofObligationStatus::Satisfied);
CHECK(unbounded.certificate.admission.selectedCoverDocuments == 2U);
CHECK(unbounded.certificate.admission.materializedCoverDocuments == 2U);
CHECK(unbounded.certificate.admission.denialReason() ==
"protected_fibers_representation_unavailable");
CHECK(unbounded.certificate.allowedDocumentHashes.size() == 2U);
CHECK(unbounded.certificate.allowedDocumentHashes.contains("y1"));
CHECK(unbounded.certificate.allowedDocumentHashes.contains("y2"));
CHECK_FALSE(unbounded.certificate.constructionFingerprint.empty());
REQUIRE(unbounded.certificate.selectedCoverIds.size() == 1U);
REQUIRE(unbounded.certificate.selectedProtectedRelationFiberIds.size() == 1U);
CHECK(unbounded.routedCandidateHashes.contains("y1"));
}
TEST_CASE("Topology routing exposes selected multiscale structural evidence from the snapshot",
"[search][topology][evidence_pipeline][multiscale][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
auto batch = buildMultiscaleOverlapTopologyBatch();
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y1"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 2;
request.options.maxDocs = 1;
request.options.collectRouteMembership = true;
request.snapshotCache = std::make_shared<TopologyRoutingSnapshotCache>([batch]() mutable {
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{std::move(batch)}};
});
const auto session = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(session.artifactAdmitted);
REQUIRE(session.certificate.allowedDocumentHashGroups.size() == 2U);
CHECK(session.certificate.allowedDocumentHashGroups[0].contains("y1"));
CHECK(session.certificate.allowedDocumentHashGroups[1].contains("y1"));
REQUIRE(session.candidateStructureEvidence.contains("y1"));
const auto& evidence = session.candidateStructureEvidence.at("y1");
CHECK(evidence.scaleAgreement == Catch::Approx(1.0F));
CHECK(evidence.overlapSupport > 0.0F);
CHECK(evidence.persistenceSupport > 0.0F);
CHECK(evidence.cohesionSupport > 0.0F);
CHECK(evidence.bridgeSupport > 0.0F);
CHECK(evidence.densitySupport > 0.0F);
}
TEST_CASE("Topology snapshot cache validates once and reuses the admitted epoch",
"[search][topology][cache][catch2]") {
auto batch = buildTwoClusterTopologyBatch();
int loadCalls = 0;
TopologyRoutingSnapshotCache cache([&]() {
++loadCalls;
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{batch}};
});
auto first = cache.get(batch.topologyEpoch);
REQUIRE(first.has_value());
REQUIRE(first.value().snapshot);
CHECK_FALSE(first.value().cacheHit);
CHECK((first.value().snapshot->clustersById.size() == batch.clusters.size()));
CHECK((first.value().snapshot->membershipsByDocumentHash.size() == batch.memberships.size()));
auto second = cache.get(batch.topologyEpoch);
REQUIRE(second.has_value());
REQUIRE(second.value().snapshot);
CHECK(second.value().cacheHit);
CHECK((loadCalls == 1));
}
TEST_CASE("Topology snapshot cache upgrades an exact index when ANN is later required",
"[search][topology][cache][ann][catch2]") {
auto batch = buildTwoClusterTopologyBatch();
int loadCalls = 0;
TopologyRoutingSnapshotCache cache([&]() {
++loadCalls;
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{batch}};
});
const auto exact = cache.get(batch.topologyEpoch, false);
REQUIRE(exact.has_value());
REQUIRE(exact.value().snapshot);
CHECK_FALSE(exact.value().snapshot->denseAnnBuildAttempted);
CHECK_FALSE(exact.value().snapshot->sparseRouteIndex.centroidAnnIndex);
const auto upgraded = cache.get(batch.topologyEpoch, true);
REQUIRE(upgraded.has_value());
REQUIRE(upgraded.value().snapshot);
CHECK(upgraded.value().cacheHit);
CHECK(upgraded.value().snapshot->denseAnnBuildAttempted);
CHECK(upgraded.value().snapshot->sparseRouteIndex.centroidAnnIndex);
CHECK(loadCalls == 1);
}
TEST_CASE("Topology construction fingerprint excludes publication and routing representatives",
"[search][topology][cache][identity][catch2]") {
auto baseline = buildTwoClusterTopologyBatch();
auto experimental = baseline;
experimental.snapshotId = "different-publication";
experimental.generatedAtUnixSeconds += 100;
experimental.topologyEpoch += 1;
experimental.clusters.front().routingRepresentatives.push_back(
yams::topology::ClusterRoutingRepresentative{.documentHash = "x1",
.embedding = {1.0F, 0.0F}});
const auto baselineIdentity = topologyRoutingConstructionFingerprint(baseline);
const auto experimentalIdentity = topologyRoutingConstructionFingerprint(experimental);
REQUIRE(baselineIdentity.size() == 16);
CHECK(experimentalIdentity == baselineIdentity);
const auto baselineRepresentations = topologyRoutingRepresentationFingerprint(baseline);
CHECK(topologyRoutingRepresentationFingerprint(experimental) != baselineRepresentations);
auto options = TopologyRoutingOptions{};
const auto policyIdentity = topologyRoutingPolicyFingerprint(baselineRepresentations, options);
options.collectGraphDiagnostics = true;
CHECK(topologyRoutingPolicyFingerprint(baselineRepresentations, options) == policyIdentity);
options.sparseDenseAlpha = 0.25F;
CHECK(topologyRoutingPolicyFingerprint(baselineRepresentations, options) != policyIdentity);
options = TopologyRoutingOptions{};
options.graphNeighborReciprocalOnly = false;
CHECK(topologyRoutingPolicyFingerprint(baselineRepresentations, options) != policyIdentity);
experimental.embeddingSpaceIdentity = "test-space-v2";
CHECK(topologyRoutingConstructionFingerprint(experimental) != baselineIdentity);
experimental.embeddingSpaceIdentity = baseline.embeddingSpaceIdentity;
experimental.protectedRelationIdentity = "rewired-relation";
CHECK(topologyRoutingConstructionFingerprint(experimental) != baselineIdentity);
experimental.protectedRelationIdentity = baseline.protectedRelationIdentity;
experimental.clusters.front().memberDocumentHashes.push_back("new-member");
CHECK(topologyRoutingConstructionFingerprint(experimental) != baselineIdentity);
}
TEST_CASE("Topology routing refuses theorem admission across coordinate spaces",
"[search][topology][identity][admission][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
const auto constructionFingerprint = topologyRoutingConstructionFingerprint(batch);
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y2"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.queryEmbeddingSpaceIdentity = "different-space-v1";
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 1;
request.options.maxDocs = 0;
request.options.collectRouteMembership = true;
request.options.routeRiskCalibration.constructionFingerprint = constructionFingerprint;
request.options.routeRiskCalibration.routingPolicyFingerprint =
topologyRoutingPolicyFingerprint(topologyRoutingRepresentationFingerprint(batch),
request.options);
request.options.routeRiskCalibration.datasetIdentity = "fixture-held-out-v1";
request.options.routeRiskCalibration.calibrationQueries = 50;
request.options.routeRiskCalibration.protectedCandidates = 100;
request.options.routeRiskCalibration.missedProtectedCandidates = 0;
request.options.routeRiskCalibration.minCalibrationQueries = 30;
request.options.routeRiskCalibration.maxMissesPerThousand = 10;
request.options.routeRiskCalibration.minBoundaryMargin = 0.0F;
request.options.routeRiskCalibration.minSeedHits = 1;
request.snapshotCache = std::make_shared<TopologyRoutingSnapshotCache>([batch]() mutable {
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{std::move(batch)}};
});
const auto mismatched = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(mismatched.certificate.hasUsefulRoute());
CHECK(mismatched.certificate.admission.coordinateSpaceAlignment ==
TopologyProofObligationStatus::Violated);
CHECK(mismatched.certificate.admission.denialReason() == "coordinate_space_mismatch");
CHECK(mismatched.certificate.action() == TopologyRouteAction::Augment);
request.queryEmbeddingSpaceIdentity = "test-space-v1";
const auto aligned = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK(aligned.certificate.admission.coordinateSpaceAlignment ==
TopologyProofObligationStatus::Satisfied);
CHECK(aligned.certificate.admission.routeRisk == TopologyProofObligationStatus::Satisfied);
// Counts from an older routing policy cannot certify a changed representative budget.
request.options.representativeLimit = 1;
const auto changedPolicy = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK(changedPolicy.certificate.admission.routeRisk ==
TopologyProofObligationStatus::Unavailable);
}
TEST_CASE("Topology snapshot cache rejects duplicate cluster identifiers",
"[search][topology][cache][validation][catch2]") {
auto batch = buildTwoClusterTopologyBatch();
REQUIRE((batch.clusters.size() >= 2));
batch.clusters[1].clusterId = batch.clusters[0].clusterId;
TopologyRoutingSnapshotCache cache([batch]() mutable {
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{std::move(batch)}};
});
const auto result = cache.get();
REQUIRE_FALSE(result.has_value());
CHECK((result.error().message == "invalid_artifact:duplicate_cluster_id"));
}
TEST_CASE("Topology snapshot cache admits a materialized boundary spill",
"[search][topology][cache][validation][overlap][catch2]") {
auto batch = buildTwoClusterTopologyBatch();
REQUIRE(batch.clusters.size() == 2);
const auto primaryId = batch.memberships.front().clusterId;
auto secondary = std::ranges::find_if(
batch.clusters, [&](const auto& cluster) { return cluster.clusterId != primaryId; });
REQUIRE(secondary != batch.clusters.end());
secondary->memberDocumentHashes.push_back(batch.memberships.front().documentHash);
std::ranges::sort(secondary->memberDocumentHashes);
secondary->memberCount = secondary->memberDocumentHashes.size();
batch.memberships.front().overlapClusterIds = {secondary->clusterId};
TopologyRoutingSnapshotCache cache([batch]() mutable {
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{std::move(batch)}};
});
const auto result = cache.get();
REQUIRE(result.has_value());
REQUIRE(result.value().snapshot);
CHECK(result.value()
.snapshot->sparseRouteIndex.clustersByDocumentHash
.at(batch.memberships.front().documentHash)
.size() == 2);
}
TEST_CASE("mergeTopologySeedHashes keeps Tier-1 first and caps vector seeds",
"[unit][search][topology][graph_neighbors]") {
const auto merged =
mergeTopologySeedHashes({"t1", "t2", "t1"}, {"v1", "t2", "v2", "v3"}, /*maxVectorSeeds=*/2);
REQUIRE((merged.size() == 4));
CHECK((merged[0] == "t1"));
CHECK((merged[1] == "t2"));
CHECK((merged[2] == "v1"));
CHECK((merged[3] == "v2"));
const auto none = mergeTopologySeedHashes({"t1", "t2"}, {"v1", "v2"}, /*maxVectorSeeds=*/0);
REQUIRE((none.size() == 2));
CHECK((none[0] == "t1"));
CHECK((none[1] == "t2"));
const auto emptyVec = mergeTopologySeedHashes({"t1"}, {}, /*maxVectorSeeds=*/16);
REQUIRE((emptyVec.size() == 1));
CHECK((emptyVec[0] == "t1"));
}
TEST_CASE("rankTopologySeedEvidence keeps ranked lexical evidence and caps fanout",
"[unit][search][topology][weighted-seeds]") {
std::vector<ComponentResult> components{
ComponentResult{.documentHash = "rank-zero",
.score = 0.80F,
.source = ComponentResult::Source::Text,
.rank = 0},
ComponentResult{.documentHash = "late-high",
.score = 0.95F,
.source = ComponentResult::Source::Text,
.rank = 10},
ComponentResult{.documentHash = "early",
.score = 0.80F,
.source = ComponentResult::Source::Text,
.rank = 1},
ComponentResult{.documentHash = "late-high",
.score = 0.90F,
.source = ComponentResult::Source::SimeonText,
.rank = 2},
ComponentResult{.documentHash = "vector-only",
.score = 1.0F,
.source = ComponentResult::Source::Vector,
.rank = 1},
};
const auto evidence = rankTopologySeedEvidence(components, 3);
REQUIRE((evidence.size() == 3));
CHECK((evidence[0].documentHash == "rank-zero"));
CHECK((evidence[1].documentHash == "early"));
CHECK((evidence[2].documentHash == "late-high"));
CHECK((evidence[0].weight > evidence[1].weight));
}
TEST_CASE("rankTopologyVectorSeedEvidence preserves vector score and rank",
"[unit][search][topology][weighted-seeds]") {
std::vector<ComponentResult> components{
ComponentResult{.documentHash = "near",
.score = 0.90F,
.source = ComponentResult::Source::Vector,
.rank = 0},
ComponentResult{.documentHash = "far",
.score = 0.95F,
.source = ComponentResult::Source::Vector,
.rank = 10},
ComponentResult{.documentHash = "near",
.score = 0.80F,
.source = ComponentResult::Source::Vector,
.rank = 2},
};
const auto evidence = rankTopologyVectorSeedEvidence(components, 2);
REQUIRE(evidence.size() == 2);
CHECK(evidence[0].documentHash == "near");
CHECK(evidence[1].documentHash == "far");
CHECK(evidence[0].weight > evidence[1].weight);
}
TEST_CASE("selectTopologyRoutesForNarrowing adapts probes and abstains at an unsafe boundary",
"[unit][search][topology][adaptive-probe]") {
std::vector<yams::topology::ClusterRoute> routes{
yams::topology::ClusterRoute{.clusterId = "a", .routeScore = 1.00},
yams::topology::ClusterRoute{.clusterId = "b", .routeScore = 0.97},
yams::topology::ClusterRoute{.clusterId = "c", .routeScore = 0.80},
};
const auto safe = selectTopologyRoutesForNarrowing(routes, /*minClusters=*/1, /*maxClusters=*/3,
/*adaptiveScoreGap=*/0.05F,
/*minBoundaryMargin=*/0.10F);
REQUIRE((safe.routes.size() == 2));
CHECK((safe.routes[0].clusterId == "a"));
CHECK((safe.routes[1].clusterId == "b"));
CHECK((safe.boundaryScoreMargin == 0.17F));
CHECK_FALSE(safe.abstained);
const auto unsafe = selectTopologyRoutesForNarrowing(
routes, /*minClusters=*/1, /*maxClusters=*/2, /*adaptiveScoreGap=*/0.05F,
/*minBoundaryMargin=*/0.20F);
REQUIRE((unsafe.routes.size() == 2));
CHECK(unsafe.abstained);
}
TEST_CASE("legacy topology false preserves the hybrid product default",
"[unit][search][topology][config]") {
SearchEngineConfig config;
REQUIRE(config.topologyRoutingMode == SearchEngineConfig::TopologyRoutingMode::HybridAssist);
CHECK(resolveTopologyRoutingMode(config, false) ==
SearchEngineConfig::TopologyRoutingMode::HybridAssist);
CHECK(resolveTopologyRoutingMode(config, true) ==
SearchEngineConfig::TopologyRoutingMode::WeakQueryOnly);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::Disabled;
CHECK(resolveTopologyRoutingMode(config) == SearchEngineConfig::TopologyRoutingMode::Disabled);
}
TEST_CASE("fillTopologySkipReason preserves session reason and fills product defaults",
"[unit][search][topology]") {
using Mode = SearchEngineConfig::TopologyRoutingMode;
TopologyRoutingOptions options;
TopologyRoutingSessionResult session;
options.routingMode = Mode::HybridAssist;
std::string reason = "graph_medoid_neighbors";
fillTopologySkipReason(reason, options, /*hasStores=*/true, session);
CHECK((reason == "graph_medoid_neighbors"));
options.routingMode = Mode::Disabled;
reason.clear();
fillTopologySkipReason(reason, options, /*hasStores=*/true, session);
CHECK((reason == "disabled"));
options.routingMode = Mode::HybridAssist;
session.loadSucceeded = true;
session.routedClusters = 2;
reason.clear();
fillTopologySkipReason(reason, options, /*hasStores=*/true, session);
CHECK((reason == "no_added_candidates"));
}
TEST_CASE("rankGraphNeighborCandidates ranks multi-seed hits and filters score/reciprocal",
"[unit][search][topology][graph_neighbors]") {
std::unordered_map<std::string, std::vector<std::tuple<std::string, float, bool>>> adj;
adj["s1"] = {
{"n_shared", 0.9F, true},
{"n_weak", 0.1F, true},
{"n_oneway", 0.95F, false},
};
adj["s2"] = {
{"n_shared", 0.85F, true},
// single-seed only — loses to multi-seed n_shared even with higher edge score
{"n_strong", 0.99F, true},
};
auto ranked = rankGraphNeighborCandidates(adj, {"s1", "s2"}, /*maxDocs=*/10,
/*minScore=*/0.25F, /*reciprocalOnly=*/true);
REQUIRE_FALSE(ranked.empty());
// Multi-seed hits beat single-seed even when the single edge score is higher.
CHECK((ranked.front() == "n_shared"));
// Weak edge below minScore and one-way reciprocal-only drop out.
CHECK((std::find(ranked.begin(), ranked.end(), "n_weak") == ranked.end()));
CHECK((std::find(ranked.begin(), ranked.end(), "n_oneway") == ranked.end()));
CHECK((std::find(ranked.begin(), ranked.end(), "n_strong") != ranked.end()));
auto limited = rankGraphNeighborCandidates(adj, {"s1", "s2"}, /*maxDocs=*/1, 0.25F, true);
REQUIRE((limited.size() == 1));
CHECK((limited.front() == "n_shared"));
// Reverse-only / one-way edges still rank when reciprocalOnly=false.
std::unordered_map<std::string, std::vector<std::tuple<std::string, float, bool>>> oneWay;
oneWay["s1"] = {{"n_oneway", 0.9F, false}};
auto oneWayRanked =
rankGraphNeighborCandidates(oneWay, {"s1"}, 5, 0.25F, /*reciprocalOnly=*/false);
REQUIRE((oneWayRanked.size() == 1));
CHECK((oneWayRanked.front() == "n_oneway"));
auto oneWayStrict =
rankGraphNeighborCandidates(oneWay, {"s1"}, 5, 0.25F, /*reciprocalOnly=*/true);
CHECK(oneWayStrict.empty());
}
TEST_CASE("rankGraphNeighborCandidates can weight support by query seed evidence",
"[unit][search][topology][graph_neighbors][weighted-seeds]") {
std::unordered_map<std::string, std::vector<std::tuple<std::string, float, bool>>> adjacency;
adjacency["low-1"] = {{"popular", 0.9F, true}};
adjacency["low-2"] = {{"popular", 0.9F, true}};
adjacency["high"] = {{"focused", 0.9F, true}};
const std::vector<std::string> hashes{"low-1", "low-2", "high"};
const std::vector<yams::topology::WeightedDocumentSeed> evidence{
{.documentHash = "low-1", .weight = 0.1F},
{.documentHash = "low-2", .weight = 0.1F},
{.documentHash = "high", .weight = 1.0F},
};
const auto unweighted = rankGraphNeighborCandidates(adjacency, hashes, /*maxDocs=*/2,
/*minScore=*/0.0F, /*reciprocalOnly=*/true);
REQUIRE(unweighted.size() == 2);
CHECK(unweighted.front() == "popular");
const auto weighted =
rankGraphNeighborCandidates(adjacency, hashes, /*maxDocs=*/2, /*minScore=*/0.0F,
/*reciprocalOnly=*/true, evidence);
REQUIRE(weighted.size() == 2);
CHECK(weighted.front() == "focused");
}
TEST_CASE("SearchEngine topology merge keeps graph-vector candidates behind baseline anchors",
"[search][topology][graph-vector][characterization][catch2]") {
using yams::search::detail::AuxiliaryVectorCandidateBatch;
SearchEngineConfig config;
config.vectorMaxResults = 2;
config.graphExpansionVectorPenalty = 0.5F;
config.graphVectorRequireCorroboration = true;
config.graphVectorRequireTextAnchoring = true;
config.graphVectorRequireBaselineTextAnchoring = true;
std::vector<ComponentResult> components{
{.documentHash = "baseline-anchor", .score = 0.9F, .source = ComponentResult::Source::Text},
{.documentHash = "graph-anchor",
.score = 0.8F,
.source = ComponentResult::Source::GraphText},
};
std::vector<AuxiliaryVectorCandidateBatch> graphTerms{
{.query = "related",
.weight = 1.0F,
.candidates = {{.documentHash = "baseline-anchor", .score = 1.0F},
{.documentHash = "graph-anchor", .score = 1.0F},
{.documentHash = "unanchored", .score = 1.0F}}},
};
const auto merged = yams::search::detail::mergeAuxiliaryVectorCandidates(
std::move(components), {}, std::move(graphTerms), config);
CHECK(merged.stats.graphVectorRawHitCount == 3U);
CHECK(merged.stats.graphVectorAddedNewCount == 1U);
CHECK(merged.stats.graphVectorBlockedUncorroboratedCount == 1U);
CHECK(merged.stats.graphVectorBlockedMissingBaselineTextAnchorCount == 1U);
REQUIRE(merged.components.size() == 3U);
CHECK(merged.components.back().documentHash == "baseline-anchor");
CHECK(merged.components.back().source == ComponentResult::Source::GraphVector);
CHECK(merged.components.back().score == Catch::Approx(0.5F));
}
TEST_CASE("Graph-neighbor trace separates stored relation from the selected cap",
"[unit][search][topology][graph_neighbors][trace]") {
TopologySearchFixture fix;
fix.addDocument("seed", "seed", {1.0F, 0.0F});
fix.addDocument("near", "near", {0.9F, 0.1F});
fix.addDocument("far", "far", {0.8F, 0.2F});
const auto nodeIds = fix.kgStore->upsertNodes({
KGNode{.nodeKey = "doc:seed", .type = "document"},
KGNode{.nodeKey = "doc:near", .type = "document"},
KGNode{.nodeKey = "doc:far", .type = "document"},
});
REQUIRE(nodeIds.has_value());
REQUIRE(nodeIds.value().size() == 3);
REQUIRE(fix.kgStore
->addEdge(KGEdge{.srcNodeId = nodeIds.value()[0],
.dstNodeId = nodeIds.value()[1],
.relation = "semantic_neighbor",
.weight = 0.9F})
.has_value());
REQUIRE(fix.kgStore
->addEdge(KGEdge{.srcNodeId = nodeIds.value()[0],
.dstNodeId = nodeIds.value()[2],
.relation = "semantic_neighbor",
.weight = 0.8F})
.has_value());
TopologyRoutingSessionRequest request;
request.seedDocumentHashes = {"seed"};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.expansionSource = SearchEngineConfig::TopologyExpansionSource::GraphNeighbors;
request.options.maxDocs = 1;
request.options.collectRouteMembership = true;
request.options.collectGraphDiagnostics = true;
request.options.graphNeighborMinScore = 0.0F;
request.options.graphNeighborReciprocalOnly = false;
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(result.graphNeighborTrace.collected);
CHECK(result.graphNeighborTrace.edgeFetchLimit == 64);
CHECK(result.graphNeighborTrace.fetchTruncatedSeedCount == 0);
CHECK(result.graphNeighborTrace.seedDocumentIds == std::vector<std::string>{"seed"});
CHECK(result.graphNeighborTrace.relationCandidateCount == 2);
CHECK(result.graphNeighborTrace.relationCandidateDocumentIds ==
std::vector<std::string>{"near", "far"});
CHECK(result.graphNeighborTrace.fetchedCandidateCount == 2);
CHECK(result.graphNeighborTrace.fetchedCandidateDocumentIds ==
std::vector<std::string>{"near", "far"});
CHECK(result.graphNeighborTrace.eligibleCandidateCount == 2);
CHECK(result.graphNeighborTrace.eligibleCandidateDocumentIds ==
std::vector<std::string>{"near", "far"});
REQUIRE(result.routedCandidateDocIds.size() == 1);
CHECK(result.routedCandidateDocIds.front() == "near");
}
TEST_CASE("Topology cluster hydration failure discards partial admission",
"[unit][search][topology][hydration]") {
TopologySearchFixture fix;
auto counting = std::make_shared<HashLookupCountingRepository>(*fix.pool);
fix.repo = counting;
seedTopologyDocuments(fix);
const auto batch = buildTwoClusterTopologyBatch();
TopologyRoutingSessionRequest request;
request.seedDocumentHashes = {"x1", "y1"};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.expansionSource = SearchEngineConfig::TopologyExpansionSource::Clusters;
request.options.maxClusters = 2;
request.options.maxDocs = 8;
request.options.minRouteScore = 0.0F;
request.snapshotCache = std::make_shared<TopologyRoutingSnapshotCache>(
[batch] { return Result<std::optional<yams::topology::TopologyArtifactBatch>>{batch}; });
counting->batchLookups = 0;
SECTION("all cluster lookups succeed") {
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK(result.applied);
// Non-membership expansion materializes one medoid per selected cluster.
CHECK(result.addedCandidateHashes.size() == 2);
CHECK(counting->batchLookups.load() == 2);
}
SECTION("second cluster lookup fails after the first was admitted") {
counting->failBatchAt = 2;
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK(counting->batchLookups.load() == 2);
CHECK_FALSE(result.applied);
CHECK(result.staleCandidates == 0);
CHECK(result.skipReason.starts_with("metadata_lookup_failed"));
CHECK(result.certificate.allowedDocumentHashes.empty());
CHECK(result.routedCandidateHashes.empty());
CHECK(result.addedCandidateHashes.empty());
CHECK(result.addedCandidates == 0);
}
}
TEST_CASE("Topology routing hydrates ranked candidates and trace stages in batches",
"[unit][search][topology][graph_neighbors][hydration]") {
TopologySearchFixture fix;
auto counting = std::make_shared<HashLookupCountingRepository>(*fix.pool);
fix.repo = counting;
fix.addDocument("seed", "seed", {1.0F, 0.0F});
std::vector<KGNode> nodes{KGNode{.nodeKey = "doc:seed", .type = "document"}};
for (int i = 0; i < 8; ++i) {
const auto hash = "near" + std::to_string(i);
fix.addDocument(hash, hash, {0.9F, 0.1F});
nodes.push_back(KGNode{.nodeKey = "doc:" + hash, .type = "document"});
}
const auto nodeIds = fix.kgStore->upsertNodes(nodes);
REQUIRE(nodeIds.has_value());
REQUIRE(nodeIds.value().size() == 9);
for (std::size_t i = 1; i < nodeIds.value().size(); ++i) {
REQUIRE(fix.kgStore
->addEdge(KGEdge{.srcNodeId = nodeIds.value()[0],
.dstNodeId = nodeIds.value()[i],
.relation = "semantic_neighbor",
.weight = 0.9F})
.has_value());
}
TopologyRoutingSessionRequest request;
request.seedDocumentHashes = {"seed"};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.expansionSource = SearchEngineConfig::TopologyExpansionSource::GraphNeighbors;
request.options.maxDocs = 8;
request.options.collectRouteMembership = true;
request.options.collectGraphDiagnostics = true;
request.options.graphNeighborMinScore = 0.0F;
request.options.graphNeighborReciprocalOnly = false;
counting->singleLookups = 0;
counting->batchLookups = 0;
SECTION("successful hydration remains batched") {
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(result.graphNeighborTrace.collected);
CHECK(result.graphNeighborTrace.relationCandidateCount == 8);
REQUIRE(result.routedCandidateDocIds.size() == 8);
CHECK(counting->batchLookups.load() >= 1);
CHECK(counting->singleLookups.load() == 0);
}
SECTION("a diagnostic batch failure retains per-document fallback") {
counting->failBatchAt = 1;
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK(result.applied);
CHECK(result.staleCandidates == 0);
CHECK(result.graphNeighborTrace.eligibleUnresolvedCount == 0);
CHECK(result.graphNeighborTrace.eligibleCandidateDocumentIds.size() == 8);
CHECK(counting->singleLookups.load() > 0);
}
SECTION("metadata failure is not evidence of stale documents") {
counting->failBatchLookups = true;
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK_FALSE(result.applied);
CHECK(result.staleCandidates == 0);
CHECK(result.skipReason.starts_with("metadata_lookup_failed"));
CHECK(result.certificate.allowedDocumentHashes.empty());
CHECK(result.addedCandidateHashes.empty());
}
}