Distinguish main matches from related suggestions and ship 0.1.9

This commit is contained in:
karl committed 2026-10-14 12:45:32 +02:00
1 parent a8ba594d0e
commit b0bdfc7816
40 files changed
+2463 -185

No files matched your search

+7 -1
View File
@@ -48,7 +48,7 @@ loaded fonts, image pixels, and genuine worker inference counters in debug mode.
## Artifacts and limits
Outputs under docs/screenshots are launcher-empty.png, search-light.png, search-dark.png,
media-search.png and performance-details.png, plus capture-manifest.json. The
media-search.png, related-suggestions.png and performance-details.png, plus capture-manifest.json. The
manifest contains actual fixture queries, counts/selection, measured response
metrics, device/model, viewport, PNG SHA256, runner/browser versions, Git base
commit and dirty-state flag. The base commit does not identify uncommitted
@@ -56,6 +56,12 @@ source exactly. It also records the actual frontend package version and built
index SHA-256. The compact empty-bar capture has no manufactured results and
uses the genuine ready backend. The ephemeral API token is never saved in the manifest.
The 0.1.9 capture also records exact hashes of the shared service, relevance
policy, frontend and capture sources. It checks that related suggestions start
collapsed and are skipped by result keyboard navigation, remain accessible when
expanded, and restore a visible primary selection when collapsed. The extra
related-suggestions image shows actual expanded neighbors, not invented results.
Captures are **browser-rendered production UI with the actual CPU engine**.
They omit native window chrome. They do not establish native WebView visual QA,
OS integration, playback codec parity, production relevance, GPU behavior or
+64 -8
View File
@@ -25,6 +25,12 @@ const out = resolve(
repository,
process.env.SEARCHI_CAPTURE_OUTPUT ?? "docs/screenshots",
);
const frontendVersion = JSON.parse(await readFile(
resolve(repository, "apps/desktop/package.json"), "utf8")).version;
if (process.env.SEARCHI_CAPTURE_EXPECTED_VERSION) {
assert.equal(frontendVersion, process.env.SEARCHI_CAPTURE_EXPECTED_VERSION,
"Frontend metadata must match the version intended for this capture");
}
const infoResponse = await fetch(new URL("/capture-info", target), {
signal: AbortSignal.timeout(15000),
});
@@ -35,6 +41,10 @@ assert(
"Requires an initialized real-engine capture backend",
);
await mkdir(out, { recursive: true });
assert(process.env.SEARCHI_CAPTURE_BACKEND_EXE,
"Set SEARCHI_CAPTURE_BACKEND_EXE to the actual owned backend executable for provenance");
const captureBackendSha256 = createHash("sha256")
.update(await readFile(process.env.SEARCHI_CAPTURE_BACKEND_EXE)).digest("hex");
const browser = await chromium.launch({
headless: true,
args: [
@@ -47,6 +57,7 @@ const failures = [];
const evidence = [];
const measured = [];
let latestMeasured;
const isRelated = (result) => result.match_kind?.endsWith("_related") === true;
const source = {
base_commit: execFileSync("git", ["rev-parse", "HEAD"], {
cwd: repository,
@@ -58,6 +69,17 @@ const source = {
encoding: "utf8",
}).trim().length > 0,
};
source.source_sha256 = {};
for (const name of [
"crates/searchi-service/src/lib.rs", "crates/searchi-service/src/relevance.rs",
"apps/desktop/src/main.ts", "apps/desktop/src/style.css",
"tools/desktop-capture/backend/src/main.rs", "tools/desktop-capture/capture.mjs",
"tools/relevance-eval/policies/evidence-v1.json",
]) {
source.source_sha256[name] = createHash("sha256")
.update(await readFile(resolve(repository, name))).digest("hex");
}
source.capture_backend_sha256 = captureBackendSha256;
try {
const context = await browser.newContext({
viewport: { width: 1440, height: 1000 },
@@ -139,6 +161,8 @@ try {
query: text,
mode: debug ? "debug" : "basic",
result_count: body.results.length,
main_result_count: body.results.filter((result) => !isRelated(result)).length,
related_result_count: body.results.filter(isRelated).length,
metrics: body.metrics,
first_filename: body.results[0]?.filename,
first_source: body.results[0]?.source,
@@ -160,7 +184,7 @@ try {
{
query: text,
filename: latestMeasured.first_filename,
count: latestMeasured.result_count,
count: latestMeasured.main_result_count,
},
{ timeout: 60000 },
);
@@ -168,15 +192,22 @@ try {
(await page.locator("#results .result").count()) > 0,
`Real query returned no matches: ${text}`,
);
assert.equal(await page.locator("#related-results .result").count(), 0,
"Collapsed related neighbors must stay out of keyboard/focusable result rows");
assert.equal(await page.locator("#related-suggestions").evaluate((element) => element.open),
false, "Related suggestions must be collapsed for a new query");
await page.locator("#results .result").first().click();
await page.waitForFunction(
() => !document.querySelector("#preview")?.hidden,
);
if (text === "cancel the supplier partnership") {
assert(
!body.results.some((result) => ["Mountain Lake.jpg", "Night Pier.jpg"].includes(result.filename)),
"Contract results must not contain the unrelated fixture photos",
body.results.filter((result) => ["Mountain Lake.jpg", "Night Pier.jpg"].includes(result.filename))
.every(isRelated),
"Contract photos must remain related suggestions rather than primary results",
);
assert(body.results.some((result) => result.filename === "Mountain Lake.jpg"),
"Eligible mountain photo must remain accessible as a related suggestion");
assert.equal(
latestMeasured.first_filename,
"Supplier Agreement.pdf",
@@ -251,12 +282,13 @@ try {
literal_query_marks: true,
actual_page_explanation: true,
why_disclosure_collapsed_by_default: true,
unrelated_photos_removed: true,
unrelated_photos_in_collapsed_related_group: true,
};
}
if (text === "mountains reflected in calm water") {
assert(body.results.every((result) => result.match_kind === "semantic_image"),
"Visual search shortlist must not contain the unrelated fixture documents");
assert(body.results.filter((result) => !isRelated(result))
.every((result) => result.match_kind === "semantic_image"),
"Visual search primary results must not contain unrelated fixture documents");
assert.equal(
latestMeasured.first_filename,
"Mountain Lake.jpg",
@@ -267,7 +299,7 @@ try {
0,
"Meaning-only photo result must not invent word-level alignment",
);
latestMeasured.ui_checks = { meaning_only_image_has_no_text_marks: true, unrelated_documents_removed: true };
latestMeasured.ui_checks = { meaning_only_image_has_no_text_marks: true, related_sources_retained: true };
}
await page.evaluate(
() =>
@@ -298,6 +330,9 @@ try {
query: queryText,
mode: latestMeasured.mode,
result_count: latestMeasured.result_count,
main_result_count: latestMeasured.main_result_count,
related_result_count: latestMeasured.related_result_count,
related_expanded: await page.locator("#related-suggestions").evaluate((element) => element.open),
selected_filename: selected,
first_source_kind: latestMeasured.first_source?.kind,
first_source_number: latestMeasured.first_source?.number,
@@ -322,7 +357,28 @@ try {
}
const documentQuery = "cancel the supplier partnership";
await query(documentQuery);
await page.locator("#query").press("ArrowDown");
assert.equal(await page.locator("#preview-title").innerText(), "Supplier Agreement.pdf",
"Collapsed related suggestions must not enter arrow-key selection");
latestMeasured.ui_checks.collapsed_related_skipped_by_keyboard = true;
await screenshot("search-light", documentQuery);
await page.locator("#related-suggestions > summary").focus();
await page.locator("#related-suggestions > summary").press("ArrowDown");
await page.locator("#related-results .result").first().waitFor({ state: "visible" });
assert.equal(await page.locator("#related-results .result").count(),
latestMeasured.related_result_count, "Expanding must restore every actual related neighbor");
assert.equal(await page.locator("#preview-title").innerText(),
await page.locator("#related-results .result strong").first().innerText(),
"ArrowDown on related disclosure must select its first actual candidate");
await page.locator("#results .result").first().click();
assert(await page.locator("#related-results .result").filter({ hasText: "Mountain Lake.jpg" }).count(),
"Expanded related suggestions must expose the actual mountain candidate");
await screenshot("related-suggestions", documentQuery);
await page.locator("#related-results .result").first().click();
await page.locator("#related-suggestions > summary").click();
await page.waitForFunction(() => document.querySelector("#preview-title")?.textContent === "Supplier Agreement.pdf");
assert.equal(await page.locator("#preview-title").innerText(), "Supplier Agreement.pdf",
"Collapsing related suggestions must restore visible primary selection");
await page.emulateMedia({ colorScheme: "dark" });
await screenshot("search-dark", documentQuery);
await page.emulateMedia({ colorScheme: "light" });
@@ -402,7 +458,7 @@ try {
kind: "browser-rendered-production-ui-real-cpu-engine",
source,
frontend_build: {
version: JSON.parse(await readFile(resolve(repository, "apps/desktop/package.json"), "utf8")).version,
version: frontendVersion,
index_sha256: createHash("sha256").update(await readFile(resolve(repository, "apps/desktop/dist/index.html"))).digest("hex"),
},
viewport: { width: 1440, height: 1000 },
+16
View File
@@ -869,13 +869,16 @@ name = "searchi-core"
version = "0.1.0"
dependencies = [
"anyhow",
"libc",
"lopdf",
"quick-xml",
"rusqlite",
"searchi-media",
"serde",
"serde_json",
"sha2 0.10.9",
"walkdir",
"windows-sys",
"zip",
]
@@ -896,8 +899,10 @@ name = "searchi-relevance-eval"
version = "0.1.0"
dependencies = [
"anyhow",
"rusqlite",
"searchi-core",
"searchi-service",
"searchi-speech",
"serde_json",
"sha2 0.10.9",
"tempfile",
@@ -926,11 +931,22 @@ dependencies = [
"searchi-core",
"searchi-media",
"searchi-semantic",
"searchi-speech",
"serde",
"sha2 0.10.9",
"usearch",
]
[[package]]
name = "searchi-speech"
version = "0.1.0"
dependencies = [
"anyhow",
"serde",
"serde_json",
"sha2 0.10.9",
]
[[package]]
name = "serde"
version = "1.0.229"
+2
View File
@@ -12,4 +12,6 @@ serde_json = "1"
sha2 = "0.10"
searchi-core = { path = "../../crates/searchi-core" }
searchi-service = { path = "../../crates/searchi-service" }
searchi-speech = { path = "../../crates/searchi-speech" }
rusqlite = { version = "0.32", features = ["bundled"] }
tempfile = "3"
+40
View File
@@ -1,5 +1,45 @@
# Frozen relevance diagnostic
## New absence holdout
`--bin absence` is a separate runner for `fixtures/absence-v3`, with exact
fixture and evidence-policy hashes enforced. It does not change the historical
v2 runner or its held-out gate. It materializes the 36 frozen source files into
an owned persistent profile, hashes existing copies before reuse, runs actual
Google media/text indexing with CPU speech recognition and reports per-asset
extraction errors and active semantic channels. A failed extraction or absent
required channel must not be described as successful abstention.
Set `SEARCHI_FORCE_CPU=1`, `SEARCHI_EXTRACT_WORKER`, `SEARCHI_GEMMA_MODEL_DIR`,
`SEARCHI_GEMMA_WORKER`, `SEARCHI_TEST_FFMPEG`, `SEARCHI_TEST_FFPROBE`,
`SEARCHI_SPEECH_WORKER`, `SEARCHI_SPEECH_BASE` and `SEARCHI_SPEECH_VAD` to verified
local assets. Reserve the shared CPU slot before running:
```powershell
cargo test --manifest-path tools/relevance-eval/Cargo.toml --bin absence --locked
cargo build --release --manifest-path tools/relevance-eval/Cargo.toml --bin absence --locked
tools/relevance-eval/target/release/absence.exe C:/path/to/searchi C:/path/to/searchi/.searchi/qa/absence-v3-runtime C:/path/to/searchi/.searchi/qa/absence-v3-results.json
```
Existing evidence outputs are never overwritten. Raw routed cosine results,
current primary and related results, and a replay of the previously shipped
leading-cluster hybrid rule are retained. The replay uses the actual vectors
and keyword candidates from this run; it is not execution or timing of an old
binary. Its fixture queries contain no identifier-specific search; it does not
replay the service's identifier promotion. Current search runs through the
shared service, including strict code scope. Results are capped at 20, with an
80-candidate budget. Primary absence counts and primary/accessibly retained
positive recall are separate; related suggestions are not claimed to be exact
answers. English/German and category metrics are reported separately. Four
partial requests are excluded from strict positive/absence counts.
Calls run lexical, then raw semantic, then current hybrid. Hybrid reuses the
query-vector cache, so these timings do not measure cold first user input. Do
not tune the frozen policy on this holdout. See the fixture README for label
and corpus limitations.
## Historical v2 diagnostic
This internal runner uses searchi's shared Rust engine and genuine Google
EmbeddingGemma 2. By default it indexes all 14 frozen source files as the fixed
distractor library and evaluates only the 24 calibration queries. Held-out
@@ -0,0 +1,20 @@
{
"id": "semantic-evidence-presentation-v1",
"frozen_before_new_query_holdout": true,
"holdout_fixture_sha256": "12354a55c1e5f1efe3437e653c074d247374852a420329db8d500771c4a24f53",
"main_semantic_cosine_minimum": 0.70,
"keep_existing_leading_cluster_rule": true,
"weak_and_tail_candidates": "related suggestions, retained and explicitly distinguishable",
"lexical_candidates": "retain existing filename/keyword priority and match kind",
"raw_semantic_diagnostic_api": "unchanged",
"decision_is": "presentation evidence, not probability or proof of artifact presence/absence",
"development_tradeoff": {
"text_all_positive_tops_main": "31/32",
"text_all_absent_tops_main": "0/16",
"code_positive_tops_main": "8/8",
"code_absent_tops_main": "0/4",
"media_positive_tops_main": "49/55, top-file correctness separately required",
"media_absent_tops_main": "1/6"
},
"rejected": ["hard deletion below a universal cutoff", "confidence percentages", "query/file-specific exceptions", "definite no-content claim from similarity alone"]
}
+406
View File
@@ -0,0 +1,406 @@
//! New frozen holdout: actual CPU retrieval, distinct primary/related evidence.
use anyhow::{ensure, Context, Result};
use searchi_core::{SearchIndex, SearchOptions, SearchResult};
use searchi_service::{is_related_result, FfmpegTools, HybridSearch, MediaLimits};
use searchi_speech::{Device, SpeechWorker};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::{
collections::{BTreeMap, HashMap, HashSet},
fs,
io::{Read, Write},
path::{Path, PathBuf},
time::Instant,
};
const FIXTURE: &str = "12354a55c1e5f1efe3437e653c074d247374852a420329db8d500771c4a24f53";
const POLICY: &str = "d3d4e437a54d1d62a70bb09c2882db6362a2566525a54f92d5fdc7fdfb91e26c";
fn digest(path: &Path) -> Result<String> {
let mut file = fs::File::open(path)?;
let mut hash = Sha256::new();
let mut bytes = [0; 65536];
loop {
let count = file.read(&mut bytes)?;
if count == 0 {
break;
}
hash.update(&bytes[..count]);
}
Ok(format!("{:x}", hash.finalize()))
}
fn checkpoint(path: &Path, report: &Value) -> Result<()> {
let mut staged = tempfile::NamedTempFile::new_in(path.parent().context("Output parent")?)?;
staged.write_all(&serde_json::to_vec_pretty(report)?)?;
staged.as_file().sync_all()?;
staged.persist(path)?;
Ok(())
}
fn env_path(key: &str) -> Result<PathBuf> {
PathBuf::from(std::env::var_os(key).with_context(|| format!("Set {key}"))?)
.canonicalize()
.map_err(Into::into)
}
fn copy_result(hit: &SearchResult) -> SearchResult {
SearchResult {
path: hit.path.clone(),
filename: hit.filename.clone(),
score: hit.score,
snippet: hit.snippet.clone(),
match_kind: hit.match_kind.clone(),
source: hit.source.clone(),
extraction_error: hit.extraction_error.clone(),
}
}
fn score(hits: &[SearchResult], query: &Value, assets: &[Value]) -> Result<Value> {
let expected = query["expected_ids"].as_array().context("Expected ids")?;
let mut ids = Vec::new();
for hit in hits {
let asset = assets
.iter()
.find(|a| {
Path::new(a["path"].as_str().unwrap_or(""))
.file_name()
.is_some_and(|n| n == hit.filename.as_str())
})
.context("Result outside fixture")?;
if !ids.contains(&asset["id"]) {
ids.push(asset["id"].clone());
}
}
Ok(
json!({"asset_ranking":ids,"top1_relevant":ids.first().is_some_and(|id|expected.contains(id)),"recall_at_5":if expected.is_empty(){None}else{Some(ids.iter().take(5).filter(|id|expected.contains(id)).count() as f64 / expected.len() as f64)},"any_result":!ids.is_empty()}),
)
}
// Replay the previously shipped 0.07-gap policy over actual retrieved vectors.
// This is labelled a replay, not a separately measured old binary.
fn baseline(mut semantic: Vec<SearchResult>, lexical: Vec<SearchResult>) -> Vec<SearchResult> {
let mut gaps: Vec<_> = semantic
.windows(2)
.enumerate()
.map(|(i, p)| (i + 1, p[0].score - p[1].score))
.collect();
gaps.sort_by(|a, b| b.1.total_cmp(&a.1));
if let Some(&(boundary, gap)) = gaps.first() {
if gap > 0.07 && gap - gaps.get(1).map_or(0.0, |p| p.1) > 2.0 * f32::EPSILON as f64 {
semantic.truncate(boundary);
}
}
let mut results = HashMap::new();
for (rank, mut hit) in lexical.into_iter().enumerate() {
hit.score = if hit.match_kind == "filename" {
1.15
} else {
1.0
} / (61.0 + rank as f64);
results.insert(hit.path.clone(), hit);
}
for (rank, mut hit) in semantic.into_iter().enumerate() {
let contribution = 1.0 / (61.0 + rank as f64);
if let Some(existing) = results.get_mut(&hit.path) {
existing.score += contribution;
if existing.match_kind == "content" {
existing.match_kind = "hybrid".into();
}
} else {
hit.score = contribution;
results.insert(hit.path.clone(), hit);
}
}
let tier = |hit: &SearchResult| match hit.match_kind.as_str() {
"exact_filename" => 0,
"exact_identifier" => 1,
_ => 2,
};
let mut results: Vec<_> = results.into_values().collect();
results.sort_by(|a, b| {
tier(a)
.cmp(&tier(b))
.then_with(|| b.score.total_cmp(&a.score))
.then_with(|| a.path.cmp(&b.path))
});
results.truncate(20);
results
}
fn coverage(db: &Path, assets: &[Value], corpus: &Path) -> Result<Value> {
let connection =
rusqlite::Connection::open_with_flags(db, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
let mut rows = Vec::new();
for asset in assets {
let path = corpus.join(
Path::new(asset["path"].as_str().context("Path")?)
.file_name()
.context("Filename")?,
);
let path = path.to_string_lossy().to_string();
let extraction = connection.query_row(
"SELECT extraction_error,length(content),extracted_sha256 FROM files WHERE path=?1",
[&path],
|r| {
Ok((
r.get::<_, Option<String>>(0)?,
r.get::<_, usize>(1)?,
r.get::<_, Option<String>>(2)?,
))
},
)?;
let mut channels = BTreeMap::<String, u64>::new();
let mut statement=connection.prepare("SELECT source_type,count(*) FROM semantic_chunks WHERE generation=(SELECT active_generation FROM semantic_meta WHERE singleton=1) AND path=?1 GROUP BY source_type")?;
for row in statement.query_map([&path], |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?))
})? {
let (k, v) = row?;
channels.insert(k, v);
}
rows.push(json!({"asset_id":asset["id"],"kind":asset["kind"],"extraction_error":extraction.0,"text_characters":extraction.1,"extracted_sha256":extraction.2,"semantic_channels":channels,"has_semantic_chunks":!channels.is_empty()}));
}
Ok(json!(rows))
}
fn summaries(rows: &[Value]) -> Value {
let mut groups = BTreeMap::<String, Vec<&Value>>::new();
for row in rows {
for key in [
"all".to_owned(),
format!("language:{}", row["query"]["language"].as_str().unwrap()),
format!("category:{}", row["query"]["category"].as_str().unwrap()),
] {
groups.entry(key).or_default().push(row);
}
}
let mut output = BTreeMap::new();
for (key, group) in groups {
let positives: Vec<_> = group
.iter()
.filter(|r| r["query"]["label"] == "answerable")
.collect();
let absent: Vec<_> = group
.iter()
.filter(|r| r["query"]["label"] == "absent")
.collect();
let mut metrics = json!({"positive_count":positives.len(),"absent_count":absent.len(),"partial_count":group.iter().filter(|r|r["query"]["label"]=="partial").count()});
for lane in ["raw", "baseline_replay", "primary", "accessible"] {
metrics[format!("{lane}_positive_recall5")] = json!(if positives.is_empty() {
None
} else {
Some(
positives
.iter()
.map(|r| r["scores"][lane]["recall_at_5"].as_f64().unwrap_or(0.0))
.sum::<f64>()
/ positives.len() as f64,
)
});
metrics[format!("{lane}_absent_nonempty")] = json!(absent
.iter()
.filter(|r| r["scores"][lane]["any_result"] == true)
.count());
}
output.insert(key, metrics);
}
json!(output)
}
fn main() -> Result<()> {
ensure!(
std::env::var("SEARCHI_FORCE_CPU").as_deref() == Ok("1"),
"CPU only"
);
let args: Vec<_> = std::env::args_os().skip(1).collect();
ensure!(
args.len() == 3,
"Supply REPO_ROOT PROFILE OUTPUT_JSON; runtime paths via SEARCHI_* environment"
);
let root = PathBuf::from(&args[0]).canonicalize()?;
let profile = PathBuf::from(&args[1]);
fs::create_dir_all(&profile)?;
let profile = profile.canonicalize()?;
ensure!(
profile.starts_with(root.join(".searchi/qa")),
"Profile must be owned QA directory"
);
let output = PathBuf::from(&args[2]);
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&output)?;
let fixture_path = root.join("fixtures/absence-v3/fixture.json");
let policy_path = root.join("tools/relevance-eval/policies/evidence-v1.json");
ensure!(
digest(&fixture_path)? == FIXTURE && digest(&policy_path)? == POLICY,
"Frozen input changed"
);
let fixture: Value = serde_json::from_slice(&fs::read(&fixture_path)?)?;
let assets = fixture["assets"].as_array().context("Assets")?;
let mut report = json!({"complete":false,"fixture_sha256":FIXTURE,"policy_sha256":POLICY,"cpu_threads":1,"rows":[],"phase":"verification"});
checkpoint(&output, &report)?;
let corpus = profile.join("corpus");
fs::create_dir_all(&corpus)?;
let mut names = HashSet::new();
for asset in assets {
let source = root
.join(asset["path"].as_str().context("Asset path")?)
.canonicalize()?;
ensure!(source.starts_with(&root), "Escaped source");
ensure!(
digest(&source)? == asset["sha256"]
&& fs::metadata(&source)?.len() == asset["bytes"].as_u64().context("Bytes")?,
"Source changed"
);
let name = source.file_name().context("Basename")?;
ensure!(names.insert(name.to_owned()), "Duplicate basename");
let destination = corpus.join(name);
if destination.exists() {
ensure!(
digest(&destination)? == asset["sha256"],
"Existing corpus changed"
);
} else {
let mut dest = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&destination)?;
let mut input = fs::File::open(&source)?;
std::io::copy(&mut input, &mut dest)?;
dest.sync_all()?;
}
}
ensure!(
fs::read_dir(&corpus)?.count() == assets.len(),
"Unexpected corpus file"
);
let model = env_path("SEARCHI_GEMMA_MODEL_DIR")?;
let worker = env_path("SEARCHI_GEMMA_WORKER")?;
let ffmpeg = env_path("SEARCHI_TEST_FFMPEG")?;
let ffprobe = env_path("SEARCHI_TEST_FFPROBE")?;
let speech = env_path("SEARCHI_SPEECH_WORKER")?;
let base = env_path("SEARCHI_SPEECH_BASE")?;
let vad = env_path("SEARCHI_SPEECH_VAD")?;
let manifest_path = root.join("tools/embeddinggemma2/model-manifest.json");
let manifest: Value = serde_json::from_slice(&fs::read(&manifest_path)?)?;
for asset in manifest["files"].as_array().context("Model files")? {
let path = model.join(asset["path"].as_str().context("Model path")?);
ensure!(
digest(&path)? == asset["sha256"]
&& fs::metadata(path)?.len() == asset["bytes"].as_u64().unwrap(),
"Model bytes changed"
);
}
report["runtime_hashes"] = json!({"runner":digest(&std::env::current_exe()?)?,"worker":digest(&worker)?,"speech_worker":digest(&speech)?,"speech_base":digest(&base)?,"speech_vad":digest(&vad)?,"ffmpeg":digest(&ffmpeg)?,"ffprobe":digest(&ffprobe)?,"model_manifest":digest(&manifest_path)?});
report["source_hashes"] = json!({"service_lib":digest(&root.join("crates/searchi-service/src/lib.rs"))?,"relevance":digest(&root.join("crates/searchi-service/src/relevance.rs"))?,"routing":digest(&root.join("crates/searchi-service/src/routing.rs"))?});
report["assets"] = fixture["assets"].clone();
report["phase"] = json!("indexing");
checkpoint(&output, &report)?;
let db = profile.join("index.sqlite");
let mut lexical = SearchIndex::open(&db)?;
let now = Instant::now();
report["lexical_report"] = json!(lexical.index_folder(&corpus)?);
report["lexical_ms"] = json!(now.elapsed().as_secs_f64() * 1000.0);
checkpoint(&output, &report)?;
let now = Instant::now();
let mut indexer = HybridSearch::open(&db, &model, &worker, 1)?.with_speech_provider(
Box::new(SpeechWorker::load(&speech, &base, &vad, Device::Cpu)?),
)?;
let scratch = profile.join("decoded-media");
fs::create_dir_all(&scratch)?;
let limits = MediaLimits {
scratch_directory: Some(scratch),
..Default::default()
};
report["index_report"] =
json!(indexer.index_embeddings_with_media(&FfmpegTools { ffmpeg, ffprobe }, &limits)?);
report["semantic_index_ms"] = json!(now.elapsed().as_secs_f64() * 1000.0);
drop(indexer);
report["coverage"] = coverage(&db, assets, &corpus)?;
checkpoint(&output, &report)?;
let now = Instant::now();
let mut search = HybridSearch::open_query(&db, &model, &worker, 1)?;
search.warmup()?;
report["startup_ms"] = json!(now.elapsed().as_secs_f64() * 1000.0);
report["worker_info"] = json!(search.worker_info());
report["phase"] = json!("queries");
checkpoint(&output, &report)?;
for query in fixture["queries"].as_array().context("Queries")? {
let options = SearchOptions {
code_only: query["scope"] == "code",
..Default::default()
};
let text = query["text"].as_str().context("Text")?;
let lexical_hits = lexical.search_with_options_measured(text, 80, &options, true)?;
let now = Instant::now();
let raw = search.search_semantic(text, 80, &options)?;
let raw_ms = now.elapsed().as_secs_f64() * 1000.0;
let baseline = baseline(
raw.iter().map(copy_result).collect(),
lexical_hits.results.iter().map(copy_result).collect(),
);
let measured = search.search_measured(text, 20, &options, true)?;
let primary: Vec<_> = measured
.results
.iter()
.filter(|r| !is_related_result(r))
.map(copy_result)
.collect();
let related: Vec<_> = measured
.results
.iter()
.filter(|r| is_related_result(r))
.map(copy_result)
.collect();
let scores = json!({"raw":score(&raw,query,assets)?,"baseline_replay":score(&baseline,query,assets)?,"primary":score(&primary,query,assets)?,"related":score(&related,query,assets)?,"accessible":score(&measured.results,query,assets)?});
report["rows"].as_array_mut().unwrap().push(json!({"query":query,"lexical":lexical_hits,"raw":raw,"raw_ms":raw_ms,"baseline_replay":baseline,"current":measured,"scores":scores}));
checkpoint(&output, &report)?;
}
report["summary"] = summaries(report["rows"].as_array().unwrap());
report["complete"] = json!(true);
report["phase"] = json!("complete");
report["order_note"]=json!("lexical then raw then hybrid; hybrid reuses query vector. Replay baseline has no separate runtime timing. Scores not confidence; coverage failures must be separated.");
checkpoint(&output, &report)?;
println!("{}", report["summary"]);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn hit(name: &str, score: f64) -> SearchResult {
SearchResult {
path: name.into(),
filename: name.into(),
snippet: String::new(),
score,
match_kind: "semantic".into(),
source: None,
extraction_error: None,
}
}
#[test]
fn baseline_gap_replay_keeps_best_and_flat_neighbors() {
assert_eq!(
baseline(vec![hit("a", 0.82), hit("b", 0.66), hit("c", 0.63)], vec![]).len(),
1
);
assert_eq!(
baseline(vec![hit("a", 0.60), hit("b", 0.58), hit("c", 0.56)], vec![]).len(),
3
);
assert_eq!(
baseline(vec![hit("a", 0.9), hit("b", 0.8), hit("c", 0.7)], vec![]).len(),
3
);
}
#[test]
fn baseline_preserves_lexical_after_semantic_split() {
let mut lexical = hit("c", 0.0);
lexical.match_kind = "exact_filename".into();
let result = baseline(
vec![hit("a", 0.82), hit("b", 0.66), hit("c", 0.63)],
vec![lexical],
);
assert_eq!(result.len(), 2);
assert_eq!(result[0].filename, "c");
}
#[test]
fn empty_absent_is_not_positive_recall() {
let scores = score(&[], &json!({"expected_ids":[]}), &[]).unwrap();
assert!(scores["recall_at_5"].is_null());
assert_eq!(scores["any_result"], false);
}
}