feat: highlight search matches and document the working UI
Core CI / core (ubuntu-latest) (push) Failing after 3m29s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 7m23s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s

This commit is contained in:
karl committed 2026-10-12 19:57:34 +02:00
1 parent b8400c4fbf
commit 79553ce921
26 files changed
+4103 -29

No files matched your search

+38
View File
@@ -5,6 +5,12 @@
Local desktop search for Windows, macOS and Linux, under active development.
Production readiness is tracked in [the roadmap](docs/ROADMAP.md).
![searchi finding a supplier agreement and its matching passage](docs/screenshots/search-light.png)
*The production UI searching real fixture files with Google EmbeddingGemma 2.
Captured on Windows with CPU inference in the read-only local browser capture harness;
native window chrome is omitted.*
## What works
Search combines instant indexed filenames, SQLite FTS5 content retrieval, and
@@ -35,6 +41,38 @@ Windows VM tests use CPU only. Files, extracted text, metadata and embeddings
stay local; treat the index as sensitive. Initial verified model provisioning
requires approximately 865 MB of downloads. Search performs no remote inference.
## See searchi working
### A familiar dark theme
![The same real document search in searchi's dark theme](docs/screenshots/search-dark.png)
Light and dark themes follow the operating system preference. The selected
result retains its source location and extracted passage. Matching query words
are highlighted in results and previews. A small, collapsed **Why this result?**
detail is available when you want the explanation.
### Find a photo from what you remember
![A natural-language search finding the mountain lake photo](docs/screenshots/media-search.png)
Actual image pixels are indexed by the Google model. The selected photo is
loaded from the indexed fixture file for preview.
### Understand where the time goes
![searchi's detailed performance panel showing real search and inference timings](docs/screenshots/performance-details.png)
Normal searches show a compact timing summary. Optional performance details
break down the real request into measured stages, including worker inference,
retrieval, filtering and ranking.
The [capture workflow](tools/desktop-capture/README.md) and
[capture manifest](docs/screenshots/capture-manifest.json) record the queries,
runtime, measured responses and image hashes. The photo sources and licenses
are listed in [the media fixture documentation](fixtures/media/README.md).
These browser captures supplement the open native desktop QA gates.
## Copy-and-paste setup prompt
```text
+16
View File
@@ -104,3 +104,19 @@ The local clipboard report excludes queries, paths, filenames, snippets and erro
strings. It captures available model/device/thread/runtime context without
guessing missing values. Histories are bounded and dropped counts remain visible.
Clipboard failure is explicit; no diagnostic telemetry is uploaded.
## Reproducible documentation capture
The optional loopback harness in `tools/desktop-capture` serves this production-built
frontend against the actual shared Rust search engine and local Google model.
Browser activation requires explicit `?capture=1`, a loopback HTTP origin and a
ready `readme-capture` bootstrap with an ephemeral nonce. Read-only search/status
commands use authenticated same-origin requests; indexed media uses restricted
fixture URLs. Indexing, dialogs, external opening and resource-setting mutations
are disabled. Ordinary browser previews remain disconnected and native Tauri
invocations/media URLs retain their existing implementation.
These documentation images show browser-rendered application content backed by
real engine queries. They do not establish native OS chrome, global shortcuts,
native playback or desktop interaction QA. The harness returns no mocked search
results or replayed model output.
+100
View File
@@ -0,0 +1,100 @@
/** Explicit, read-only bridge for documented browser captures of the real engine.
* This never activates in a normal browser preview or remote website. */
const commands = new Set([
"stats",
"desktop_status",
"semantic_status",
"search",
"hybrid_search",
"search_measured",
"hybrid_search_measured",
"prepare_media_preview",
]);
export function captureRequested(href: string) {
const url = new URL(href);
return (
url.searchParams.get("capture") === "1" &&
url.protocol === "http:" &&
["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
);
}
export class CaptureBridge {
private token: string | null = null;
private origin: string | null = null;
get active() {
return this.token !== null;
}
async initialize(href: string, request: typeof fetch = fetch) {
this.token = null;
this.origin = null;
if (!captureRequested(href)) return false;
const origin = new URL(href).origin;
const response = await request(`${origin}/capture-info`, {
credentials: "omit",
cache: "no-store",
redirect: "error",
});
if (!response.ok)
throw new Error("The local capture engine is unavailable.");
const info = (await response.json()) as {
mode?: unknown;
token?: unknown;
ready?: unknown;
};
if (
info.mode !== "readme-capture" ||
info.ready !== true ||
typeof info.token !== "string" ||
info.token.length < 32 ||
info.token.length > 512
)
throw new Error(
"The local endpoint did not identify a ready read-only capture engine.",
);
this.origin = origin;
this.token = info.token;
return true;
}
async invoke<T>(
command: string,
args?: Record<string, unknown>,
request: typeof fetch = fetch,
): Promise<T> {
if (!this.active || !this.origin)
throw new Error("Run the desktop app to use real local search.");
if (!commands.has(command))
throw new Error(
"This action is unavailable in the read-only documentation capture.",
);
const response = await request(`${this.origin}/capture-api/${command}`, {
method: "POST",
credentials: "omit",
cache: "no-store",
redirect: "error",
headers: {
"Content-Type": "application/json",
"X-Searchi-Capture-Token": this.token!,
},
body: JSON.stringify(args ?? {}),
});
const value = await response.json();
if (!response.ok) throw value;
return value as T;
}
mediaURL(path: string) {
if (
!this.active ||
!this.origin ||
!/^\/capture-media\/[A-Za-z0-9_-]+(?:\.[A-Za-z0-9]+)?$/.test(path)
)
throw new Error(
"Capture media must be an indexed fixture URL issued by the local engine.",
);
return new URL(path, this.origin).href;
}
}
+30
View File
@@ -0,0 +1,30 @@
import {
invoke as nativeInvoke,
isTauri,
convertFileSrc,
} from "@tauri-apps/api/core";
import { CaptureBridge } from "./capture-bridge";
const capture = new CaptureBridge();
export const isNativeDesktop = () => isTauri();
export const isCaptureSurface = () => capture.active;
export const isDesktopSurface = () => isTauri() || capture.active;
export async function initializeDesktopSurface() {
if (isTauri()) return;
await capture.initialize(window.location.href);
}
export function invoke<T>(
command: string,
args?: Record<string, unknown>,
): Promise<T> {
return isTauri()
? nativeInvoke<T>(command, args)
: capture.invoke<T>(command, args);
}
export function prepareMediaURL(path: string) {
if (isTauri()) return convertFileSrc(path);
return capture.mediaURL(path);
}
+119 -24
View File
@@ -1,4 +1,11 @@
import { invoke, isTauri, convertFileSrc } from "@tauri-apps/api/core";
import {
invoke,
initializeDesktopSurface,
isNativeDesktop,
isDesktopSurface,
isCaptureSurface,
prepareMediaURL,
} from "./desktop-api";
import { listen } from "@tauri-apps/api/event";
import {
AutomaticIndexGate,
@@ -14,6 +21,13 @@ import {
briefMs,
} from "./search-performance";
import type { MeasuredResponse, SearchLane } from "./search-performance";
import { displayPath } from "./path-display";
import {
literalQuery,
renderHighlighted,
displayExcerpt,
matchExplanation,
} from "./match-display";
import "@fontsource-variable/inter/wght.css";
import "./style.css";
import {
@@ -95,7 +109,7 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
<form id="search-form" role="search"><label class="sr-only" for="query">Search your files</label>${icon("search")}<input id="query" type="search" placeholder="Search a filename, phrase, or what you remember…" autocomplete="off" spellcheck="false" maxlength="1000"><kbd id="focus-shortcut">Ctrl K</kbd><button id="clear-query" class="icon-button" type="button" aria-label="Clear search" title="Clear search">${icon("dismiss")}</button></form>
<details id="semantic-panel"><summary>${icon("sparkles")}<span id="semantic-status" role="status" aria-live="polite">Preparing local semantic search…</span><span id="semantic-device" class="kind">Local model</span>${icon("chevron")}</summary><div class="semantic-content"><strong>Google EmbeddingGemma 2</strong><progress id="semantic-progress" aria-label="Local model setup progress"></progress><p id="semantic-detail" class="hint">The verified local model downloads automatically on first launch (865 MB). Your files stay on this device.</p><div class="semantic-actions"><button id="retry-model" type="button" hidden>Retry model setup</button><button id="rebuild-semantic" type="button" hidden>Rebuild semantic index</button></div></div></details>
<form id="folder-form" hidden><label for="folder">Folder to index</label><div class="folder-row"><input id="folder" placeholder="Absolute folder path" required><button id="browse-folder" type="button">Browse…</button><button type="submit">Index folder</button><button id="cancel-folder" type="button">Cancel</button></div><p class="hint">Only choose folders you want searched. Large folders may take a while.</p></form>
<p id="notice" role="status" aria-live="polite"></p><details id="performance-panel" hidden><summary>Performance details</summary><div id="performance-detail"></div><button id="copy-performance" type="button">Copy local timing report</button><span id="copy-performance-status" role="status"></span></details><div class="workspace"><section class="results-pane" aria-label="Search results"><p id="result-summary" class="eyebrow">READY WHEN YOU ARE</p><ul id="results" aria-label="Matching files"></ul><div id="empty">${icon("search")}<h2>Find what matters. Locally.</h2><p>Index a folder, then search a filename, phrase, or what you remember.</p></div></section><aside id="preview" aria-label="Result preview" hidden><div class="preview-header"><span id="preview-icon" class="file-icon"></span><div class="preview-heading"><h2 id="preview-title"></h2><p id="preview-path" class="path"></p></div></div><div class="preview-toolbar"><span id="preview-kind"></span><div class="preview-actions"><button id="open-result" type="button">${icon("open")}<span>Open file</span></button><button id="reveal-result" type="button" title="Show in folder" aria-label="Show in folder">${icon("folder")}</button></div></div><div id="media-preview" class="media-preview" hidden></div><p id="media-preview-status" class="hint" role="status" hidden></p><div class="passage"><p id="passage-label" class="eyebrow">EXTRACTED PASSAGE</p><pre id="preview-text"></pre></div></aside></div>
<p id="notice" role="status" aria-live="polite"></p><details id="performance-panel" hidden><summary>Performance details</summary><div id="performance-detail"></div><button id="copy-performance" type="button">Copy local timing report</button><span id="copy-performance-status" role="status"></span></details><div class="workspace"><section class="results-pane" aria-label="Search results"><p id="result-summary" class="eyebrow">READY WHEN YOU ARE</p><ul id="results" aria-label="Matching files"></ul><div id="empty">${icon("search")}<h2>Find what matters. Locally.</h2><p>Index a folder, then search a filename, phrase, or what you remember.</p></div></section><aside id="preview" aria-label="Result preview" hidden><div class="preview-header"><span id="preview-icon" class="file-icon"></span><div class="preview-heading"><h2 id="preview-title"></h2><p id="preview-path" class="path"></p></div></div><div class="preview-toolbar"><span id="preview-kind"></span><div class="preview-actions"><button id="open-result" type="button">${icon("open")}<span>Open file</span></button><button id="reveal-result" type="button" title="Show in folder" aria-label="Show in folder">${icon("folder")}</button></div></div><details id="match-details" class="match-details"><summary>Why this result?</summary><p id="match-reason"></p><p id="match-legend"></p></details><div id="media-preview" class="media-preview" hidden></div><p id="media-preview-status" class="hint" role="status" hidden></p><div class="passage"><p id="passage-label" class="eyebrow">EXTRACTED PASSAGE</p><pre id="preview-text"></pre></div></aside></div>
</main><footer><span class="footer-privacy">${icon("lock")}<span>Private search. Everything stays on your device.</span><span id="privacy" class="privacy">Local</span></span><span class="keyboard-hints"><kbd>${icon("arrowUp")}</kbd><kbd>${icon("arrowDown")}</kbd><span>Navigate</span><kbd>Enter</kbd><span>Open</span><kbd>Esc</kbd><span>Clear</span></span></footer>`;
const element = <T extends HTMLElement>(id: string) =>
@@ -110,6 +124,8 @@ try {
/* Diagnostics remain opt-in when storage is unavailable. */
}
let results: SearchResult[] = [];
let displayedLiteralQuery = literalQuery("");
let explainedSelection = "";
let selected = -1;
let requestId = 0;
let timer: ReturnType<typeof setTimeout>;
@@ -166,7 +182,7 @@ const mediaLane = new LatestLane<PreviewRequest>(
const image = document.createElement("img");
image.alt = request.result.filename;
image.addEventListener("error", failed, { once: true });
image.src = convertFileSrc(prepared.path);
image.src = prepareMediaURL(prepared.path);
host.append(image);
} else {
const player = document.createElement(prepared.kind);
@@ -194,7 +210,7 @@ const mediaLane = new LatestLane<PreviewRequest>(
},
{ once: true },
);
player.src = convertFileSrc(prepared.path);
player.src = prepareMediaURL(prepared.path);
host.append(player);
}
host.hidden = false;
@@ -363,13 +379,14 @@ async function refreshSemanticStatus() {
}
const retry = element<HTMLButtonElement>("retry-model");
retry.hidden = !error || status.ready;
retry.disabled = working;
retry.disabled = working || isCaptureSurface();
const rebuild = element<HTMLButtonElement>("rebuild-semantic");
rebuild.hidden = !status.ready || (!status.chunks && !status.pending);
rebuild.disabled = working || indexingActive;
rebuild.disabled = working || indexingActive || isCaptureSurface();
element<HTMLSelectElement>("cpu-threads").disabled =
working || indexingActive;
working || indexingActive || isCaptureSurface();
if (
!isCaptureSurface() &&
status.ready &&
status.stale &&
status.pending > 0 &&
@@ -411,6 +428,7 @@ async function refreshDesktopStatus() {
const threads = element<HTMLSelectElement>("cpu-threads");
threads.value = String(status.settings.cpu_threads);
threads.disabled =
isCaptureSurface() ||
status.indexing ||
semanticJobActive ||
Boolean(
@@ -418,18 +436,19 @@ async function refreshDesktopStatus() {
semanticStatus?.warming ||
semanticStatus?.embedding,
);
element<HTMLButtonElement>("add-folder").disabled = status.indexing;
element<HTMLButtonElement>("add-folder").disabled =
status.indexing || isCaptureSurface();
const roots = element("roots");
roots.replaceChildren();
for (const root of status.settings.roots) {
const item = document.createElement("li");
const path = document.createElement("span");
path.textContent = root;
path.textContent = displayPath(root);
path.className = "path";
const reindex = document.createElement("button");
reindex.type = "button";
reindex.textContent = "Reindex";
reindex.disabled = status.indexing;
reindex.disabled = status.indexing || isCaptureSurface();
reindex.addEventListener("click", () => {
element("folder-form").hidden = false;
element<HTMLInputElement>("folder").value = root;
@@ -467,21 +486,57 @@ function select(index: number, focus = false) {
const selectedIcon = fileIcon(result.filename);
element("preview-icon").innerHTML = selectedIcon.html;
element("preview-icon").dataset.type = selectedIcon.type;
element("preview-title").textContent = result.filename;
element("preview-path").textContent = result.path;
renderHighlighted(
element("preview-title"),
result.filename,
displayedLiteralQuery,
);
renderHighlighted(
element("preview-path"),
displayPath(result.path),
displayedLiteralQuery,
);
element("preview-kind").textContent =
`${matchLabel(result.match_kind)}${result.source ? ` · ${sourceLabel(result.source)}` : ""}`;
const media = mediaKind(result.filename);
element("passage-label").textContent = media
? "MATCHED CONTENT"
const explanation = matchExplanation(result);
const semanticMedia = [
"semantic_image",
"semantic_video",
"semantic_audio",
].includes(result.match_kind);
element("passage-label").textContent = semanticMedia
? "MATCHED MEDIA"
: explanation.semanticPassage
? "MATCHED PASSAGE"
: "EXTRACTED PASSAGE";
element("preview-text").textContent =
const passageText =
result.snippet ||
(result.extraction_error
? `Content could not be extracted: ${result.extraction_error}`
: media
? result.source?.matched_query || "Preview the matched media below."
? "Preview the matched media below."
: "No extracted text is available for this file.");
renderHighlighted(
element("preview-text"),
passageText,
semanticMedia ? literalQuery("") : displayedLiteralQuery,
);
document
.querySelector<HTMLElement>(".passage")!
.classList.toggle("semantic-passage", explanation.semanticPassage);
element("match-reason").textContent = explanation.reason;
element("match-legend").textContent = explanation.legend;
const explanationKey = JSON.stringify([
result.path,
result.match_kind,
result.source?.kind,
result.source?.number,
displayedLiteralQuery.query,
]);
if (explanationKey !== explainedSelection)
element<HTMLDetailsElement>("match-details").open = false;
explainedSelection = explanationKey;
element<HTMLButtonElement>("open-result").title = result.source
? `Open in your default application. The matched location ${sourceLabel(result.source)} is shown in Searchi; default application opening starts at its own location.`
: "Open in your default application";
@@ -491,7 +546,7 @@ function select(index: number, focus = false) {
if (selection.changed) {
mediaLane.clear();
unloadMedia();
if (media && isTauri()) {
if (media && isDesktopSurface()) {
element("media-preview-status").hidden = false;
element("media-preview-status").textContent = "Preparing media preview…";
mediaLane.submit({ revision: selection.revision, result });
@@ -555,10 +610,23 @@ function render() {
resultRows.set(result.path, entry);
}
entry.button.dataset.index = String(i);
entry.button.title = result.path;
entry.title.textContent = result.filename;
entry.path.textContent = result.path;
entry.snippet.textContent = result.snippet;
entry.button.title = displayPath(result.path);
renderHighlighted(entry.title, result.filename, displayedLiteralQuery);
renderHighlighted(
entry.path,
displayPath(result.path),
displayedLiteralQuery,
);
const semanticMedia = [
"semantic_image",
"semantic_video",
"semantic_audio",
].includes(result.match_kind);
renderHighlighted(
entry.snippet,
displayExcerpt(result.snippet, displayedLiteralQuery),
semanticMedia ? literalQuery("") : displayedLiteralQuery,
);
entry.kind.textContent = `${matchLabel(result.match_kind)}${result.source ? ` · ${sourceLabel(result.source)}` : ""}`;
const asset = fileIcon(result.filename);
if (entry.file.dataset.type !== asset.type) {
@@ -627,6 +695,7 @@ function applyResults(
const resultsHadFocus = list.contains(document.activeElement);
shownLane = lane;
displayedRequestId = request.id;
displayedLiteralQuery = literalQuery(request.query);
results = found;
selected = -1;
render();
@@ -643,7 +712,7 @@ function applyResults(
const metric = request.measurement.latestNative(lane);
const trace = request.measurement.export();
element("result-summary").textContent =
`${found.length} RESULTS · ${lane === "hybrid" ? "KEYWORD + MEANING" : "KEYWORD"} · NATIVE ${briefMs(metric?.total_ms ?? 0)} MS · ${lane === "hybrid" ? "FINAL" : "FIRST RESPONSE"} ${Math.round(lane === "hybrid" ? (trace.hybrid_final_results_ms ?? 0) : (trace.first_results_ms ?? 0))} MS`;
`${found.length} RESULTS · ${lane === "hybrid" ? "KEYWORD + MEANING" : "KEYWORD"} · SEARCH ${briefMs(metric?.total_ms ?? 0)} MS · SHOWN ${briefMs(trace.latest_displayed_ms ?? 0)} MS`;
refreshPerformance(request);
}
@@ -801,6 +870,7 @@ async function search(inputAt = performance.now()) {
if (!value) {
element("performance-panel").hidden = true;
results = [];
displayedLiteralQuery = literalQuery("");
displayedRequestId = null;
selected = -1;
render();
@@ -1044,10 +1114,28 @@ document.addEventListener("keydown", (event) => {
}
}
});
if (isTauri()) {
async function initializeSurface() {
await initializeDesktopSurface();
if (isDesktopSurface()) {
if (isCaptureSurface()) {
for (const id of [
"add-folder",
"open-result",
"reveal-result",
"browse-folder",
"retry-model",
"rebuild-semantic",
"cpu-threads",
]) {
const control = element<HTMLButtonElement>(id);
control.disabled = true;
control.title = "Unavailable in the read-only documentation capture";
}
}
void refreshStats().catch(showError);
void refreshDesktopStatus().catch(showError);
void refreshSemanticStatus();
if (isNativeDesktop())
void listen("focus-search", () => {
query.focus();
query.select();
@@ -1059,7 +1147,7 @@ if (isTauri()) {
if (indexingActive) void refreshDesktopStatus().catch(showError);
}, 2000);
query.focus();
} else {
} else {
element("index-status").textContent = "Desktop preview";
element("semantic-status").textContent =
"Native semantic search runs in the desktop app.";
@@ -1067,4 +1155,11 @@ if (isTauri()) {
element("semantic-detail").textContent =
"This browser preview does not download models or return simulated search results.";
showError("Run the Tauri desktop app to index and search real files.");
}
}
void initializeSurface().catch((error) => {
element("index-status").textContent = "Capture unavailable";
element("semantic-status").textContent =
"The real local capture engine could not initialize.";
showError(error);
});
+287
View File
@@ -0,0 +1,287 @@
/** Literal text evidence only. Semantic relevance never implies word importance. */
import { timestampLabel } from "./media-display.ts";
const fold = (value: string) =>
value.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
const stopwords = new Set(
"a an and are as at be been but by can could did do does for from had has have how i if in into is it its me my of on or our should so than that the their them there these they this those to was we were what when where which who will with would you your am auch auf aus bei bin bis das dass dem den der des die dieser du ein eine einer einem einen eines er es für hat ich im ist mit nach nicht noch nur oder sein sind so sie sich und uns vom von vor war waren was wie wir wird zu zum zur"
.split(" ")
.map(fold),
);
export interface LiteralQuery {
readonly query: string;
readonly patterns: readonly string[];
}
export interface HighlightPart {
text: string;
matched: boolean;
}
export function literalQuery(value: string): LiteralQuery {
const query = value.slice(0, 4096);
const patterns = new Set<string>();
const add = (text: string) => {
const normalized = fold(text.trim());
if (
normalized &&
!stopwords.has(normalized) &&
/[\p{L}\p{N}]/u.test(normalized) &&
patterns.size < 64
)
patterns.add(normalized);
};
const meaningful = (text: string) =>
[...fold(text).matchAll(/[\p{L}\p{N}]+/gu)].some(
(word) => !stopwords.has(word[0]),
);
// Fixed parsers only: user input is never compiled as a regular expression.
for (const phrase of query.matchAll(/["“]([^"”]+)["”]/gu)) {
if (meaningful(phrase[1])) add(phrase[1]);
}
const unquoted = query.replace(/["“”]/gu, "");
const words = unquoted.split(/\s+/u);
for (const raw of words) {
const word = raw.replace(/^[,;:!?()[\]{}]+|[,;:!?()[\]{}.]+$/gu, "");
add(word);
if (patterns.size >= 64) break;
}
return Object.freeze({
query,
patterns: Object.freeze([...patterns].sort((a, b) => b.length - a.length)),
});
}
export function highlightParts(
text: string,
query: LiteralQuery,
): HighlightPart[] {
if (!text || !query.patterns.length) return [{ text, matched: false }];
const offsets: { start: number; end: number }[] = [];
let normalized = "";
// Index bounded text only; untouched suffixes still preserve the complete source.
for (const segment of text
.slice(0, 65_536)
.matchAll(/[^\p{M}]\p{M}*|\p{M}+/gu)) {
const value = fold(segment[0]);
normalized += value;
for (let index = 0; index < value.length; index++)
offsets.push({
start: segment.index,
end: segment.index + segment[0].length,
});
}
const intervals: { start: number; end: number }[] = [];
for (const pattern of query.patterns) {
let from = 0;
let matches = 0;
while (matches < 256) {
const start = normalized.indexOf(pattern, from);
if (start < 0) break;
const end = start + pattern.length;
const identifier = /[_-]/u.test(pattern);
const left = normalized.slice(Math.max(0, start - 2), start);
const right = normalized.slice(end, end + 2);
const joinedLeft = identifier
? /[\p{L}\p{N}_-]$/u.test(left)
: /[\p{L}\p{N}]$/u.test(left);
const joinedRight = identifier
? /^[\p{L}\p{N}_-]/u.test(right)
: /^[\p{L}\p{N}]/u.test(right);
if (!joinedLeft && !joinedRight && offsets[start] && offsets[end - 1]) {
intervals.push({
start: offsets[start].start,
end: offsets[end - 1].end,
});
matches++;
}
from = end;
}
}
intervals.sort((a, b) => a.start - b.start || b.end - a.end);
const merged: { start: number; end: number }[] = [];
for (const interval of intervals) {
const previous = merged.at(-1);
if (previous && interval.start < previous.end)
previous.end = Math.max(previous.end, interval.end);
else merged.push({ ...interval });
}
const parts: HighlightPart[] = [];
let position = 0;
for (const interval of merged.slice(0, 256)) {
if (position < interval.start)
parts.push({
text: text.slice(position, interval.start),
matched: false,
});
parts.push({
text: text.slice(interval.start, interval.end),
matched: true,
});
position = interval.end;
}
if (position < text.length)
parts.push({ text: text.slice(position), matched: false });
return parts.length ? parts : [{ text, matched: false }];
}
const rendered = new WeakMap<
HTMLElement,
{ text: string; query: LiteralQuery }
>();
const excerpts = new WeakMap<LiteralQuery, Map<string, string>>();
export function displayExcerpt(
text: string,
query: LiteralQuery,
length = 420,
) {
if (text.length <= length) return text;
let cache = excerpts.get(query);
if (!cache) {
cache = new Map();
excerpts.set(query, cache);
}
const key = `${length}:${text}`;
const stored = cache.get(key);
if (stored !== undefined) return stored;
let position = 0;
let firstMatch = 0;
for (const part of highlightParts(text, query)) {
if (part.matched) {
firstMatch = position;
break;
}
position += part.text.length;
}
let start = Math.max(0, firstMatch - 100);
if (start > 0 && /[\uDC00-\uDFFF]/u.test(text[start])) start--;
let end = Math.min(text.length, start + length);
if (end < text.length && /[\uD800-\uDBFF]/u.test(text[end - 1])) end--;
const excerpt = `${start ? "… " : ""}${text.slice(start, end)}${end < text.length ? " …" : ""}`;
if (cache.size >= 128) cache.delete(cache.keys().next().value!);
cache.set(key, excerpt);
return excerpt;
}
export function renderHighlighted(
target: HTMLElement,
text: string,
query: LiteralQuery,
) {
const previous = rendered.get(target);
if (previous?.text === text && previous.query === query) return;
const doc = target.ownerDocument;
const fragment = doc.createDocumentFragment();
for (const part of highlightParts(text, query)) {
if (part.matched) {
const mark = doc.createElement("mark");
mark.dataset.match = "literal";
mark.textContent = part.text;
fragment.append(mark);
} else fragment.append(doc.createTextNode(part.text));
}
target.replaceChildren(fragment);
rendered.set(target, { text, query });
}
interface MatchResult {
match_kind: string;
source?: { kind: string; number: number };
}
export function matchExplanation(result: MatchResult) {
const source = result.source;
const location =
source?.kind === "page"
? ` on page ${source.number}`
: source?.kind === "paragraph"
? ` in paragraph ${source.number}`
: "";
const time =
source?.kind === "timestamp_ms"
? ` near ${timestampLabel(source.number)}`
: "";
const literalLegend =
"Marked words are literal query matches, ignoring case and accents.";
switch (result.match_kind) {
case "semantic_image":
return {
reason:
"Related image content. The match applies to the image as a whole.",
legend: literalLegend,
semanticPassage: false,
};
case "semantic_video":
return {
reason: `Related video frame${time}. The timestamp is the indexed frame position.`,
legend: literalLegend,
semanticPassage: false,
};
case "semantic_audio":
return {
reason: `Related audio${time}. The preview starts at an indexed segment, not an exact spoken phrase.`,
legend: literalLegend,
semanticPassage: false,
};
case "hybrid":
return {
reason: `Query words and related meaning matched this file; showing the passage${location}.`,
legend: `${literalLegend} Meaning can match without the same words.`,
semanticPassage: false,
};
case "semantic":
return {
reason: `Related passage${location}. Similar meaning was found in this passage or its file title.`,
legend: `${literalLegend} Meaning can match without the same words.`,
semanticPassage: true,
};
case "exact_filename":
return {
reason: "The filename matches your query.",
legend: literalLegend,
semanticPassage: false,
};
case "filename":
return {
reason: "Your query appears in the filename.",
legend: literalLegend,
semanticPassage: false,
};
case "path":
return {
reason: "Your query appears in the file path.",
legend: literalLegend,
semanticPassage: false,
};
case "name_terms":
return {
reason: "Query words matched indexed filename, path or content terms.",
legend: literalLegend,
semanticPassage: false,
};
case "media_metadata":
return {
reason: "Query words matched artist, title or other media metadata.",
legend: literalLegend,
semanticPassage: false,
};
case "exact_identifier":
return {
reason: "An exact identifier matched extracted content.",
legend: literalLegend,
semanticPassage: false,
};
case "content":
case "keyword":
return {
reason: `Query words matched extracted content${location}.`,
legend: literalLegend,
semanticPassage: false,
};
default:
return {
reason: "This result was returned by local search.",
legend: literalLegend,
semanticPassage: false,
};
}
}
+6
View File
@@ -0,0 +1,6 @@
/** Explorer-style labels; callers retain the original path for file operations. */
export function displayPath(path: string) {
if (path.startsWith("\\\\?\\UNC\\")) return "\\\\" + path.slice(8);
if (/^\\\\\?\\[A-Za-z]:\\/.test(path)) return path.slice(4);
return path;
}
+45
View File
@@ -836,3 +836,48 @@ footer {
margin-left: 8px;
color: var(--muted);
}
mark[data-match="literal"] {
background: #ffebae;
color: #31280c;
border-radius: 2px;
padding: 0 1px;
font-weight: 600;
box-decoration-break: clone;
}
.passage.semantic-passage {
background: var(--selected);
border-left: 3px solid var(--brand);
}
.match-details {
margin: 0 20px 12px;
color: var(--muted);
font-size: 11px;
}
.match-details summary {
width: fit-content;
cursor: pointer;
}
.match-details p {
margin: 6px 0 0;
line-height: 1.5;
}
#match-legend {
font-size: 10px;
}
@media (prefers-color-scheme: dark) {
mark[data-match="literal"] {
background: #65531e;
color: #fff2bd;
}
}
@media (forced-colors: active) {
mark[data-match="literal"] {
background: Highlight;
color: HighlightText;
outline: 1px solid Highlight;
}
.passage.semantic-passage {
background: Canvas;
border-color: Highlight;
}
}
@@ -0,0 +1,58 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {CaptureBridge, captureRequested} from '../src/capture-bridge.ts';
const ready = () => new Response(JSON.stringify({mode: 'readme-capture', ready: true, token: 'a'.repeat(64)}));
test('ordinary previews and remote pages never initialize a capture bridge', async () => {
const bridge = new CaptureBridge();
let called = 0;
for (const href of ['http://127.0.0.1:1431/', 'https://example.com/?capture=1', 'file:///tmp/demo?capture=1', 'http://127.0.0.1:1431/?capture=0']) {
assert.equal(captureRequested(href), false);
assert.equal(await bridge.initialize(href, async () => { called++; return ready(); }), false);
}
assert.equal(called, 0);
assert.equal(bridge.active, false);
});
test('ready identity and nonce are required before any command dispatch', async () => {
for (const info of [{mode:'other',ready:true,token:'a'.repeat(64)}, {mode:'readme-capture',ready:false,token:'a'.repeat(64)}, {mode:'readme-capture',ready:true,token:'short'}]) {
const bridge = new CaptureBridge();
await assert.rejects(bridge.initialize('http://127.0.0.1:1431/?capture=1', async () => new Response(JSON.stringify(info))));
assert.equal(bridge.active, false);
await assert.rejects(bridge.invoke('stats', {}, async () => assert.fail('Must not dispatch')));
}
});
test('read-only commands use authenticated same-origin requests and mutation is rejected', async () => {
const bridge = new CaptureBridge();
await bridge.initialize('http://127.0.0.1:1431/?capture=1', async () => ready());
let request;
const result = await bridge.invoke('search_measured', {query:'real query'}, async (url, options) => {
request = {url, options}; return new Response(JSON.stringify({results:[],metrics:{total_ms:1}}));
});
assert.equal(result.metrics.total_ms, 1);
assert.equal(request.url, 'http://127.0.0.1:1431/capture-api/search_measured');
assert.equal(request.options.headers['X-Searchi-Capture-Token'], 'a'.repeat(64));
assert.equal(request.options.redirect, 'error');
assert.equal(request.options.credentials, 'omit');
for (const command of ['open_result','index_folder','set_cpu_threads','../../other']) {
await assert.rejects(bridge.invoke(command, {}, async () => assert.fail('Mutation must not dispatch')), /read-only/);
}
});
test('a failed reinitialization cannot retain earlier capture authorization', async () => {
const bridge = new CaptureBridge();
await bridge.initialize('http://127.0.0.1:1431/?capture=1', async () => ready());
await assert.rejects(bridge.initialize('http://127.0.0.1:1431/?capture=1', async () => new Response('{}')));
assert.equal(bridge.active, false);
await assert.rejects(bridge.invoke('stats', {}, async () => assert.fail('No stale authorization')));
});
test('media resolver rejects file paths, remote URLs and traversal; failed measured payload survives', async () => {
const bridge = new CaptureBridge();
await bridge.initialize('http://127.0.0.1:1431/?capture=1', async () => ready());
assert.equal(bridge.mediaURL('/capture-media/cat'), 'http://127.0.0.1:1431/capture-media/cat');
for (const path of ['C:/secret.jpg','https://example.com/cat','//example.com/cat','/capture-media/../secret','/capture-media/%2e%2e','/capture-media/cat?token=other']) assert.throws(() => bridge.mediaURL(path));
await assert.rejects(bridge.invoke('hybrid_search_measured', {}, async () => new Response(JSON.stringify({message:'busy',metrics:{total_ms:4}}), {status:503})), error => error.message === 'busy' && error.metrics.total_ms === 4);
});
+82
View File
@@ -0,0 +1,82 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {literalQuery,highlightParts,renderHighlighted,displayExcerpt,matchExplanation} from '../src/match-display.ts';
const marked = (text,query) => highlightParts(text,literalQuery(query)).filter(part => part.matched).map(part => part.text);
test('literal marks use whole words, identifiers, quoted phrases and filename separators', () => {
assert.deepEqual(marked('Contract contractual subcontract CONTRACT.', 'contract'), ['Contract','CONTRACT']);
assert.deepEqual(marked('E_SEARCH_071 E_SEARCH_0712', 'E_SEARCH_071'), ['E_SEARCH_071']);
assert.deepEqual(marked('invoice_october.pdf', 'invoice october'), ['invoice','october']);
assert.deepEqual(marked('Give end of contract notice. Contract end.', '"end of contract"'), ['end of contract','Contract','end']);
assert.deepEqual(marked('Use C++ and literal a+b, not aab.', 'C++ a+b'), ['C++','a+b']);
});
test('accent and case matching preserves original combining characters and surrogate offsets', () => {
const source='🧾 Café Cafe\u0301 MÜNCHEN München. 𐐀cafe';
const parts=highlightParts(source,literalQuery('cafe munchen'));
assert.equal(parts.map(part => part.text).join(''),source);
assert.deepEqual(parts.filter(part => part.matched).map(part => part.text), ['Café','Cafe\u0301','MÜNCHEN','München']);
assert.equal(parts[0].text, '🧾 ');
});
test('common English/German stopwords and punctuation do not create noisy evidence', () => {
assert.deepEqual(marked('The and für über supplier', 'the and für'), []);
assert.deepEqual(marked('The and. Supplier.', 'the and.'), []);
assert.deepEqual(marked('The supplier partnership may end.', 'cancel the supplier partnership'), ['supplier','partnership']);
});
test('query snapshots and mark runs are bounded; later inputs cannot relabel older results', () => {
let input='supplier';
const snapshot=literalQuery(input);
input='garden';
assert.equal(snapshot.query,'supplier');
assert.deepEqual(highlightParts('supplier garden',snapshot).filter(part => part.matched).map(part => part.text),['supplier']);
assert.equal(Object.isFrozen(snapshot.patterns),true);
assert.ok(literalQuery('x'.repeat(9000)).query.length <= 4096);
assert.ok(literalQuery(Array.from({length:100},(_,i)=>`term${i}`).join(' ')).patterns.length <= 64);
assert.ok(marked('supplier '.repeat(1000),'supplier').length <= 256);
});
test('list excerpts remain original text around the first literal hit and retain ellipsis', () => {
const text='Intro '.repeat(200)+'supplier partnership notice '+ 'Closing '.repeat(200);
const excerpt=displayExcerpt(text,literalQuery('supplier partnership'));
assert.ok(excerpt.length <= 424);
assert.ok(excerpt.startsWith('… '));
assert.ok(excerpt.endsWith(' …'));
assert.ok(excerpt.includes('supplier partnership'));
assert.ok(text.includes(excerpt.slice(2,-2)));
});
test('bounded marks retain the earliest hit across terms instead of favoring longest-term repetition', () => {
const source='short '+ 'longerterm '.repeat(400);
const parts=highlightParts(source,literalQuery('short longerterm'));
assert.equal(parts[0].text,'short');
assert.equal(parts[0].matched,true);
assert.ok(parts.filter(part=>part.matched).length <= 256);
assert.equal(parts.map(part=>part.text).join(''),source);
});
test('markup and regex-looking queries create text nodes and marks, never parsed HTML', () => {
const nodes=[];
const doc={createDocumentFragment:()=>({append:node=>nodes.push(node)}),createElement:tag=>({tag,dataset:{},textContent:''}),createTextNode:text=>({text})};
const target={ownerDocument:doc,replaceChildren:()=>{}};
const source='<img src=x onerror=evil()> supplier [a-z].';
renderHighlighted(target,source,literalQuery('supplier [a-z]'));
assert.equal(nodes.map(node=>node.text ?? node.textContent).join(''),source);
assert.ok(nodes.some(node=>node.tag==='mark' && node.textContent==='supplier'));
assert.ok(nodes.every(node=>node.tag===undefined || node.tag==='mark'));
});
test('optional reasons use real source routes and never claim semantic word confidence', () => {
const text=matchExplanation({match_kind:'semantic',source:{kind:'page',number:2}});
assert.match(text.reason,/page 2/);
assert.match(text.legend,/without the same words/);
assert.equal(text.semanticPassage,true);
assert.match(matchExplanation({match_kind:'semantic_audio',source:{kind:'timestamp_ms',number:13129}}).reason,/0:13.*not an exact spoken phrase/);
assert.match(matchExplanation({match_kind:'semantic_video',source:{kind:'timestamp_ms',number:24167}}).reason,/frame.*0:24/);
assert.match(matchExplanation({match_kind:'semantic_image'}).reason,/whole/);
assert.match(matchExplanation({match_kind:'name_terms'}).reason,/filename, path or content/);
assert.equal(matchExplanation({match_kind:'hybrid',source:{kind:'page',number:2}}).semanticPassage,false);
assert.doesNotMatch(JSON.stringify(text),/confidence|percent|importance/);
});
+1
View File
@@ -6,6 +6,7 @@
"lib": ["ES2022", "DOM"],
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"skipLibCheck": true
},
"include": ["src"]
+36 -4
View File
@@ -1,16 +1,48 @@
# Design QA
final result: blocked
final result: browser captures checked; native desktop visual gate remains open
## Browser-rendered implementation evidence (2026-10-12)
Four 1440×1000 captures now show the production frontend against the actual
shared Rust/Google CPU engine: document search in both themes, an indexed photo
preview, and measured performance details. The selected PDF result really maps
to page 2 and its notice clause. Photo pixels are loaded from the indexed file;
the trace includes actual worker inference counters. Capture assertions passed,
and every final PNG was opened and inspected.
The brand mark, local typography, two-pane structure, selection, passage text,
photo preview and diagnostics render visibly. No primary selected content is
clipped. Windows namespace prefixes are hidden only in display labels; file
operations retain the original indexed paths. The ordinary summary uses clear
Search/Shown timing labels. The last partially visible result is inside the
scrollable result list.
The final captures also check literal query-word marks in filenames, snippets
and the extracted passage. A small **Why this result?** disclosure stays
collapsed by default in the selected preview. Its page-2 explanation was opened
and checked during capture, then closed for the ordinary screenshots. Semantic
passage emphasis identifies the retrieved text; it does not claim model-derived
word importance. Photo preview text contains no invented literal matches.
These captures omit native window chrome and do not verify native WebView
behavior, OS integration, animation, playback or native keyboard/focus behavior.
The runner uses reduced motion and an owned CPU-rendered headless browser.
Its responses come from the live engine without result replay or UI mocks.
See [the capture workflow](tools/desktop-capture/README.md) and
[capture manifest](docs/screenshots/capture-manifest.json).
## Native desktop gate
Target: Karl's selected first search/preview concept, combined with Raycast-style
compact interaction and the supplied searchi brand board. Source images were
opened and inspected. The frontend implementation builds and its state tests pass.
Rendered comparison is unavailable: the in-app browser bootstrap fails because
Native rendered comparison is unavailable: the in-app browser bootstrap fails because
its browser-service module cannot be found. Prior native Windows capture attempts
returned GetCursorPos access denied and monitor capture error 0x80070057.
No matching-state implementation screenshot has been captured. No side-by-side
visual comparison or visual pass is claimed.
No matching-state native implementation screenshot has been captured. A native
side-by-side visual comparison and visual pass remain unverified.
The native development app launched in forced-CPU mode. Its accessibility tree
could be read without screenshots and exposed a missing font-import error, which
+17 -1
View File
@@ -9,7 +9,7 @@ and `SEARCHI_FORCE_CPU=1`.
- Native desktop: **9/9 tests passed**, including structured trace/basic responses.
Post-generation cached query warmup is implemented but its interactive first-query gate remains open.
Frontend: **18/18 tests passed**, TypeScript/Vite production build and
Frontend: **31/31 tests passed**, TypeScript/Vite production build and
formatting passed. Preview policy tests cover fractional timestamp seeking,
image labels, one active/latest pending request, stale replies and Escape.
- Semantic adapter: **8 default tests passed**; the explicit 48 MP resource test
@@ -75,6 +75,22 @@ mirroring; they do not run merely because code is pushed to Gitea. The current
workflow checks do not automatically certify downloaded Google assets or the
separately built worker. CI configuration is not remote job evidence.
## README capture evidence (2026-10-12)
The README now includes four visually inspected browser captures of the
production frontend over a real, retained Google CPU engine: light/dark PDF
search, image search and granular performance details. The independent capture
backend passed three tests and strict Clippy. Live capture assertions verified
the first PDF result and page 2, its notice clause, loaded photo pixels, and
actual worker inference counters. All captures use an owned headless browser
with CPU flags and licensed/generated fixture content; no recorded responses
are replayed. These checks supplement native desktop QA, which remains open.
Literal highlights are checked in result snippets and the actual preview text.
The small match explanation is closed by default and can be opened to inspect
the real page location. Media placeholder text receives no invented word marks.
[Reproduction](../tools/desktop-capture/README.md),
[runtime/query/image evidence](screenshots/capture-manifest.json).
## Historical evidence
Initial engine/CLI checks covered retrieval, persistence, filesystem changes,
+450
View File
@@ -0,0 +1,450 @@
{
"kind": "browser-rendered-production-ui-real-cpu-engine",
"source": {
"base_commit": "b8400c4fbf4038820c383ed429207a3b339bf0a5",
"working_tree_dirty": true
},
"viewport": {
"width": 1440,
"height": 1000
},
"runner": {
"playwright": "1.64.0",
"chromium": "156.0.8078.4"
},
"backend": {
"mode": "readme-capture",
"model": "google/embeddinggemma-2",
"device": "cpu",
"runtime": "b11461-no-logits-v1",
"threads": 1,
"dimensions": 768,
"ready": true,
"rust_build_profile": "debug"
},
"cpu_flags": [
"--disable-gpu",
"--disable-accelerated-video-decode",
"--disable-webgl"
],
"native_window_chrome": false,
"native_visual_qa_complete": false,
"measured_queries": [
{
"query": "cancel the supplier partnership",
"mode": "basic",
"result_count": 7,
"metrics": {
"dropped_spans": 0,
"result_count": 7,
"total_ms": 84.7475,
"trace": null
},
"first_filename": "Supplier Agreement.pdf",
"first_source": {
"kind": "page",
"number": 2
},
"ui_checks": {
"literal_query_marks": true,
"actual_page_explanation": true,
"why_disclosure_collapsed_by_default": true
}
},
{
"query": "mountains reflected in calm water",
"mode": "basic",
"result_count": 7,
"metrics": {
"dropped_spans": 0,
"result_count": 7,
"total_ms": 83.50019999999999,
"trace": null
},
"first_filename": "Mountain Lake.jpg",
"first_source": {
"kind": "image",
"number": 0
},
"ui_checks": {
"meaning_only_image_has_no_text_marks": true
}
},
{
"query": "cancel the supplier partnership",
"mode": "debug",
"result_count": 7,
"metrics": {
"dropped_spans": 0,
"result_count": 7,
"total_ms": 81.5728,
"trace": [
{
"counters": {},
"duration_ms": 81.5544,
"name": "hybrid.search",
"parent": null,
"start_ms": 0.0106
},
{
"counters": {},
"duration_ms": 1.5203,
"name": "lexical.search",
"parent": 0,
"start_ms": 0.0281
},
{
"counters": {
"query_bytes": 31
},
"duration_ms": 0.0383,
"name": "lexical.normalization",
"parent": 1,
"start_ms": 0.0386
},
{
"counters": {
"frequency_cache_hits": 29,
"frequency_cache_misses": 0,
"trigram_index": 1
},
"duration_ms": 0.1389,
"name": "lexical.candidate_plan",
"parent": 1,
"start_ms": 0.0871
},
{
"counters": {},
"duration_ms": 0.1952,
"name": "lexical.exact_filename",
"parent": 1,
"start_ms": 0.2358
},
{
"counters": {
"filters_active": 0,
"rows": 0
},
"duration_ms": 0.16620000000000001,
"name": "lexical.filtered_sql",
"parent": 4,
"start_ms": 0.254
},
{
"counters": {},
"duration_ms": 0.183,
"name": "lexical.filename_substring",
"parent": 1,
"start_ms": 0.44539999999999996
},
{
"counters": {
"filters_active": 0,
"rows": 0
},
"duration_ms": 0.15589999999999998,
"name": "lexical.filtered_sql",
"parent": 6,
"start_ms": 0.4609
},
{
"counters": {},
"duration_ms": 0.1605,
"name": "lexical.path_substring",
"parent": 1,
"start_ms": 0.642
},
{
"counters": {
"filters_active": 0,
"rows": 0
},
"duration_ms": 0.1332,
"name": "lexical.filtered_sql",
"parent": 8,
"start_ms": 0.6575
},
{
"counters": {},
"duration_ms": 0.1908,
"name": "lexical.content",
"parent": 1,
"start_ms": 0.8159000000000001
},
{
"counters": {
"filters_active": 0,
"rows": 0
},
"duration_ms": 0.1622,
"name": "lexical.filtered_sql",
"parent": 10,
"start_ms": 0.8313
},
{
"counters": {},
"duration_ms": 0.21509999999999999,
"name": "lexical.filename_terms",
"parent": 1,
"start_ms": 1.0205
},
{
"counters": {
"filters_active": 0,
"rows": 0
},
"duration_ms": 0.1858,
"name": "lexical.filtered_sql",
"parent": 12,
"start_ms": 1.0362
},
{
"counters": {},
"duration_ms": 0.27109999999999995,
"name": "lexical.media_metadata",
"parent": 1,
"start_ms": 1.2537
},
{
"counters": {
"filters_active": 0,
"rows": 0
},
"duration_ms": 0.2419,
"name": "lexical.filtered_sql",
"parent": 14,
"start_ms": 1.2715999999999998
},
{
"counters": {},
"duration_ms": 0.0155,
"name": "hybrid.identifier_priority",
"parent": 0,
"start_ms": 1.5598999999999998
},
{
"counters": {
"failed": 0,
"request_encode_us": 20,
"request_write_us": 54,
"response_validate_us": 94,
"worker_batch_prepare_us": 16,
"worker_inference_us": 77535,
"worker_pool_normalize_us": 18,
"worker_token_count": 13,
"worker_tokenization_us": 146,
"worker_wait_and_response_decode_us": 78781
},
"duration_ms": 79.06530000000001,
"name": "semantic.query_embedding",
"parent": 0,
"start_ms": 1.5843999999999998
},
{
"counters": {
"general_lane": 1
},
"duration_ms": 0.8277,
"name": "semantic.routing",
"parent": 0,
"start_ms": 80.6811
},
{
"counters": {
"ann_passes": 1,
"filesystem_checks": 2,
"result_files": 7
},
"duration_ms": 0.7831,
"name": "semantic.retrieval",
"parent": 18,
"start_ms": 80.71549999999999
},
{
"counters": {},
"duration_ms": 0.1451,
"name": "semantic.generation_snapshot",
"parent": 19,
"start_ms": 80.73429999999999
},
{
"counters": {
"cache_hit": 1
},
"duration_ms": 0.017499999999999998,
"name": "semantic.ann_load",
"parent": 19,
"start_ms": 80.89150000000001
},
{
"counters": {},
"duration_ms": 0.0108,
"name": "semantic.filter_normalization",
"parent": 19,
"start_ms": 80.9172
},
{
"counters": {
"cache_hit": 1,
"eligible_chunks": 8
},
"duration_ms": 0.0245,
"name": "semantic.eligibility",
"parent": 19,
"start_ms": 80.93599999999999
},
{
"counters": {
"requested_chunks": 8
},
"duration_ms": 0.49450000000000005,
"name": "semantic.ann_pass",
"parent": 19,
"start_ms": 80.9745
},
{
"counters": {
"returned_chunks": 8
},
"duration_ms": 0.036000000000000004,
"name": "semantic.ann_search",
"parent": 24,
"start_ms": 80.9829
},
{
"counters": {
"filesystem_checks": 2,
"unique_parents": 7
},
"duration_ms": 0.4331,
"name": "semantic.parent_materialization",
"parent": 24,
"start_ms": 81.0271
},
{
"counters": {
"valid": 1
},
"duration_ms": 0.1684,
"name": "semantic.parent_filesystem_validation",
"parent": 26,
"start_ms": 81.1383
},
{
"counters": {
"valid": 1
},
"duration_ms": 0.0964,
"name": "semantic.parent_filesystem_validation",
"parent": 26,
"start_ms": 81.333
},
{
"counters": {
"lexical_files": 0,
"semantic_files": 7
},
"duration_ms": 0.0385,
"name": "hybrid.rank_fusion",
"parent": 0,
"start_ms": 81.5188
}
]
},
"first_filename": "Supplier Agreement.pdf",
"first_source": {
"kind": "page",
"number": 2
},
"ui_checks": {
"literal_query_marks": true,
"actual_page_explanation": true,
"why_disclosure_collapsed_by_default": true
}
}
],
"screenshots": [
{
"file": "search-light.png",
"sha256": "a8347729d7995d0354fd45c1d0762d3fe33adbf0dfcf9ac3387073e522710b15",
"query": "cancel the supplier partnership",
"mode": "basic",
"result_count": 7,
"selected_filename": "Supplier Agreement.pdf",
"first_source_kind": "page",
"first_source_number": 2,
"theme": "light",
"summary": "7 RESULTS · KEYWORD + MEANING · SEARCH 85 MS · SHOWN 543 MS",
"state_assertions": {
"semantic_ready": true,
"hybrid_final": true,
"image_visible": false,
"actual_worker_inference": false,
"literal_query_marks": true,
"actual_page_explanation": true,
"why_disclosure_collapsed_by_default": true
}
},
{
"file": "search-dark.png",
"sha256": "b8cf9505ada4afb439edd469d58868fff92dd99cf9b62b6d4a5127307c1e36a7",
"query": "cancel the supplier partnership",
"mode": "basic",
"result_count": 7,
"selected_filename": "Supplier Agreement.pdf",
"first_source_kind": "page",
"first_source_number": 2,
"theme": "dark",
"summary": "7 RESULTS · KEYWORD + MEANING · SEARCH 85 MS · SHOWN 543 MS",
"state_assertions": {
"semantic_ready": true,
"hybrid_final": true,
"image_visible": false,
"actual_worker_inference": false,
"literal_query_marks": true,
"actual_page_explanation": true,
"why_disclosure_collapsed_by_default": true
}
},
{
"file": "media-search.png",
"sha256": "db832104e3b2fc59bd07a4ea393d38d363c00385023e5f9e469726e076e9b9fe",
"query": "mountains reflected in calm water",
"mode": "basic",
"result_count": 7,
"selected_filename": "Mountain Lake.jpg",
"first_source_kind": "image",
"first_source_number": 0,
"theme": "light",
"summary": "7 RESULTS · KEYWORD + MEANING · SEARCH 84 MS · SHOWN 519 MS",
"state_assertions": {
"semantic_ready": true,
"hybrid_final": true,
"image_visible": true,
"actual_worker_inference": false,
"meaning_only_image_has_no_text_marks": true
}
},
{
"file": "performance-details.png",
"sha256": "149753ca90cda5d81254d43e15118b61325a61d7f0fb366b783d4e2dfac5d96d",
"query": "cancel the supplier partnership",
"mode": "debug",
"result_count": 7,
"selected_filename": "Supplier Agreement.pdf",
"first_source_kind": "page",
"first_source_number": 2,
"theme": "light",
"summary": "7 RESULTS · KEYWORD + MEANING · SEARCH 82 MS · SHOWN 514 MS",
"state_assertions": {
"semantic_ready": true,
"hybrid_final": true,
"image_visible": false,
"actual_worker_inference": true,
"literal_query_marks": true,
"actual_page_explanation": true,
"why_disclosure_collapsed_by_default": true
}
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

+60
View File
@@ -0,0 +1,60 @@
# Real-engine desktop screenshots
This independent software-test runner launches its own bundled headless Chromium
with CPU rendering/video-decode flags. It does not control the user's Chrome,
reuse personal profiles, or use Windows UI Automation. Screenshots render the
production frontend against the actual shared Rust/Google EmbeddingGemma 2 engine;
responses are neither replayed nor supplied by page evaluation.
Playwright 1.64.0 is pinned in the package lock. See the official
[browser installation](https://playwright.dev/docs/browsers) and
[screenshot API](https://playwright.dev/docs/screenshots). The browser binaries
stay under this tool's ignored node_modules directory.
## Prepare
Build the production frontend from apps/desktop with `npm ci && npm run build`.
Build/stage the CPU Google worker and verified media helpers as documented in
[BUILDING](../../docs/BUILDING.md). Provision the actual Google models first.
The backend uses a dedicated generated/verified fixture corpus, never a user
home/document directory. It indexes documents and two licensed real photos,
then warms its retained CPU worker before listening on 127.0.0.1:1431.
```powershell
$env:SEARCHI_FORCE_CPU = '1'
cargo run --manifest-path tools/desktop-capture/backend/Cargo.toml -- --root .searchi/readme-capture/files --db .searchi/readme-capture/index.sqlite --models .searchi/models/embeddinggemma2 --worker .searchi/runtime/cpu/Release/searchi-gemma-worker.exe --dist apps/desktop/dist --ffmpeg .searchi/media-runtime/bin/ffmpeg.exe --ffprobe .searchi/media-runtime/bin/ffprobe.exe
```
Keep that owned test process running while capturing. Its loopback API is a
fixture-only test adapter with origin/token checks, not a product HTTP server.
The production Tauri app keeps its native command/asset-protocol interface.
In another terminal, from the repository root:
```powershell
npm ci --prefix tools/desktop-capture
$env:PLAYWRIGHT_BROWSERS_PATH = '0'
npm run --prefix tools/desktop-capture install-browser
npm run --prefix tools/desktop-capture capture
```
The runner always uses the local browser cache and its own temporary profile.
CPU flags are `--disable-gpu --disable-accelerated-video-decode --disable-webgl`.
Only loopback fixture origin requests are permitted during capture. It waits for
actual backend readiness, measured hybrid responses, matching DOM results,
loaded fonts, image pixels, and genuine worker inference counters in debug mode.
## Artifacts and limits
Outputs under docs/screenshots are search-light.png, search-dark.png,
media-search.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
source exactly. The ephemeral API token is never saved in the manifest.
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
other operating-system releases. The runner closes only its browser; stop only
the separately launched owned backend when finished.
File diff suppressed because it is too large. Load diff
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "searchi-desktop-capture"
version = "0.1.0"
edition = "2021"
publish = false
[workspace]
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tiny_http = "0.12"
mime_guess = "2"
uuid = { version = "1", features = ["v4"] }
sha2 = "0.10"
rusqlite = { version = "0.32", features = ["bundled"] }
lopdf = "0.45"
searchi-core = { path = "../../../crates/searchi-core" }
searchi-service = { path = "../../../crates/searchi-service" }
searchi-media = { path = "../../../crates/searchi-media" }
[dev-dependencies]
tempfile = "3"
+45
View File
@@ -0,0 +1,45 @@
# Real-engine screenshot backend
This independent software-test server serves the built desktop frontend and
connects its explicit `?capture=1` adapter to the actual shared Rust engine.
It is not the packaged desktop application or a production HTTP API.
The dedicated corpus contains five generated documents, including a genuine
two-page Supplier Agreement PDF, plus the licensed mountain-lake/night-pier
photos from `fixtures/media/fixture.json` (image03/image05). Photo checksums and
original license attribution are retained there. Tests verify actual PDF page-2
retrieval and unchanged-file reuse. No screenshots/results/vectors are replayed.
Build the frontend, then run this standalone Cargo crate with `--locked`:
```powershell
cargo run --manifest-path tools/desktop-capture/backend/Cargo.toml --locked -- `
--root .searchi/desktop-capture/corpus `
--db .searchi/desktop-capture/index.db `
--models .searchi/models/embeddinggemma2 `
--worker .searchi/runtime/cpu/Release/searchi-gemma-worker.exe `
--dist apps/desktop/dist `
--ffmpeg .searchi/media-runtime/bin/ffmpeg.exe `
--ffprobe .searchi/media-runtime/bin/ffprobe.exe
```
Indexing, real CPU model inference and prewarming finish before the server
listens on `http://127.0.0.1:1431/?capture=1`. It forces CPU mode and one model
thread. Runtime never downloads anything. Existing matching corpus files are
not rewritten; incompatible files or database roots are rejected.
`GET /capture-info` returns the explicit mode, readiness, a per-process token
and actual worker information. `POST /capture-api/{command}` requires the exact
loopback Host, same-origin Origin and `X-Searchi-Capture-Token`, with a 64 KiB
body cap. Only desktop/status/stats, measured lexical/hybrid search and fixed
indexed-photo preview commands exist. Indexing, file opening, settings and
arbitrary filesystem access are not browser commands. Static assets are
canonicalized beneath the supplied frontend build; media IDs are only `lake`
and `pier`. There are no CORS allowances or remote bindings.
The operator must supply an empty dedicated corpus root and database; the
server must not be pointed at user documents. This intentionally small fixture
demonstrates real behavior, not production-scale performance. Host/Origin/token,
path containment, unrelated-file rejection, PDF extraction and fixture reuse
have CPU tests. The browser runner records actual receipts and screenshot
measurements separately.
+519
View File
@@ -0,0 +1,519 @@
//! Independent screenshot test backend, not a production HTTP service.
use anyhow::{bail, ensure, Context, Result};
use clap::Parser;
use searchi_core::{SearchIndex, SearchOptions};
use searchi_service::{FfmpegTools, HybridSearch, MediaLimits};
use serde::Deserialize;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::{
collections::BTreeMap,
fs,
io::Read,
path::{Component, Path, PathBuf},
};
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
#[derive(Parser)]
struct Args {
/// Dedicated empty/generated corpus directory. Never a user document root.
#[arg(long)]
root: PathBuf,
#[arg(long)]
models: PathBuf,
#[arg(long)]
worker: PathBuf,
#[arg(long)]
db: PathBuf,
#[arg(long)]
dist: PathBuf,
#[arg(long)]
ffmpeg: PathBuf,
#[arg(long)]
ffprobe: PathBuf,
}
#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct Filters {
folder: Option<PathBuf>,
extension: Option<String>,
min_size: Option<u64>,
max_size: Option<u64>,
min_duration_ms: Option<u32>,
max_duration_ms: Option<u32>,
modified_after_unix_ms: Option<i64>,
modified_before_unix_ms: Option<i64>,
}
impl Filters {
fn options(self, root: &Path) -> Result<SearchOptions> {
if let Some(folder) = &self.folder {
ensure!(
folder.canonicalize()?.starts_with(root),
"Capture folder filter must stay within the fixture root"
);
}
Ok(SearchOptions {
folder: self.folder,
extension: self.extension,
min_size: self.min_size,
max_size: self.max_size,
min_duration_ms: self.min_duration_ms,
max_duration_ms: self.max_duration_ms,
modified_after_unix_ms: self.modified_after_unix_ms,
modified_before_unix_ms: self.modified_before_unix_ms,
})
}
}
struct App {
root: PathBuf,
db: PathBuf,
dist: PathBuf,
models: PathBuf,
token: String,
lexical: SearchIndex,
hybrid: HybridSearch,
media: BTreeMap<String, PathBuf>,
}
fn main() -> Result<()> {
let args = Args::parse();
std::env::set_var("SEARCHI_FORCE_CPU", "1");
let root = prepare_fixture(&args.root)?;
if let Some(parent) = args.db.parent() {
fs::create_dir_all(parent)?;
}
let mut lexical = SearchIndex::open(&args.db)?;
validate_database(&args.db, &root)?;
lexical.index_folder(&root)?;
lexical.prewarm()?;
let tools = FfmpegTools::new(args.ffmpeg, args.ffprobe)?;
let mut hybrid = HybridSearch::open(&args.db, &args.models, &args.worker, 1)?;
let status = HybridSearch::embedding_status(&args.db)?;
if status.stale || status.generation.is_none() {
let report = hybrid.index_embeddings_with_media(&tools, &MediaLimits::default())?;
ensure!(
report.media_failures == 0,
"Capture media indexing failed: {:?}",
report.failures
);
println!(
"Indexed real fixture: {} passages, {} vectors, {} images",
report.passages, report.chunks, report.media_files
);
}
hybrid.warmup()?;
let dist = args.dist.canonicalize()?;
ensure!(
dist.join("index.html").is_file(),
"Build the frontend before capture"
);
let media = BTreeMap::from([
("lake".into(), root.join("Mountain Lake.jpg")),
("pier".into(), root.join("Night Pier.jpg")),
]);
let app = App {
root,
db: args.db,
dist,
models: args.models,
token: uuid::Uuid::new_v4().to_string(),
lexical,
hybrid,
media,
};
let server = Server::http(("127.0.0.1", 1431)).map_err(|e| anyhow::anyhow!(e.to_string()))?;
println!("READY http://127.0.0.1:1431/?capture=1 (real CPU engine, local fixture only)");
serve(server, app)
}
fn valid_host(host: &str) -> bool {
matches!(host, "127.0.0.1:1431" | "localhost:1431")
}
fn permitted(headers: &[(String, String)], post: bool, token: &str) -> bool {
let get = |key: &str| {
headers
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(key))
.map(|(_, value)| value.as_str())
};
let Some(host) = get("host") else {
return false;
};
if !valid_host(host) {
return false;
}
if let Some(origin) = get("origin") {
if origin != format!("http://{host}") {
return false;
}
}
!post
|| (get("origin") == Some(format!("http://{host}").as_str())
&& get("x-searchi-capture-token") == Some(token))
}
fn serve(server: Server, mut app: App) -> Result<()> {
for mut request in server.incoming_requests() {
let headers = request
.headers()
.iter()
.map(|h| (h.field.as_str().to_string(), h.value.as_str().to_owned()))
.collect::<Vec<_>>();
let post = request.method() == &Method::Post;
if !permitted(&headers, post, &app.token) {
reply(
request,
403,
"application/json",
br#"{"message":"Capture request denied"}"#.to_vec(),
)?;
continue;
}
let path = request.url().split('?').next().unwrap_or("").to_string();
let outcome: Result<(u16, String, Vec<u8>)> = (|| {
if request.method() == &Method::Get && path == "/capture-info" {
return Ok((
200,
"application/json".into(),
serde_json::to_vec(
&json!({"mode":"readme-capture","token":app.token,"ready":true,"worker_info":app.hybrid.worker_info(),"rust_build_profile":if cfg!(debug_assertions) {"debug"} else {"release"}}),
)?,
));
}
if post {
let command = path
.strip_prefix("/capture-api/")
.context("Unknown capture route")?;
ensure!(
request.body_length().is_none_or(|n| n <= 65536),
"Capture request too large"
);
let mut body = Vec::new();
request.as_reader().take(65537).read_to_end(&mut body)?;
ensure!(body.len() <= 65536, "Capture request too large");
let args: Value = serde_json::from_slice(&body)?;
return Ok((
200,
"application/json".into(),
serde_json::to_vec(&app.command(command, args)?)?,
));
}
ensure!(request.method() == &Method::Get, "Unsupported method");
if let Some(id) = path.strip_prefix("/capture-media/") {
let file = app.media.get(id).context("Unknown fixture media ID")?;
ensure!(
app.lexical.contains_path(&file.to_string_lossy())?,
"Media no longer indexed"
);
return Ok((200, "image/jpeg".into(), bounded_read(file)?));
}
let file = static_path(&app.dist, &path)?;
let mime = mime_guess::from_path(&file)
.first_or_octet_stream()
.to_string();
Ok((200, mime, bounded_read(&file)?))
})();
match outcome {
Ok((status, mime, body)) => reply(request, status, &mime, body)?,
Err(error) => reply(
request,
400,
"application/json",
serde_json::to_vec(&json!({"message":format!("{error:#}")}))?,
)?,
}
}
Ok(())
}
fn reply(request: Request, status: u16, mime: &str, body: Vec<u8>) -> Result<()> {
let response = Response::from_data(body)
.with_status_code(StatusCode(status))
.with_header(
Header::from_bytes("Content-Type", mime)
.map_err(|_| anyhow::anyhow!("Invalid content type"))?,
)
.with_header(Header::from_bytes("Cache-Control", "no-store").unwrap())
.with_header(Header::from_bytes("X-Content-Type-Options", "nosniff").unwrap());
request.respond(response)?;
Ok(())
}
fn bounded_read(path: &Path) -> Result<Vec<u8>> {
ensure!(path.is_file(), "Capture resource must be a regular file");
let mut bytes = Vec::new();
fs::File::open(path)?
.take(20 * 1024 * 1024 + 1)
.read_to_end(&mut bytes)?;
ensure!(
bytes.len() <= 20 * 1024 * 1024,
"Capture resource exceeds20MiB"
);
Ok(bytes)
}
fn static_path(dist: &Path, url: &str) -> Result<PathBuf> {
ensure!(
!url.contains('%') && !url.contains('\\'),
"Encoded or Windows paths are not capture routes"
);
let relative = Path::new(url.trim_start_matches('/'));
ensure!(
relative
.components()
.all(|c| matches!(c, Component::Normal(_))),
"Unsafe static resource path"
);
let target = if relative.as_os_str().is_empty() {
dist.join("index.html")
} else {
dist.join(relative)
};
let target = target.canonicalize()?;
ensure!(
target.starts_with(dist),
"Static resource escapes build directory"
);
Ok(target)
}
impl App {
fn command(&mut self, name: &str, args: Value) -> Result<Value> {
match name {
"desktop_status" => Ok(
json!({"settings":{"roots":[self.root],"cpu_threads":1},"indexing":false,"shortcut_status":"Ctrl K · reproducible CPU capture"}),
),
"stats" => Ok(serde_json::to_value(self.lexical.stats()?)?),
"semantic_status" => {
let status = HybridSearch::embedding_status(&self.db)?;
Ok(
json!({"model":"Google EmbeddingGemma 2","ready":true,"model_directory":self.models,"worker_ready":true,"downloading":false,"embedding":false,"device_policy":"cpu","error":null,"downloaded_bytes":864676480u64,"total_bytes":864676480u64,"generation":status.generation,"chunks":status.chunks,"stale":status.stale,"pending":status.pending,"warming":false,"worker_info":self.hybrid.worker_info(),"media_tools_ready":true,"media_setup_error":null,"media_failures":status.failures}),
)
}
"search_measured" | "hybrid_search_measured" => {
let query = args["query"].as_str().context("Query required")?;
ensure!(query.len() <= 4096, "Query too large");
let filters: Filters = if args["filters"].is_null() {
Filters::default()
} else {
serde_json::from_value(args["filters"].clone())?
};
let options = filters.options(&self.root)?;
let debug = args["debugPerformance"].as_bool().unwrap_or(false);
if name == "search_measured" {
Ok(serde_json::to_value(
self.lexical
.search_with_options_measured(query, 50, &options, debug)?,
)?)
} else {
Ok(serde_json::to_value(
self.hybrid.search_measured(query, 50, &options, debug)?,
)?)
}
}
"prepare_media_preview" => {
let path = args["path"].as_str().context("Media path required")?;
let (id, _) = self
.media
.iter()
.find(|(_, file)| file.to_string_lossy() == path)
.context("Only known indexed fixture photos may be previewed")?;
ensure!(self.lexical.contains_path(path)?, "Photo no longer indexed");
Ok(
json!({"path":format!("/capture-media/{id}"),"kind":"image","mime":"image/jpeg"}),
)
}
_ => bail!("This capture harness supports read-only fixture commands"),
}
}
}
fn validate_database(db: &Path, root: &Path) -> Result<()> {
let connection = rusqlite::Connection::open(db)?;
let mut query = connection.prepare("SELECT path FROM files")?;
for path in query.query_map([], |row| row.get::<_, String>(0))? {
let path = PathBuf::from(path?);
ensure!(
path.parent() == Some(root),
"Capture database contains files outside dedicated fixture root"
);
ensure!(
fixture_names().contains(&path.file_name().unwrap().to_string_lossy().as_ref()),
"Capture database contains unrelated files"
);
}
Ok(())
}
fn fixture_names() -> Vec<&'static str> {
vec![
"Supplier Agreement.pdf",
"Vendor Onboarding.md",
"Q3 Procurement.md",
"Travel Notes.md",
"Garden Notes.md",
"Mountain Lake.jpg",
"Night Pier.jpg",
]
}
fn prepare_fixture(root: &Path) -> Result<PathBuf> {
fs::create_dir_all(root)?;
let root = root.canonicalize()?;
for entry in fs::read_dir(&root)? {
let entry = entry?;
ensure!(
entry.file_type()?.is_file()
&& fixture_names().contains(&entry.file_name().to_string_lossy().as_ref()),
"Use an empty dedicated capture root, not user files"
);
}
let texts=[("Vendor Onboarding.md","# Vendor onboarding\nCollect supplier tax details, banking information and insurance documents. The purchasing team completes compliance review before signing a new partnership."),("Q3 Procurement.md","# Q3 procurement review\nWe will review supplier delivery times, packaging costs and the annual purchasing budget. Present the comparison of existing vendors at Thursday's planning meeting."),("Travel Notes.md","# Travel plans\nTake the train to Berlin for the museum weekend. Reserve a room near the station and pack a raincoat."),("Garden Notes.md","# Garden notes\nTomatoes need sunlight and regular watering. Plant herbs in pots on the balcony and add compost in spring.")];
for (name, text) in texts {
write_known(&root.join(name), text.as_bytes())?;
}
supplier_pdf(&root.join("Supplier Agreement.pdf"))?;
let repo = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.canonicalize()?;
let manifest: Value =
serde_json::from_slice(&fs::read(repo.join("fixtures/media/fixture.json"))?)?;
for (id, name) in [
("image03", "Mountain Lake.jpg"),
("image05", "Night Pier.jpg"),
] {
let asset = manifest["assets"]
.as_array()
.context("Missing licensed assets")?
.iter()
.find(|a| a["id"] == id)
.context("Missing fixed licensed photo")?;
let source = repo
.join("fixtures/media")
.join(asset["path"].as_str().unwrap());
let bytes = bounded_read(&source)?;
ensure!(
asset["sha256"].as_str() == Some(format!("{:x}", Sha256::digest(&bytes)).as_str()),
"Licensed photo checksum changed"
);
write_known(&root.join(name), &bytes)?;
}
Ok(root)
}
fn write_known(path: &Path, bytes: &[u8]) -> Result<()> {
if path.exists() {
ensure!(
fs::read(path)? == bytes,
"Capture fixture file differs; use a new dedicated root"
);
} else {
fs::write(path, bytes)?;
}
Ok(())
}
fn supplier_pdf(path: &Path) -> Result<()> {
use lopdf::{
content::{Content, Operation},
dictionary, Document, Object, Stream,
};
let mut document = Document::with_version("1.5");
let pages_id = document.new_object_id();
let font=document.add_object(dictionary!{"Type"=>"Font","Subtype"=>"Type1","BaseFont"=>"Helvetica","Encoding"=>"WinAnsiEncoding"});
let resources = document.add_object(dictionary! {"Font"=>dictionary!{"F1"=>font}});
let mut pages = Vec::new();
for lines in [
vec![
"Supplier Agreement",
"The purchasing team and supplier enter a partnership.",
"Orders and delivery schedules are confirmed in writing.",
],
vec![
"Termination and cancellation",
"The supplier partnership may be ended with sixty days",
"written notice. Send the cancellation letter to the vendor.",
"Return unused materials and settle outstanding invoices.",
],
]
.iter()
{
let mut operations = vec![
Operation::new("BT", vec![]),
Operation::new("Tf", vec![Object::Name(b"F1".to_vec()), 12.into()]),
Operation::new("Td", vec![50.into(), 740.into()]),
];
for line in lines {
operations.push(Operation::new(
"Tj",
vec![Object::string_literal(format!("{line} "))],
));
operations.push(Operation::new("Td", vec![0.into(), (-20).into()]));
}
operations.push(Operation::new("ET", vec![]));
let stream = document.add_object(Stream::new(
dictionary! {},
Content { operations }.encode()?,
));
let page =
document.add_object(dictionary! {"Type"=>"Page","Parent"=>pages_id,"Contents"=>stream});
pages.push(Object::Reference(page));
}
document.objects.insert(pages_id,Object::Dictionary(dictionary!{"Type"=>"Pages","Kids"=>pages,"Count"=>2,"Resources"=>resources,"MediaBox"=>vec![0.into(),0.into(),595.into(),842.into()]}));
let catalog = document.add_object(dictionary! {"Type"=>"Catalog","Pages"=>pages_id});
document.trailer.set("Root", catalog);
document.compress();
let mut bytes = Vec::new();
document.save_to(&mut bytes)?;
write_known(path, &bytes)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixture_reuses_unchanged_files_and_real_pdf_page_two() -> Result<()> {
let temporary = tempfile::tempdir()?;
let root = prepare_fixture(&temporary.path().join("corpus"))?;
let db = temporary.path().join("index.db");
let mut index = SearchIndex::open(&db)?;
validate_database(&db, &root)?;
let first = index.index_folder(&root)?;
assert_eq!(first.discovered, 7);
let hits = index.search("sixty days", 10)?;
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].filename, "Supplier Agreement.pdf");
assert_eq!(hits[0].source.as_ref().unwrap().number, 2);
prepare_fixture(&root)?;
let reused = index.index_folder(&root)?;
assert_eq!(reused.unchanged, 7);
assert_eq!(reused.indexed, 0);
let foreign = temporary.path().join("foreign");
fs::create_dir(&foreign)?;
fs::write(foreign.join("secret.txt"), "private")?;
index.index_folder(&foreign)?;
assert!(validate_database(&db, &root).is_err());
Ok(())
}
#[test]
fn origin_host_and_token_all_required_for_post() {
let headers = vec![
("Host".into(), "127.0.0.1:1431".into()),
("Origin".into(), "http://127.0.0.1:1431".into()),
("X-Searchi-Capture-Token".into(), "secret".into()),
];
assert!(permitted(&headers, true, "secret"));
assert!(!permitted(&headers, true, "wrong"));
assert!(!permitted(&headers[..1], true, "secret"));
let mut attacker = headers.clone();
attacker[1].1 = "https://attacker.example".into();
assert!(!permitted(&attacker, true, "secret"));
attacker[0].1 = "attacker.example:1431".into();
assert!(!permitted(&attacker, false, "secret"));
}
#[test]
fn static_routes_reject_traversal_and_unrelated_root_files() -> Result<()> {
let root = tempfile::tempdir()?;
fs::write(root.path().join("index.html"), "real frontend")?;
let canonical = root.path().canonicalize()?;
assert!(static_path(&canonical, "/").is_ok());
for path in ["/../secret", "/%2e%2e/secret", "/C:\\secret"] {
assert!(static_path(&canonical, path).is_err());
}
fs::write(root.path().join("user-secret.txt"), "private")?;
assert!(prepare_fixture(root.path()).is_err());
Ok(())
}
}
+398
View File
@@ -0,0 +1,398 @@
/** Capture the production frontend against a live, real local Rust/Gemma fixture. */
import { mkdir, writeFile } from "node:fs/promises";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { execFileSync } from "node:child_process";
process.env.PLAYWRIGHT_BROWSERS_PATH = "0";
const { chromium } = await import("playwright");
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const target = new URL(
process.env.SEARCHI_CAPTURE_URL ?? "http://127.0.0.1:1431/?capture=1",
);
assert(
target.protocol === "http:" &&
["127.0.0.1", "localhost"].includes(target.hostname),
"Capture must use an owned loopback server",
);
assert(
target.searchParams.get("capture") === "1",
"The explicit capture adapter must be enabled",
);
const out = resolve(
repository,
process.env.SEARCHI_CAPTURE_OUTPUT ?? "docs/screenshots",
);
const infoResponse = await fetch(new URL("/capture-info", target), {
signal: AbortSignal.timeout(15000),
});
assert(infoResponse.ok, "Real fixture backend is not ready");
const info = await infoResponse.json();
assert(
info.mode === "readme-capture" && info.ready === true,
"Requires an initialized real-engine capture backend",
);
await mkdir(out, { recursive: true });
const browser = await chromium.launch({
headless: true,
args: [
"--disable-gpu",
"--disable-accelerated-video-decode",
"--disable-webgl",
],
});
const failures = [];
const evidence = [];
const measured = [];
let latestMeasured;
const source = {
base_commit: execFileSync("git", ["rev-parse", "HEAD"], {
cwd: repository,
encoding: "utf8",
}).trim(),
working_tree_dirty:
execFileSync("git", ["status", "--porcelain"], {
cwd: repository,
encoding: "utf8",
}).trim().length > 0,
};
try {
const context = await browser.newContext({
viewport: { width: 1440, height: 1000 },
deviceScaleFactor: 1,
colorScheme: "light",
reducedMotion: "reduce",
});
await context.route("**/*", (route) => {
const url = new URL(route.request().url());
return url.origin === target.origin ||
["data:", "blob:"].includes(url.protocol)
? route.continue()
: route.abort();
});
const page = await context.newPage();
page.on("pageerror", (error) => failures.push(String(error)));
await page.goto(target.href, { waitUntil: "networkidle", timeout: 30000 });
await page.waitForFunction(
() =>
document
.querySelector("#semantic-status")
?.textContent?.includes("Semantic search ready"),
{},
{ timeout: 120000 },
);
await page.evaluate(async () => {
await document.fonts.ready;
});
assert(
await page
.locator(".brand-symbol")
.evaluate((image) => image.complete && image.naturalWidth > 0),
"Brand asset failed to load",
);
async function query(text, debug = false) {
const awaited = page.waitForResponse(
(response) => {
if (
new URL(response.url()).pathname !==
"/capture-api/hybrid_search_measured"
)
return false;
const args = response.request().postDataJSON();
return args?.query === text && Boolean(args.debugPerformance) === debug;
},
{ timeout: 60000 },
);
await page.locator("#query").fill(text);
const response = await awaited;
assert(response.ok(), "Real measured engine query failed");
const body = await response.json();
assert(
Array.isArray(body.results) && body.metrics,
"Measured backend response missing actual results/metrics",
);
latestMeasured = {
query: text,
mode: debug ? "debug" : "basic",
result_count: body.results.length,
metrics: body.metrics,
first_filename: body.results[0]?.filename,
first_source: body.results[0]?.source,
};
measured.push(latestMeasured);
await page.waitForFunction(
(expected) => {
const summary =
document.querySelector("#result-summary")?.textContent ?? "";
return (
document.querySelector("#query")?.value === expected.query &&
summary.includes("KEYWORD + MEANING") &&
document.querySelector("#results .result strong")?.textContent ===
expected.filename &&
document.querySelectorAll("#results .result").length ===
expected.count
);
},
{
query: text,
filename: latestMeasured.first_filename,
count: latestMeasured.result_count,
},
{ timeout: 60000 },
);
assert(
(await page.locator("#results .result").count()) > 0,
`Real query returned no matches: ${text}`,
);
await page.locator("#results .result").first().click();
await page.waitForFunction(
() => !document.querySelector("#preview")?.hidden,
);
if (text === "cancel the supplier partnership") {
assert.equal(
latestMeasured.first_filename,
"Supplier Agreement.pdf",
"Contract query must retrieve the known PDF fixture",
);
assert.equal(
latestMeasured.first_source?.kind,
"page",
"PDF match must retain actual page source",
);
assert.equal(
latestMeasured.first_source?.number,
2,
"Termination clause must be on actual page2",
);
const passage = await page.locator("#preview-text").innerText();
assert.match(
passage,
/sixty|\b60\b/i,
"Actual PDF preview is missing notice period",
);
assert.match(
passage,
/terminat/i,
"Actual PDF preview is missing termination section",
);
const literalWords = ["supplier", "partnership"];
const rowMarks = await page
.locator("#results .result")
.first()
.locator('.snippet mark[data-match="literal"]')
.allTextContents();
const passageMarks = await page
.locator('#preview-text mark[data-match="literal"]')
.allTextContents();
for (const word of literalWords) {
assert(
rowMarks.some((mark) => mark.toLowerCase() === word),
`Result snippet must highlight actual query word ${word}`,
);
assert(
passageMarks.some((mark) => mark.toLowerCase() === word),
`PDF passage must highlight actual query word ${word}`,
);
}
const why = page.locator("#match-reason");
const disclosure = why.locator("xpath=ancestor::details[1]");
assert.equal(
await disclosure.count(),
1,
"Match reason must live in a compact disclosure",
);
assert.equal(
await disclosure.evaluate((element) => element.open),
false,
"Why details should stay collapsed by default",
);
await disclosure.locator(":scope > summary").click();
await why.waitFor({ state: "visible" });
assert.match(
await page.locator("#match-reason").innerText(),
/page\s*2/i,
"Match explanation must use actual PDF page2",
);
await disclosure.locator(":scope > summary").click();
assert.equal(
await disclosure.evaluate((element) => element.open),
false,
"Normal screenshot must preserve collapsed why details",
);
latestMeasured.ui_checks = {
literal_query_marks: true,
actual_page_explanation: true,
why_disclosure_collapsed_by_default: true,
};
}
if (text === "mountains reflected in calm water") {
assert.equal(
latestMeasured.first_filename,
"Mountain Lake.jpg",
"Visual query must retrieve the known licensed lake",
);
assert.equal(
await page.locator("#preview-text mark").count(),
0,
"Meaning-only photo result must not invent word-level alignment",
);
latestMeasured.ui_checks = { meaning_only_image_has_no_text_marks: true };
}
await page.evaluate(
() =>
new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
),
);
}
async function screenshot(name, queryText) {
assert.deepEqual(failures, [], "Frontend page errors occurred");
const path = resolve(out, `${name}.png`);
await page.screenshot({ path, animations: "disabled", fullPage: false });
const selected = await page.locator("#preview-title").innerText();
assert.equal(
selected,
latestMeasured.first_filename,
"Selected DOM result is not the actual first hybrid result",
);
evidence.push({
file: `${name}.png`,
sha256: createHash("sha256")
.update(await readFile(path))
.digest("hex"),
query: queryText,
mode: latestMeasured.mode,
result_count: latestMeasured.result_count,
selected_filename: selected,
first_source_kind: latestMeasured.first_source?.kind,
first_source_number: latestMeasured.first_source?.number,
theme: await page.evaluate(() =>
matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
),
summary: await page.locator("#result-summary").innerText(),
state_assertions: {
semantic_ready: true,
hybrid_final: true,
image_visible: name === "media-search",
actual_worker_inference:
latestMeasured.mode === "debug" &&
latestMeasured.metrics.trace.some(
(span) => span.counters.worker_inference_us > 0,
),
...latestMeasured.ui_checks,
},
});
}
const documentQuery = "cancel the supplier partnership";
await query(documentQuery);
await screenshot("search-light", documentQuery);
await page.emulateMedia({ colorScheme: "dark" });
await screenshot("search-dark", documentQuery);
await page.emulateMedia({ colorScheme: "light" });
const imageQuery = "mountains reflected in calm water";
await query(imageQuery);
await page.waitForFunction(
() => {
const image = document.querySelector("#media-preview img");
return (
image instanceof HTMLImageElement &&
image.complete &&
image.naturalWidth > 0
);
},
{},
{ timeout: 60000 },
);
await screenshot("media-search", imageQuery);
await page.locator("#settings summary").click();
await page.locator("#performance-toggle").check();
await page.locator("#settings summary").click();
await query(documentQuery, true);
await page.locator("#performance-panel").waitFor({ state: "visible" });
await page.locator("#performance-panel summary").click();
await page.waitForFunction(
() =>
document
.querySelector("#performance-detail")
?.textContent?.includes("hybrid") ||
document
.querySelector("#performance-detail")
?.textContent?.includes("Hybrid"),
);
assert(
latestMeasured.metrics.trace?.some(
(span) => span.counters.worker_inference_us > 0,
),
"Detailed trace did not record real worker inference",
);
await page
.locator("#performance-detail .performance-span")
.filter({ hasText: "worker inference" })
.first()
.scrollIntoViewIfNeeded();
await screenshot("performance-details", documentQuery);
const semanticResponse = await context.request.post(
new URL("/capture-api/semantic_status", target).href,
{
headers: { "X-Searchi-Capture-Token": info.token, Origin: target.origin },
data: {},
},
);
assert(semanticResponse.ok(), "Actual semantic status unavailable");
const semantic = await semanticResponse.json();
assert.equal(
semantic.worker_info?.model,
"google/embeddinggemma-2",
"Capture requires the actual Google model",
);
assert.equal(
semantic.worker_info?.threads,
1,
"Capture inference requires one CPU thread",
);
assert.equal(
semantic.worker_info?.device,
"cpu",
"Capture worker is not CPU",
);
await writeFile(
resolve(out, "capture-manifest.json"),
JSON.stringify(
{
kind: "browser-rendered-production-ui-real-cpu-engine",
source,
viewport: { width: 1440, height: 1000 },
runner: { playwright: "1.64.0", chromium: browser.version() },
backend: {
mode: info.mode,
model: semantic.worker_info.model,
device: semantic.worker_info.device,
runtime: semantic.worker_info.runtime,
threads: semantic.worker_info.threads,
dimensions: semantic.worker_info.dimensions,
ready: info.ready,
rust_build_profile: info.rust_build_profile,
},
cpu_flags: [
"--disable-gpu",
"--disable-accelerated-video-decode",
"--disable-webgl",
],
native_window_chrome: false,
native_visual_qa_complete: false,
measured_queries: measured,
screenshots: evidence,
},
null,
2,
) + "\n",
);
console.log(
`Captured ${evidence.length} real-engine UI screenshots in ${out}`,
);
await context.close();
} finally {
await browser.close();
}
+42
View File
@@ -0,0 +1,42 @@
{
"name": "searchi-desktop-capture",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "searchi-desktop-capture",
"version": "0.1.0",
"dependencies": {
"playwright": "1.64.0"
}
},
"node_modules/playwright": {
"version": "1.64.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.0.tgz",
"integrity": "sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.64.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright-core": {
"version": "1.64.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0.tgz",
"integrity": "sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "searchi-desktop-capture",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"capture": "node capture.mjs",
"install-browser": "playwright install chromium --only-shell"
},
"dependencies": {
"playwright": "1.64.0"
}
}