* fix(graph): handle qualified C++ symbols Signed-off-by: trvon <me@trevon.dev> * refactor(vector)!: remove dead transition APIs Split Simeon vector and retrieval linkage so consumers inherit only the runtime surface they use. BREAKING CHANGE: remove unused async/model/config helpers and the undefined vector-backend factory from public headers. Signed-off-by: trvon <me@trevon.dev> * fix(vector): harden PQ snapshot lifecycle Validate and atomically install persisted generations, revalidate after races, and fall back to exact search when a snapshot is stale. Add the exact-scan reference lane and distinguish observed work counters from unavailable backend metrics. Signed-off-by: trvon <me@trevon.dev> * feat(topology): expose route coordinates Persist chart distortion and local-dimension evidence, retain pre-scalarization route axes, and emit detailed trace rows without treating unavailable vector work as zero. Signed-off-by: trvon <me@trevon.dev> * feat(xplan): add vector decision gates Track binary and corpus identity, preserve missing-work semantics, and add repeated PQ routing, crossover, and rerank plans with quality and cost summaries. Signed-off-by: trvon <me@trevon.dev> * feat(topology): formalize coordinate routing Separate chart geometry from route-risk admission and prove candidate-stage recall and strict-focus boundaries. Record the measured atlas limitations and literature basis for the next construction experiment. Signed-off-by: trvon <me@trevon.dev> * fix(search): serialize SymSpell readers SQLite-backed lookups reuse mutable prepared statements, so shared-reader locking is unsafe. Advance the owned SymSpell revision for batched delete insertion and cover concurrent root lookups under TSAN. Signed-off-by: trvon <me@trevon.dev> * fix(vector): make mutations atomic Preserve stored vectors on replacement and commit failures, reject cross-thread writes during explicit transactions, and restore exact reranking for quantized-primary PQ rows. * fix(search)!: surface SymSpell write errors Propagate persistent dictionary failures through metadata ingestion, roll back failed batches, and make clear remove stored terms. BREAKING CHANGE: SymSpellSearch mutations now return Result values. * feat(topology): formalize candidate expansion * feat(vector): add coordinate-aware embeddings * feat(topology): preserve protected relation cover * feat(search): add graph candidate rescue * feat(daemon): expose retrieval hydration telemetry * feat(xplan): add rescue and hydration gates * refactor(content): dedupe media handlers Reuse shared graph presenter helpers and centralize repeated audio/video/archive buffer processing, validation, lifecycle, and stats helper logic. * feat(lifecycle): prune stale corpus data Keep cleanup preview-first while retention stays opt-in and long prune work no longer holds IPC responses open. * feat(search): add topology candidate rescue Preserve high-value routed candidates across vector backends while keeping exact rerank diagnostics explicit. * feat(search): expose evidence paths * docs: define mobile host contract * formal: model bounded candidate rescue * fix(prune): inspect extensionless signatures * fix(prune): tolerate stale extensions --------- Signed-off-by: trvon <me@trevon.dev>
191 lines
6.1 KiB
C++
191 lines
6.1 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <yams/compat/unistd.h>
|
|
#include <yams/vector/embedding_generator.h>
|
|
#include <yams/vector/simeon_embedding_backend.h>
|
|
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
#include <catch2/matchers/catch_matchers_string.hpp>
|
|
|
|
using namespace yams::vector;
|
|
using Catch::Matchers::StartsWith;
|
|
|
|
namespace {
|
|
|
|
EmbeddingConfig make_cfg(size_t dim = 256) {
|
|
EmbeddingConfig cfg;
|
|
cfg.backend = EmbeddingConfig::Backend::Simeon;
|
|
cfg.embedding_dim = dim;
|
|
cfg.normalize_embeddings = true;
|
|
cfg.max_sequence_length = 512;
|
|
return cfg;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("SimeonBackend initializes and reports its dimension", "[vector][simeon]") {
|
|
auto backend = makeSimeonBackend(make_cfg(384));
|
|
REQUIRE(backend != nullptr);
|
|
REQUIRE(backend->isAvailable());
|
|
REQUIRE(backend->initialize());
|
|
REQUIRE(backend->isInitialized());
|
|
REQUIRE(backend->getEmbeddingDimension() == 384u);
|
|
REQUIRE(backend->getBackendName() == "Simeon");
|
|
}
|
|
|
|
TEST_CASE("SimeonBackend fixed hash profile exposes the frozen retrieval space",
|
|
"[vector][simeon][identity]") {
|
|
auto cfg = make_cfg(1024);
|
|
cfg.simeon_encoder_profile = EmbeddingConfig::SimeonEncoderProfile::FixedHash384;
|
|
|
|
auto backend = makeSimeonBackend(cfg);
|
|
REQUIRE(backend != nullptr);
|
|
REQUIRE(backend->initialize());
|
|
CHECK(backend->getEmbeddingDimension() == 384u);
|
|
CHECK(backend->getEmbeddingSpaceIdentity() == "simeon-v1-384");
|
|
CHECK(simeonEmbeddingSpaceIdentity(cfg) == "simeon-v1-384");
|
|
}
|
|
|
|
TEST_CASE("SimeonBackend produces deterministic, unit-norm embeddings", "[vector][simeon]") {
|
|
auto backend = makeSimeonBackend(make_cfg(256));
|
|
REQUIRE(backend->initialize());
|
|
|
|
const std::string text = "The quick brown fox jumps over the lazy dog.";
|
|
auto r1 = backend->generateEmbedding(text);
|
|
auto r2 = backend->generateEmbedding(text);
|
|
REQUIRE(r1);
|
|
REQUIRE(r2);
|
|
REQUIRE(r1.value().size() == 256u);
|
|
REQUIRE(r1.value() == r2.value());
|
|
|
|
double acc = 0.0;
|
|
for (float f : r1.value())
|
|
acc += static_cast<double>(f) * static_cast<double>(f);
|
|
REQUIRE(std::fabs(std::sqrt(acc) - 1.0) < 1e-4);
|
|
}
|
|
|
|
TEST_CASE("SimeonBackend batch matches per-item encoding", "[vector][simeon]") {
|
|
auto backend = makeSimeonBackend(make_cfg(128));
|
|
REQUIRE(backend->initialize());
|
|
|
|
const std::vector<std::string> texts = {"alpha bravo charlie", "delta echo foxtrot",
|
|
"golf hotel india"};
|
|
auto per = std::vector<std::vector<float>>{};
|
|
for (const auto& t : texts) {
|
|
auto r = backend->generateEmbedding(t);
|
|
REQUIRE(r);
|
|
per.push_back(std::move(r).value());
|
|
}
|
|
auto batch = backend->generateEmbeddings(std::span<const std::string>(texts));
|
|
REQUIRE(batch);
|
|
REQUIRE(batch.value().size() == per.size());
|
|
for (size_t i = 0; i < per.size(); ++i) {
|
|
REQUIRE(batch.value()[i] == per[i]);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("EmbeddingGenerator selects Simeon via env override", "[vector][simeon]") {
|
|
setenv("YAMS_EMBED_BACKEND", "simeon", 1);
|
|
|
|
EmbeddingConfig cfg;
|
|
cfg.embedding_dim = 384;
|
|
cfg.normalize_embeddings = true;
|
|
EmbeddingGenerator gen(cfg);
|
|
REQUIRE(gen.initialize());
|
|
REQUIRE(gen.getBackendName() == "Simeon");
|
|
REQUIRE(gen.getEmbeddingDimension() == 384u);
|
|
|
|
auto v = gen.generateEmbedding("hello world");
|
|
REQUIRE(v.size() == 384u);
|
|
|
|
unsetenv("YAMS_EMBED_BACKEND");
|
|
}
|
|
|
|
TEST_CASE("EmbeddingGenerator selects Simeon when configured directly", "[vector][simeon]") {
|
|
EmbeddingConfig cfg;
|
|
cfg.backend = EmbeddingConfig::Backend::Simeon;
|
|
cfg.embedding_dim = 128;
|
|
cfg.normalize_embeddings = true;
|
|
|
|
EmbeddingGenerator gen(cfg);
|
|
REQUIRE(gen.initialize());
|
|
REQUIRE(gen.getBackendName() == "Simeon");
|
|
REQUIRE(gen.getEmbeddingDimension() == 128u);
|
|
}
|
|
|
|
TEST_CASE("EmbeddingGenerator moved-from observers report neutral state",
|
|
"[vector][embedding-generator][move][catch2]") {
|
|
EmbeddingGenerator source(make_cfg(128));
|
|
EmbeddingGenerator destination(std::move(source));
|
|
|
|
CHECK_FALSE(source.isInitialized());
|
|
CHECK((source.estimateTokenCount("moved-from") == 0));
|
|
CHECK((source.getLastError().empty()));
|
|
CHECK_FALSE(source.hasError());
|
|
CHECK((destination.getConfig().embedding_dim == 128));
|
|
}
|
|
|
|
TEST_CASE("EmbeddingGenerator selects Daemon via env override", "[vector][daemon]") {
|
|
setenv("YAMS_EMBED_BACKEND", "daemon", 1);
|
|
|
|
EmbeddingConfig cfg;
|
|
cfg.backend = EmbeddingConfig::Backend::Simeon;
|
|
EmbeddingGenerator gen(cfg);
|
|
|
|
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
|
|
|
|
unsetenv("YAMS_EMBED_BACKEND");
|
|
}
|
|
|
|
TEST_CASE("EmbeddingGenerator maps legacy onnx env override to Daemon", "[vector][daemon]") {
|
|
setenv("YAMS_EMBED_BACKEND", "onnx", 1);
|
|
|
|
EmbeddingConfig cfg;
|
|
cfg.backend = EmbeddingConfig::Backend::Simeon;
|
|
EmbeddingGenerator gen(cfg);
|
|
|
|
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
|
|
|
|
unsetenv("YAMS_EMBED_BACKEND");
|
|
}
|
|
|
|
TEST_CASE("EmbeddingGenerator maps onnxruntime env override to Daemon path", "[vector][daemon]") {
|
|
setenv("YAMS_EMBED_BACKEND", "onnxruntime", 1);
|
|
|
|
EmbeddingConfig cfg;
|
|
cfg.backend = EmbeddingConfig::Backend::Simeon;
|
|
EmbeddingGenerator gen(cfg);
|
|
|
|
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
|
|
|
|
unsetenv("YAMS_EMBED_BACKEND");
|
|
}
|
|
|
|
TEST_CASE("EmbeddingGenerator maps OnnxRuntime config to Daemon path", "[vector][daemon]") {
|
|
EmbeddingConfig cfg;
|
|
cfg.backend = EmbeddingConfig::Backend::OnnxRuntime;
|
|
|
|
EmbeddingGenerator gen(cfg);
|
|
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
|
|
}
|
|
|
|
TEST_CASE("EmbeddingGenerator maps legacy Hybrid config to Daemon path", "[vector][daemon]") {
|
|
EmbeddingConfig cfg;
|
|
cfg.backend = EmbeddingConfig::Backend::Hybrid;
|
|
|
|
EmbeddingGenerator gen(cfg);
|
|
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
|
|
}
|
|
|
|
TEST_CASE("EmbeddingGenerator selects Daemon when configured directly", "[vector][daemon]") {
|
|
EmbeddingConfig cfg;
|
|
cfg.backend = EmbeddingConfig::Backend::Daemon;
|
|
|
|
EmbeddingGenerator gen(cfg);
|
|
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
|
|
}
|