Distinguish main matches from related suggestions and ship 0.1.9
This commit is contained in:
1 parent
a8ba594d0e
commit
b0bdfc7816
40 files changed
+2463
-185
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "searchi-desktop",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "searchi-desktop",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.9",
|
||||
"dependencies": {
|
||||
"@fluentui/svg-icons": "1.1.344",
|
||||
"@fontsource-variable/inter": "5.3.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "searchi-desktop",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Generated
+1
-1
@@ -3799,7 +3799,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "searchi-desktop"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"image",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "searchi-desktop"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
edition = "2021"
|
||||
description = "Local Searchi desktop shell"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "searchi",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.9",
|
||||
"identifier": "dev.searchi.desktop",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
+80
-18
@@ -53,6 +53,8 @@ import {
|
||||
renderHighlighted,
|
||||
displayExcerpt,
|
||||
matchExplanation,
|
||||
baseMatchKind,
|
||||
partitionResults,
|
||||
} from "./match-display";
|
||||
import "@fontsource-variable/inter/wght.css";
|
||||
import "./style.css";
|
||||
@@ -166,7 +168,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><p id="speech-status" class="hint" role="status" hidden></p><progress id="speech-progress" aria-label="Local speech model setup progress" hidden></progress><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="application-results" aria-label="Applications" hidden></ul><ul id="results" aria-label="Matching files"></ul><div id="empty">${icon("search")}<h2>Find what matters. Locally.</h2><p>Your device is indexed in the background. Search an application, filename, phrase, or what you remember.</p></div></section><aside id="application-preview" aria-label="Application preview" hidden><div class="application-detail"><img src="/searchi-symbol.png" alt="" width="48" height="48"><h2 id="application-name"></h2><p class="hint">Installed application</p><button id="launch-application" type="button">${icon("open")} Launch application</button></div></aside><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><details id="ai-panel" class="ai-panel" hidden><summary>Summarize or ask</summary><p id="ai-disclosure" class="hint"></p><p class="eyebrow">PASSAGE THAT WILL BE SENT</p><pre id="ai-source-text"></pre><div class="ai-actions"><button id="ai-summarize" type="button" disabled>Summarize this passage</button><label class="sr-only" for="ai-question">Question about this passage</label><input id="ai-question" placeholder="Ask about this passage…" maxlength="2048"><button id="ai-ask" type="button" disabled>Ask</button></div><p id="ai-status" class="hint" role="status"></p><pre id="ai-answer"></pre><p id="ai-answer-label" class="hint"></p><button id="ai-citation" type="button" hidden>Selected source</button></details><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="application-results" aria-label="Applications" hidden></ul><ul id="results" aria-label="Matching files"></ul><details id="related-suggestions" hidden><summary>Related suggestions</summary><ul id="related-results" aria-label="Related file suggestions"></ul></details><div id="empty">${icon("search")}<h2>Find what matters. Locally.</h2><p>Your device is indexed in the background. Search an application, filename, phrase, or what you remember.</p></div></section><aside id="application-preview" aria-label="Application preview" hidden><div class="application-detail"><img src="/searchi-symbol.png" alt="" width="48" height="48"><h2 id="application-name"></h2><p class="hint">Installed application</p><button id="launch-application" type="button">${icon("open")} Launch application</button></div></aside><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><details id="ai-panel" class="ai-panel" hidden><summary>Summarize or ask</summary><p id="ai-disclosure" class="hint"></p><p class="eyebrow">PASSAGE THAT WILL BE SENT</p><pre id="ai-source-text"></pre><div class="ai-actions"><button id="ai-summarize" type="button" disabled>Summarize this passage</button><label class="sr-only" for="ai-question">Question about this passage</label><input id="ai-question" placeholder="Ask about this passage…" maxlength="2048"><button id="ai-ask" type="button" disabled>Ask</button></div><p id="ai-status" class="hint" role="status"></p><pre id="ai-answer"></pre><p id="ai-answer-label" class="hint"></p><button id="ai-citation" type="button" hidden>Selected source</button></details><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 id="privacy-summary">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>${isNativeDesktop() ? "Hide" : "Clear"}</span></span></footer>`;
|
||||
|
||||
const element = <T extends HTMLElement>(id: string) =>
|
||||
@@ -248,6 +250,32 @@ try {
|
||||
/* Diagnostics remain opt-in when storage is unavailable. */
|
||||
}
|
||||
let results: SearchResult[] = [];
|
||||
let allResults: SearchResult[] = [];
|
||||
const relatedDisclosure = element<HTMLDetailsElement>("related-suggestions");
|
||||
const relatedList = element<HTMLUListElement>("related-results");
|
||||
relatedDisclosure
|
||||
.querySelector("summary")!
|
||||
.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "ArrowDown") return;
|
||||
event.preventDefault();
|
||||
relatedDisclosure.open = true;
|
||||
results = partitionResults(allResults, true).visible;
|
||||
render();
|
||||
const index = partitionResults(allResults).primary.length;
|
||||
if (results[index]) select(index, true);
|
||||
});
|
||||
relatedDisclosure.addEventListener("toggle", () => {
|
||||
const previous = results[selected]?.path;
|
||||
results = partitionResults(allResults, relatedDisclosure.open).visible;
|
||||
selected = -1;
|
||||
render();
|
||||
const retained = previous
|
||||
? results.findIndex((r) => r.path === previous)
|
||||
: -1;
|
||||
if (retained >= 0) select(retained, false, false);
|
||||
else if (results.length && selectedApplication < 0) select(0, false, false);
|
||||
else if (selectedApplication < 0) element("preview").hidden = true;
|
||||
});
|
||||
interface ApplicationResult {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -346,6 +374,7 @@ async function refreshApplications() {
|
||||
element("application-preview").hidden = true;
|
||||
}
|
||||
element("empty").hidden = Boolean(found.length || results.length);
|
||||
updateRelatedEmptyState();
|
||||
const summary = element("result-summary");
|
||||
const fileSummary = (summary.textContent ?? "").replace(
|
||||
/^\d+ APPLICATIONS? � /,
|
||||
@@ -513,7 +542,7 @@ function refreshEmptySearchState() {
|
||||
if (
|
||||
!latestSearch ||
|
||||
latestSearch.id !== requestId ||
|
||||
results.length ||
|
||||
allResults.length ||
|
||||
applications.length
|
||||
)
|
||||
return;
|
||||
@@ -1105,12 +1134,16 @@ function select(index: number, focus = false, userChoice = true) {
|
||||
selected = Math.max(0, Math.min(index, results.length - 1));
|
||||
const result = results[selected];
|
||||
if (userChoice) resultSelection.choose(result.path);
|
||||
list.querySelectorAll<HTMLButtonElement>("button").forEach((button, i) => {
|
||||
button.classList.toggle("selected", i === selected);
|
||||
button.setAttribute("aria-current", String(i === selected));
|
||||
button.tabIndex = i === selected ? 0 : -1;
|
||||
if (i === selected && focus) button.focus();
|
||||
});
|
||||
document
|
||||
.querySelectorAll<HTMLButtonElement>(
|
||||
"#results button, #related-results button",
|
||||
)
|
||||
.forEach((button, i) => {
|
||||
button.classList.toggle("selected", i === selected);
|
||||
button.setAttribute("aria-current", String(i === selected));
|
||||
button.tabIndex = i === selected ? 0 : -1;
|
||||
if (i === selected && focus) button.focus();
|
||||
});
|
||||
element("preview").hidden = false;
|
||||
const selectedIcon = fileIcon(result.filename);
|
||||
element("preview-icon").innerHTML = selectedIcon.html;
|
||||
@@ -1126,14 +1159,14 @@ function select(index: number, focus = false, userChoice = true) {
|
||||
displayedLiteralQuery,
|
||||
);
|
||||
element("preview-kind").textContent =
|
||||
`${matchLabel(result.match_kind)}${result.source ? ` · ${sourceLabel(result.source)}` : ""}`;
|
||||
`${matchLabel(baseMatchKind(result.match_kind))}${result.source ? ` · ${sourceLabel(result.source)}` : ""}`;
|
||||
const media = mediaKind(result.filename);
|
||||
const explanation = matchExplanation(result);
|
||||
const semanticMedia = [
|
||||
"semantic_image",
|
||||
"semantic_video",
|
||||
"semantic_audio",
|
||||
].includes(result.match_kind);
|
||||
].includes(baseMatchKind(result.match_kind));
|
||||
element("passage-label").textContent = semanticMedia
|
||||
? "MATCHED MEDIA"
|
||||
: explanation.semanticPassage
|
||||
@@ -1200,8 +1233,25 @@ function select(index: number, focus = false, userChoice = true) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateRelatedEmptyState() {
|
||||
const partition = partitionResults(allResults);
|
||||
if (
|
||||
!partition.primary.length &&
|
||||
partition.related.length &&
|
||||
!applications.length
|
||||
) {
|
||||
element("empty").querySelector("h2")!.textContent = "No strong matches";
|
||||
element("empty").querySelector("p")!.textContent =
|
||||
"You can explore the related suggestions.";
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
const scrollTop = list.scrollTop;
|
||||
const partition = partitionResults(allResults, relatedDisclosure.open);
|
||||
relatedDisclosure.hidden = partition.related.length === 0;
|
||||
relatedDisclosure.querySelector("summary")!.textContent =
|
||||
`Related suggestions (${partition.related.length})`;
|
||||
const retained = new Set(results.map((result) => result.path));
|
||||
for (const [key, entry] of resultRows) {
|
||||
if (!retained.has(key)) {
|
||||
@@ -1210,6 +1260,7 @@ function render() {
|
||||
}
|
||||
}
|
||||
element("empty").hidden = results.length > 0 || applications.length > 0;
|
||||
updateRelatedEmptyState();
|
||||
if (!results.length) {
|
||||
element("preview").hidden = true;
|
||||
previewPath = null;
|
||||
@@ -1253,21 +1304,25 @@ function render() {
|
||||
"semantic_image",
|
||||
"semantic_video",
|
||||
"semantic_audio",
|
||||
].includes(result.match_kind);
|
||||
].includes(baseMatchKind(result.match_kind));
|
||||
entry.snippet.classList.toggle("code-snippet", displayedCodeScope);
|
||||
renderHighlighted(
|
||||
entry.snippet,
|
||||
displayExcerpt(result.snippet, displayedLiteralQuery),
|
||||
semanticMedia ? literalQuery("") : displayedLiteralQuery,
|
||||
);
|
||||
entry.kind.textContent = `${matchLabel(result.match_kind)}${result.source ? ` · ${sourceLabel(result.source)}` : ""}`;
|
||||
entry.kind.textContent = `${matchLabel(baseMatchKind(result.match_kind))}${result.source ? ` · ${sourceLabel(result.source)}` : ""}`;
|
||||
const asset = fileIcon(result.filename);
|
||||
if (entry.file.dataset.type !== asset.type) {
|
||||
entry.file.innerHTML = asset.html;
|
||||
entry.file.dataset.type = asset.type;
|
||||
}
|
||||
const positioned = list.children.item(i);
|
||||
if (positioned !== entry.row) list.insertBefore(entry.row, positioned);
|
||||
const target = result.match_kind.endsWith("_related") ? relatedList : list;
|
||||
const position = result.match_kind.endsWith("_related")
|
||||
? i - partition.primary.length
|
||||
: i;
|
||||
const positioned = target.children.item(position);
|
||||
if (positioned !== entry.row) target.insertBefore(entry.row, positioned);
|
||||
});
|
||||
list.scrollTop = scrollTop;
|
||||
}
|
||||
@@ -1326,12 +1381,15 @@ function applyResults(
|
||||
)
|
||||
return;
|
||||
const renderStarted = performance.now();
|
||||
const resultsHadFocus = list.contains(document.activeElement);
|
||||
const resultsHadFocus =
|
||||
list.contains(document.activeElement) ||
|
||||
relatedList.contains(document.activeElement);
|
||||
shownLane = lane;
|
||||
displayedRequestId = request.id;
|
||||
displayedLiteralQuery = literalQuery(request.query);
|
||||
displayedCodeScope = request.filters.code_only === true;
|
||||
results = found;
|
||||
allResults = found;
|
||||
results = partitionResults(found, relatedDisclosure.open).visible;
|
||||
selected = -1;
|
||||
render();
|
||||
const preserved = resultSelection.resolve(
|
||||
@@ -1344,12 +1402,12 @@ function applyResults(
|
||||
lane,
|
||||
renderStarted,
|
||||
performance.now(),
|
||||
found.length,
|
||||
results.length,
|
||||
);
|
||||
const metric = request.measurement.latestNative(lane);
|
||||
const trace = request.measurement.export();
|
||||
element("result-summary").textContent =
|
||||
`${found.length} RESULTS · ${lane === "hybrid" ? "KEYWORD + MEANING" : "KEYWORD"} · SEARCH ${briefMs(metric?.total_ms ?? 0)} MS · SHOWN ${briefMs(trace.latest_displayed_ms ?? 0)} MS`;
|
||||
`${partitionResults(found).primary.length ? `${partitionResults(found).primary.length} RESULTS` : found.length ? "NO STRONG MATCHES" : "0 RESULTS"} · ${lane === "hybrid" ? "KEYWORD + MEANING" : "KEYWORD"} · SEARCH ${briefMs(metric?.total_ms ?? 0)} MS · SHOWN ${briefMs(trace.latest_displayed_ms ?? 0)} MS`;
|
||||
refreshPerformance(request);
|
||||
}
|
||||
|
||||
@@ -1551,6 +1609,7 @@ async function search(inputAt = performance.now()) {
|
||||
syncLauncher();
|
||||
void refreshApplications().catch(showError);
|
||||
invalidateSearch();
|
||||
relatedDisclosure.open = false;
|
||||
querySemanticFailed = false;
|
||||
shownLane = "lexical";
|
||||
const value = query.value.trim();
|
||||
@@ -1559,6 +1618,8 @@ async function search(inputAt = performance.now()) {
|
||||
if (!hasSearchInput(value, filters)) {
|
||||
resultSelection.begin("");
|
||||
element("performance-panel").hidden = true;
|
||||
allResults = [];
|
||||
relatedDisclosure.open = false;
|
||||
results = [];
|
||||
displayedLiteralQuery = literalQuery("");
|
||||
displayedRequestId = null;
|
||||
@@ -1809,6 +1870,7 @@ document.addEventListener("keydown", (event) => {
|
||||
if (
|
||||
event.target === query ||
|
||||
list.contains(event.target as Node) ||
|
||||
relatedList.contains(event.target as Node) ||
|
||||
element("application-results").contains(event.target as Node)
|
||||
) {
|
||||
if (event.key === "Enter" && selectedApplication >= 0) {
|
||||
|
||||
@@ -189,6 +189,15 @@ interface MatchResult {
|
||||
source?: { kind: string; number: number };
|
||||
}
|
||||
export function matchExplanation(result: MatchResult) {
|
||||
const explanation = baseMatchExplanation(result);
|
||||
return result.match_kind.endsWith("_related")
|
||||
? {
|
||||
...explanation,
|
||||
reason: `Related suggestion: similarity alone does not verify this answers your search. ${explanation.reason}`,
|
||||
}
|
||||
: explanation;
|
||||
}
|
||||
function baseMatchExplanation(result: MatchResult) {
|
||||
const source = result.source;
|
||||
const location =
|
||||
source?.kind === "page"
|
||||
@@ -204,7 +213,7 @@ export function matchExplanation(result: MatchResult) {
|
||||
: "";
|
||||
const literalLegend =
|
||||
"Marked words are literal query matches, ignoring case and accents.";
|
||||
switch (result.match_kind) {
|
||||
switch (baseMatchKind(result.match_kind)) {
|
||||
case "semantic_image":
|
||||
return {
|
||||
reason:
|
||||
@@ -293,3 +302,19 @@ export function matchExplanation(result: MatchResult) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function baseMatchKind(kind: string): string {
|
||||
return kind.endsWith("_related") ? kind.slice(0, -8) : kind;
|
||||
}
|
||||
export function partitionResults<T extends { match_kind: string }>(
|
||||
found: T[],
|
||||
expanded = false,
|
||||
) {
|
||||
const primary = found.filter((r) => !r.match_kind.endsWith("_related"));
|
||||
const related = found.filter((r) => r.match_kind.endsWith("_related"));
|
||||
return {
|
||||
primary,
|
||||
related,
|
||||
visible: expanded ? [...primary, ...related] : primary,
|
||||
};
|
||||
}
|
||||
@@ -1205,3 +1205,25 @@ footer {
|
||||
background: Canvas;
|
||||
}
|
||||
}
|
||||
|
||||
#related-suggestions {
|
||||
margin-top: 10px;
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
max-height: 55%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#related-suggestions > summary {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
#related-results {
|
||||
list-style: none;
|
||||
padding: 0 8px 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#related-results li {
|
||||
padding: 3px 0;
|
||||
}
|
||||
@@ -88,3 +88,37 @@ test('code passage reasons retain actual indexed source line', () => {
|
||||
assert.match(matchExplanation({match_kind:'semantic', source:{kind:'line',number:97}}).reason, /at line 97/);
|
||||
assert.match(matchExplanation({match_kind:'hybrid', source:{kind:'line',number:97}}).reason, /showing the passage at line 97/);
|
||||
});
|
||||
|
||||
import {baseMatchKind,partitionResults} from '../src/match-display.ts';
|
||||
import {ResultSelection} from '../src/selection-state.ts';
|
||||
test('related candidates remain retained but excluded from collapsed keyboard selection', () => {
|
||||
const weak={path:'mountain',match_kind:'semantic_image_related'}, strong={path:'supplier',match_kind:'semantic'}, exact={path:'name',match_kind:'exact_filename'};
|
||||
const found=[weak,strong,exact];
|
||||
assert.deepEqual(partitionResults(found),{primary:[strong,exact],related:[weak],visible:[strong,exact]});
|
||||
assert.deepEqual(partitionResults(found,true).visible,[strong,exact,weak]);
|
||||
assert.deepEqual(found,[weak,strong,exact]);
|
||||
});
|
||||
test('only related has no implicit Enter target; empty stays distinct', () => {
|
||||
const weak={path:'weak',match_kind:'semantic_related'};
|
||||
const selection=new ResultSelection(); selection.begin('missing');
|
||||
assert.equal(selection.resolve(partitionResults([weak]).visible.map(r=>r.path)),-1);
|
||||
assert.equal(selection.resolve(partitionResults([weak],true).visible.map(r=>r.path)),0);
|
||||
assert.deepEqual(partitionResults([]),{primary:[],related:[],visible:[]});
|
||||
});
|
||||
test('progressive primary results preserve user intent across related disclosure', () => {
|
||||
const a={path:'a',match_kind:'content'},b={path:'b',match_kind:'semantic_related'};
|
||||
const selection=new ResultSelection(); selection.begin('query');
|
||||
selection.resolve(partitionResults([a,b]).visible.map(r=>r.path)); selection.choose('b');
|
||||
assert.equal(selection.resolve(partitionResults([a,b],true).visible.map(r=>r.path)),1);
|
||||
assert.equal(selection.resolve(partitionResults([a,b]).visible.map(r=>r.path)),0);
|
||||
assert.equal(selection.resolve(partitionResults([a,b],true).visible.map(r=>r.path)),1);
|
||||
});
|
||||
test('related media and speech retain source routes and explain unverified similarity', () => {
|
||||
for(const kind of ['semantic','semantic_image','semantic_audio','semantic_video','semantic_speech']) {
|
||||
assert.equal(baseMatchKind(kind+'_related'),kind);
|
||||
const primary=matchExplanation({match_kind:kind,source:{kind:'timestamp_ms',number:1500}});
|
||||
const related=matchExplanation({match_kind:kind+'_related',source:{kind:'timestamp_ms',number:1500}});
|
||||
assert.equal(related.semanticPassage,primary.semanticPassage); assert.equal(related.legend,primary.legend);
|
||||
assert.ok(related.reason.includes('similarity alone does not verify')); assert.ok(related.reason.endsWith(primary.reason));
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user