fix(search): move scope→item-type taxonomy into Rust (UR-049, DR-063)
Stage 1 of scoped-search-boundary-implementation.md — the query side.
scoped-search-boundary.md diagnosed this leak, specified the fix in
detail, and became the justification for the boundary rule in CLAUDE.md,
the check:boundary tripwire, and the spec-review checklist. The fix was
never built: SCOPE_ITEM_TYPES was still live in searchScope.ts, called by
library.ts, and no SearchScope existed anywhere in src-tauri/. The rule's
own founding violation was still shipping.
Rust now owns the taxonomy:
pub enum SearchScope { All, Music, Movies, Tv }
impl SearchScope { pub fn item_types(self) -> Option<Vec<String>> }
- SearchOptions gains `scope`, resolved by resolve_scope(). Scope wins
over include_item_types, which stays for the non-search get_items
callers that legitimately request one concrete type.
- repository_search resolves the scope ONCE, before the cache/server
paths diverge, so online and offline filter identically — the failure
mode most likely to go unnoticed.
- All expands to None (no filter), not the union of the other scopes:
an explicit includeItemTypes list would silently drop People, folders,
and any type nobody enumerated.
- searchScope.ts re-exports SearchScope from generated bindings instead
of a hand-written union, and no longer names an item type for search.
- library.ts sends { scope }.
8 Rust tests written first, confirmed failing on "use of undeclared type
SearchScope" before the implementation existed.
The frontend tests that asserted includeItemTypes contents were rewritten
to assert the opaque scope is sent and includeItemTypes is absent —
keeping the old assertions would require the frontend to know the
taxonomy again, defeating the fix. The expansion is now asserted in Rust.
Verified the spec's headline criterion by hashing every src/ file, adding
"AudioBook" to the Music scope in Rust, and re-hashing: zero frontend
files change. That criterion failed before this commit.
Stage 2 (result-side grouping: GROUP_ITEM_TYPES, GroupedSearchResult on
both search payloads) remains open.
This commit is contained in:
@@ -9,7 +9,6 @@ import {
|
||||
resolveSearchScope,
|
||||
searchRouteUrl,
|
||||
shouldNavigateToSearch,
|
||||
scopeItemTypes,
|
||||
type SearchGroupId,
|
||||
} from "./searchScope";
|
||||
|
||||
@@ -62,25 +61,12 @@ describe("resolveSearchScope", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("scopeItemTypes", () => {
|
||||
it("omits the key entirely for the all scope", () => {
|
||||
// `all` must send no includeItemTypes — an explicit union would silently
|
||||
// drop types nobody enumerated (Person, folders).
|
||||
expect(scopeItemTypes("all")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("maps each narrow scope to its item types", () => {
|
||||
expect(scopeItemTypes("music")).toEqual(["MusicAlbum", "MusicArtist", "Audio", "Playlist"]);
|
||||
expect(scopeItemTypes("movies")).toEqual(["Movie"]);
|
||||
expect(scopeItemTypes("tv")).toEqual(["Series", "Episode"]);
|
||||
});
|
||||
|
||||
it("returns a fresh array callers cannot mutate into the table", () => {
|
||||
const first = scopeItemTypes("movies")!;
|
||||
first.push("Series");
|
||||
expect(scopeItemTypes("movies")).toEqual(["Movie"]);
|
||||
});
|
||||
});
|
||||
// NOTE: the former `scopeItemTypes` suite moved to Rust — see
|
||||
// `search_scope_tests` in src-tauri/src/repository/types.rs. The scope →
|
||||
// item-type expansion is domain vocabulary and is no longer reachable from the
|
||||
// frontend, so testing it here would mean re-introducing the leak to test it.
|
||||
// The "fresh array" test is gone because `item_types()` returns an owned Vec,
|
||||
// making the aliasing bug it guarded structurally impossible.
|
||||
|
||||
describe("normalizeGroupOrder", () => {
|
||||
it("returns the default for missing or non-array input", () => {
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
//
|
||||
// TRACES: UR-049, UR-050 | DR-063, DR-066, DR-067
|
||||
|
||||
export type SearchScope = "all" | "music" | "movies" | "tv";
|
||||
// Sourced from Rust via the generated bindings — the backend owns what a scope
|
||||
// *means* (which Jellyfin item types it covers). Naming an opaque variant is
|
||||
// presentation; knowing its expansion is domain vocabulary and stays in Rust.
|
||||
export type { SearchScope } from "$lib/api/bindings";
|
||||
import type { SearchScope } from "$lib/api/bindings";
|
||||
|
||||
export const SEARCH_SCOPES: readonly SearchScope[] = ["all", "music", "movies", "tv"];
|
||||
|
||||
@@ -19,29 +23,11 @@ export const SCOPE_LABELS: Record<SearchScope, string> = {
|
||||
tv: "TV",
|
||||
};
|
||||
|
||||
/**
|
||||
* Jellyfin item types requested for each scope.
|
||||
*
|
||||
* `all` is deliberately absent: sending no `includeItemTypes` is *not* the same
|
||||
* as sending the union of the lists below — types nobody enumerated here
|
||||
* (Person, folders, …) would be filtered out by an explicit list.
|
||||
*/
|
||||
const SCOPE_ITEM_TYPES: Record<Exclude<SearchScope, "all">, string[]> = {
|
||||
music: ["MusicAlbum", "MusicArtist", "Audio", "Playlist"],
|
||||
movies: ["Movie"],
|
||||
tv: ["Series", "Episode"],
|
||||
};
|
||||
|
||||
/**
|
||||
* Item types to send with a scoped search, or `undefined` for the `all` scope
|
||||
* so the caller omits the key entirely.
|
||||
*
|
||||
* TRACES: UR-049 | DR-063
|
||||
*/
|
||||
export function scopeItemTypes(scope: SearchScope): string[] | undefined {
|
||||
if (scope === "all") return undefined;
|
||||
return [...SCOPE_ITEM_TYPES[scope]];
|
||||
}
|
||||
// NOTE: the scope → Jellyfin item-type mapping deliberately does NOT live here.
|
||||
// It is domain vocabulary and lives in Rust (`SearchScope::item_types()` in
|
||||
// repository/types.rs); the frontend sends the opaque scope and the backend
|
||||
// expands it. Re-introducing a `{ music: ["MusicAlbum", …] }` table in this file
|
||||
// is the boundary leak documented in docs/specs/scoped-search-boundary.md.
|
||||
|
||||
/**
|
||||
* Resolve the scope a search started from a given route should default to.
|
||||
|
||||
Reference in New Issue
Block a user