# vault search _(no submodules)_ ## Purpose `search.ts` implements free-text search across vault items with category / favorite / tag filters. The result projection deliberately strips `concealedFields`, so search results NEVER contain plaintext passwords / PINs / secure notes — even if a caller forgets to do its own filtering. The implementation is a linear scan over the non-deleted rows; with the schema-enforced caps (1000-row `limit`, 256-char `query`, 32-tag filter) this remains O(N × Q) bounded well under millisecond latency for typical vaults. Future versions may swap in an FTS5 in-memory index without changing the public surface. ## Features - `search(query) → SearchResult[]` — Zod-validated free-text query with optional `category` / `tags` / `favorite` filters and result limit. - Scoring weights — title = 4, plaintext fields = 2, tags = 1; ties break by insertion order via stable sort. - Tag filter is intersection (every tag in `query.tags` must appear on the row). - Empty `query` string returns every matching row (filtered) — useful for filter-only listing. - Concealed-field plaintext is structurally excluded from the result type — the projection uses `Omit`. - Every search records a histogram sample and an `op: "search"` outcome counter. ## Usage: Basic Example ```ts import { VaultSearch } from "@suaveplan/vault/search/search"; const search = new VaultSearch({ rows: () => items.allRows(), vaultKey, clock: Date.now, }); const results = search.search({ query: "github", category: "login" }); for (const item of results) { console.log(item.title, item.fields.username); // item.concealedFields is `never` here — type-checked exclusion. } ``` ## Usage: Advanced Example ```ts import { VaultSearch } from "@suaveplan/vault/search/search"; const search = new VaultSearch({ rows, vaultKey, clock }); // Tag intersection — only items with BOTH "work" AND "dev". const workDev = search.search({ tags: ["work", "dev"], limit: 50 }); // Favorite-only listing with no text filter. const favorites = search.search({ favorite: true }); // Empty query, category filter, custom limit. const allLogins = search.search({ category: "login", limit: 20 }); ``` ## API Reference - `class VaultSearch`. - `constructor(options: SearchOptions)`. - `search(query: SearchQuery): SearchResult[]` — throws `ITEM.VALIDATION_FAILED` on schema violation. - `interface SearchOptions` — `{ rows: () => VaultItemRow[], vaultKey, clock }`. - `type SearchResult = Omit` — search projection without concealed plaintext. ## Implementation Notes - The result projection strips `concealedFields` via destructuring; this guarantees no concealed-field plaintext can flow to a search caller, even if a future code path adds new concealed fields. The `SearchResult` type alias propagates the exclusion to TypeScript consumers. - Scoring is deterministic and field-weighted; titles dominate so a query like `"github"` ranks `{ title: "GitHub" }` ahead of an item that merely mentions GitHub in a username field. Ties retain insertion order via the stable `Array.prototype.sort`. - The `rows` callback is invoked on every search so the search index always reflects the latest state. Building a persistent index across multiple searches is intentionally deferred to a future version. - The injected `vaultKey` is needed by the projection to decrypt nothing — concealed-field values are stripped before decryption — but `rowToItem` still derives the per-item key during projection. This keeps the projection pure and avoids special-casing search inside `rowToItem`. - The OTel counter uses `op: "search"` — search is treated as a read-side item op for telemetry purposes, sharing the items histogram + counter to keep the metric surface narrow.