Search Match Engine
Search runs entirely on the client against the Dexie cache, so it works offline (Product Brief §1.1). The engine lives in src/lib/search/.
The modules of the engine and how they connect:
flowchart LR
Search["search(...)<br/>(search.ts)"] --> Matchable["isMatchable(query)<br/>(normalize.ts)"]
Search --> Match["match(...)<br/>(match.ts)"]
Match --> Fold["fold(s)<br/>(normalize.ts)"]
Match --> Snippet["snippet(text, query)<br/>(snippet.ts)"]
Match --> Cache[("Dexie cache<br/>(TaskRow + StepRow)")]
Match --> Result["SearchResult[]<br/>(MatchOutcome)"]
Pieces
Section titled “Pieces”normalize.ts—fold(s)normalizes text (case/diacritics) for accent-insensitive matching;isMatchable(query)gates whether a query is searchable (e.g. non-empty after folding).match.ts—match(...)produces aMatchOutcomeover the match fieldsMatchField = 'title' | 'notes' | 'step', withMatchOptions/SearchResulttypes.search.ts—search(...)is the top-level entry that runs the matcher across the cached Task set (title, notes, and child Step titles) and returns rankedSearchResults.snippet.ts—snippet(text, query, max = 80)builds a highlighted excerpt around the match for result display.
Search spans the Tasks a user can see (owned + member Lists), matching across a Task’s title, its notes, and its Steps’ titles. Because it reads the local cache, results are instant and available offline; there is no server search endpoint on the hot path.
The match/scoring pipeline inside match(...), with field precedence and the cap branch:
flowchart TD
Q{isMatchable<br/>query?} -->|no| Empty["empty MatchOutcome"]
Q -->|yes| P1["Pass 1: fold title then notes<br/>per Task (one entry each)"]
P1 --> P2{Task already<br/>title/notes match?}
P2 -->|no| Step["Pass 2: fold Step title<br/>attribute to parent Task"]
P2 -->|yes| Skip["skip Step (precedence)"]
Step --> Sort["sort: tier then<br/>updatedAt desc then id asc"]
Skip --> Sort
Sort --> Cap{count > cap?}
Cap -->|yes| Trunc["slice to cap, truncated = true"]
Cap -->|no| Full["return all results"]
Related: Offline Outbox & Optimistic Writes (the cache it searches), Create-Form & Search Triggers (UI entry, deferred guide).