{"version":3,"file":"DGSI4i5O.mjs","sources":["../../../../../../src/lib/search.ts"],"sourcesContent":["/**\n * Shared search primitives — phase 9a.\n *\n * Five pure functions that the next three phase 9 tasks consume:\n *\n *   - `fuzzyRank`         — score a single (text, query) pair.\n *   - `searchDrives`      — filter + rank `catalog.drives[]`.\n *   - `searchNotes`       — filter + rank an in-memory note array.\n *   - `searchParameters`  — filter + rank `catalog.parameters[]`.\n *                           This is the existing `search()` from\n *                           `catalog.ts`, relocated verbatim (only\n *                           the algorithm shape changed: the new\n *                           function sorts by rank instead of\n *                           returning the source order).\n *   - `highlightMatches`  — wrap matched substrings in `<mark>`\n *                           for inline highlighting, XSS-safe.\n *\n * **Why a single module.** The catalog.ts file is the schema\n * surface — keeping the search primitives in their own module\n * means future per-entity search functions (e.g. `searchFaultCodes`\n * in phase 9c) don't have to re-declare the substring-match logic\n * or the ranker. The orchestrator-locked decision (phase 9 plan,\n * task 9a) is \"do not duplicate the substring-match logic.\"\n *\n * **Determinism.** All ranking and sort order is fully deterministic\n * for a given (text, query) pair — same inputs always produce the\n * same rank order, no randomness, no Date.now(). This is a\n * requirement: the ranker runs in both the SSR load function and\n * the client-side UI, and the two views must agree.\n *\n * @see `catalog.ts` for the source `Drive` / `CatalogParameter` types.\n * @see `db.ts` for the `LocalFieldNote` type.\n */\nimport type { Catalog, CatalogParameter, Drive, FaultCode } from \"./catalog.js\";\nimport type { LocalFieldNote } from \"./db.js\";\nimport type { SharedGuidance } from \"@lceui/shared\";\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a string for case-insensitive comparison. */\nfunction normalize(value: string): string {\n\treturn value.trim().toLowerCase();\n}\n\n/**\n * Escape a string so it can be embedded literally inside a RegExp\n * pattern. Used to build case-insensitive term matchers from raw\n * user input without letting regex metacharacters through.\n */\nfunction escapeRegex(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Escape HTML special chars so a string is safe to embed inside\n * Svelte's `{@html ...}` directive. Order matters: `&` must be\n * replaced FIRST so we don't double-escape the entities we add\n * later in the function (`&lt;` etc. would become `&amp;lt;` if\n * we ran `&` after `<`).\n */\nfunction escapeHtml(value: string): string {\n\treturn value\n\t\t.replace(/&/g, \"&amp;\")\n\t\t.replace(/</g, \"&lt;\")\n\t\t.replace(/>/g, \"&gt;\")\n\t\t.replace(/\"/g, \"&quot;\")\n\t\t.replace(/'/g, \"&#39;\");\n}\n\n/**\n * Compute a quality rank for how well a single piece of text matches\n * a query. Higher rank = better match. Returns 0 when there is no\n * match.\n *\n * **Rank tiers (in evaluation order):**\n *\n *   - Exact match (case-insensitive, trimmed): 1000\n *   - StartsWith (case-insensitive): 500\n *   - Word-boundary match (term follows a non-word char): 200\n *   - Includes (substring anywhere): 100\n *\n * **Multi-word queries.** A whitespace-separated query (e.g.\n * `\"kdl16 speed\"`) requires ALL terms to match. The returned rank\n * is the MIN per-term rank — the worst-matching term determines the\n * overall quality. This is why `\"kone eco\"` against `\"KONE EcoSpace\"`\n * comes out at 200 (word-boundary) rather than 500 (startsWith for\n * `\"kone\"`): the `\"eco\"` term is word-boundary, not prefix.\n *\n * **Empty query or empty text returns 0.** Callers (the search\n * functions below) treat rank 0 as \"no match\" and filter these rows\n * out, returning the full list for empty queries.\n */\nexport function fuzzyRank(text: string, query: string): number {\n\tconst t = normalize(text);\n\tconst q = normalize(query);\n\tif (!t || !q) return 0;\n\n\tconst terms = q.split(/\\s+/).filter(Boolean);\n\tif (terms.length === 0) return 0;\n\n\t// Every term must appear in the text (case-insensitive substring).\n\t// One missing term means the whole row doesn't match — rank 0.\n\tfor (const term of terms) {\n\t\tif (!t.includes(term)) return 0;\n\t}\n\n\t// Single-term query: pick the highest matching tier. The order\n\t// matters — `t === term` wins over `startsWith` wins over\n\t// word-boundary wins over includes.\n\tif (terms.length === 1) {\n\t\tconst term = terms[0] as string;\n\t\tif (t === term) return 1000;\n\t\tif (t.startsWith(term)) return 500;\n\t\tif (new RegExp(`\\\\b${escapeRegex(term)}`).test(t)) return 200;\n\t\treturn 100;\n\t}\n\n\t// Multi-term: each term contributes its own per-term rank; the\n\t// MIN wins. Per-term rank uses the same tiers as the single-term\n\t// path (startsWith outranks word-boundary outranks includes).\n\t// We never see an \"exact\" tier here because t is necessarily\n\t// longer than the multi-term query — the terms split on\n\t// whitespace so each term is shorter than the joined query.\n\tlet minRank = Number.POSITIVE_INFINITY;\n\tfor (const term of terms) {\n\t\tlet termRank: number;\n\t\tif (t.startsWith(term)) {\n\t\t\ttermRank = 500;\n\t\t} else if (new RegExp(`\\\\b${escapeRegex(term)}`).test(t)) {\n\t\t\ttermRank = 200;\n\t\t} else {\n\t\t\ttermRank = 100;\n\t\t}\n\t\tif (termRank < minRank) minRank = termRank;\n\t}\n\treturn minRank === Number.POSITIVE_INFINITY ? 0 : minRank;\n}\n\n// ---------------------------------------------------------------------------\n// Entity-specific search functions\n// ---------------------------------------------------------------------------\n\n/**\n * Search the catalog's drives by `code`, `name`, `sourceDocument`,\n * and `supplementalDocument`. Returns drives sorted by\n * `fuzzyRank` DESC (highest quality match first). When two drives\n * share the same rank, the source order is preserved.\n *\n * Empty / whitespace-only query returns the full list unsorted\n * (preserving the source order from `catalog.drives`). This is the\n * \"no filter\" path the search box uses when cleared.\n *\n * The per-drive rank is `Math.max(...)` across the four fields —\n * a drive that matches strongly in any one field wins over a drive\n * that matches weakly in all four. This is intentional: a tech\n * typing `\"kdl16\"` cares that the KDL16 drive comes up first, even\n * though the ranker's per-field tier only sees 500 (startsWith on\n * `code`), not the 1000 (exact) tier it would get if the entire\n * drive name were considered as one string.\n */\nexport function searchDrives(catalog: Catalog, query: string): Drive[] {\n\tconst q = normalize(query);\n\tif (!q) return [...catalog.drives];\n\n\tconst scored: Array<{ drive: Drive; rank: number }> = [];\n\tfor (const drive of catalog.drives) {\n\t\tconst rank = Math.max(\n\t\t\tfuzzyRank(drive.code, q),\n\t\t\tfuzzyRank(drive.name, q),\n\t\t\tfuzzyRank(drive.sourceDocument, q),\n\t\t\tfuzzyRank(drive.supplementalDocument, q)\n\t\t);\n\t\tif (rank > 0) scored.push({ drive, rank });\n\t}\n\tscored.sort((a, b) => b.rank - a.rank);\n\treturn scored.map((s) => s.drive);\n}\n\n/**\n * Coerce a Date or ISO 8601 string into a `number` epoch-ms.\n *\n * `searchNotes` accepts BOTH `LocalFieldNote[]` (Dexie — always\n * `Date`) and `AdminFieldNote[]` (server wire — ISO string after\n * the SvelteKit JSON round-trip). The sort + tie-breaker below\n * need a comparable numeric timestamp regardless of input shape,\n * so we coerce defensively.\n */\nfunction toEpochMs(value: Date | string): number {\n\tconst d = value instanceof Date ? value : new Date(value);\n\tconst t = d.getTime();\n\t// An invalid Date returns NaN — sort that to the end (largest\n\t// possible timestamp) so a malformed row doesn't poison the\n\t// order. The fuzzy-rank pass below filters these out anyway.\n\treturn Number.isNaN(t) ? Number.POSITIVE_INFINITY : t;\n}\n\n/**\n * Search an in-memory list of field notes by `body`, `anchorId`,\n * and `anchorType`. Returns notes sorted by `fuzzyRank` DESC; ties\n * broken by `clientCreatedAt` DESC (newest first).\n *\n * Empty query returns the full list sorted newest-first (this is\n * what the `/notes` page shows with no active search). The\n * `clientCreatedAt` DESC tie-breaker applies in both branches so the\n * newest-first ordering is consistent regardless of query state.\n *\n * The input is `readonly` — this function never mutates the caller's\n * array. The return value is a new array.\n *\n * **Generic over the row shape.** The signature accepts any record\n * with `{ body, anchorId, anchorType, clientCreatedAt }` so the\n * phase-10 `/notes` overview page can run this on the server's\n * `AdminFieldNote[]` (which carries the same four fields plus\n * ownerUsername + server stamps) without casting. Local callers\n * still pass `LocalFieldNote[]` and the type narrows unchanged.\n *\n * `clientCreatedAt` may be either a `Date` (Dexie / `LocalFieldNote`)\n * or an ISO 8601 string (server wire shape after JSON round-trip).\n * Both are coerced to epoch-ms via `toEpochMs`; invalid dates sort\n * to the end so they don't poison the order.\n */\nexport function searchNotes<\n\tT extends {\n\t\tbody: string;\n\t\tanchorId: string | null;\n\t\tanchorType: string;\n\t\tclientCreatedAt: Date | string;\n\t},\n>(notes: readonly T[], query: string): T[] {\n\t// Newest-first base order, shared by both branches.\n\tconst sorted = [...notes].sort(\n\t\t(a, b) => toEpochMs(b.clientCreatedAt) - toEpochMs(a.clientCreatedAt)\n\t);\n\n\tconst q = normalize(query);\n\tif (!q) return sorted;\n\n\tconst scored: Array<{ note: T; rank: number }> = [];\n\tfor (const note of sorted) {\n\t\tconst rank = Math.max(\n\t\t\tfuzzyRank(note.body, q),\n\t\t\tfuzzyRank(note.anchorId ?? '', q),\n\t\t\tfuzzyRank(note.anchorType, q)\n\t\t);\n\t\tif (rank > 0) scored.push({ note, rank });\n\t}\n\tscored.sort((a, b) => {\n\t\tif (b.rank !== a.rank) return b.rank - a.rank;\n\t\treturn (\n\t\t\ttoEpochMs(b.note.clientCreatedAt) - toEpochMs(a.note.clientCreatedAt)\n\t\t);\n\t});\n\treturn scored.map((s) => s.note);\n}\n\n/**\n * Phase 10-5 — search an in-memory list of shared-guidance rows by\n * `body`, `anchorId`, `authorUsername`, and `anchorType`. Mirrors\n * `searchNotes` but keyed on `updatedAt` (the shared-guidance\n * equivalent of \"newest first\") since shared guidance rows have no\n * `clientCreatedAt`.\n *\n * Same rank algorithm as `searchNotes` — same `fuzzyRank` tiers,\n * same multi-term AND-of-terms semantics, same `Math.max(...)` over\n * the searchable fields. The result is sorted by rank DESC,\n * ties broken by `updatedAt` DESC.\n *\n * Empty query returns the full list sorted by `updatedAt` DESC.\n *\n * `updatedAt` may be a `Date` (typed via `z.coerce.date()` on the\n * Zod schema) or an ISO 8601 string (after the SvelteKit JSON\n * round-trip on the SSR boundary). Both are coerced via `toEpochMs`.\n *\n * **Generic over the row shape** — mirrors `searchNotes` so the\n * page-server's local wire-shape projection (with `updatedAt: string`)\n * works without an extra cast on the call site.\n *\n * Phase 15: `authorUsername` is now a searchable field — the\n * operator's brief asks the search box on /notes to \"substring match\n * on body + anchorId + authorUsername (for shared guidance)\". Rows\n * without an `authorUsername` field (unlikely in practice — every\n * shared_guidance row has one) just contribute 0 to the rank.\n */\nexport function searchSharedGuidance<\n\tT extends {\n\t\tbody: string;\n\t\tanchorId: string | null;\n\t\tanchorType: string;\n\t\tupdatedAt: Date | string;\n\t\tauthorUsername?: string;\n\t},\n>(items: readonly T[], query: string): T[] {\n\t// Newest-first base order on updatedAt.\n\tconst sorted = [...items].sort(\n\t\t(a, b) => toEpochMs(b.updatedAt) - toEpochMs(a.updatedAt)\n\t);\n\n\tconst q = normalize(query);\n\tif (!q) return sorted;\n\n\tconst scored: Array<{ item: T; rank: number }> = [];\n\tfor (const item of sorted) {\n\t\tconst rank = Math.max(\n\t\t\tfuzzyRank(item.body, q),\n\t\t\tfuzzyRank(item.anchorId ?? '', q),\n\t\t\tfuzzyRank(item.anchorType, q),\n\t\t\titem.authorUsername !== undefined\n\t\t\t\t? fuzzyRank(item.authorUsername, q)\n\t\t\t\t: 0\n\t\t);\n\t\tif (rank > 0) scored.push({ item, rank });\n\t}\n\tscored.sort((a, b) => {\n\t\tif (b.rank !== a.rank) return b.rank - a.rank;\n\t\treturn toEpochMs(b.item.updatedAt) - toEpochMs(a.item.updatedAt);\n\t});\n\treturn scored.map((s) => s.item);\n}\n\n/**\n * Search the catalog's parameters by `id`, `function`, `description`,\n * and `sourceCodeIdentifier`. Returns parameters sorted by\n * `fuzzyRank` DESC.\n *\n * Empty query returns the full list unsorted (source order). The\n * filter set is identical to the previous `search()` function that\n * lived in `catalog.ts` — this is a relocation + sort-by-rank\n * enhancement, not an algorithm change.\n */\nexport function searchParameters(\n\tcatalog: Catalog,\n\tquery: string\n): CatalogParameter[] {\n\tconst q = normalize(query);\n\tif (!q) return [...catalog.parameters];\n\n\tconst scored: Array<{ param: CatalogParameter; rank: number }> = [];\n\tfor (const param of catalog.parameters) {\n\t\tconst rank = Math.max(\n\t\t\tfuzzyRank(param.id, q),\n\t\t\tfuzzyRank(param.function, q),\n\t\t\tfuzzyRank(param.description, q),\n\t\t\tfuzzyRank(param.sourceCodeIdentifier, q)\n\t\t);\n\t\tif (rank > 0) scored.push({ param, rank });\n\t}\n\tscored.sort((a, b) => b.rank - a.rank);\n\treturn scored.map((s) => s.param);\n}\n\n/**\n * v0.9.28 t2 — search the catalog's fault codes by `code`,\n * `name`, `description`, `reason`, and `action`. Returns fault\n * codes sorted by `fuzzyRank` DESC.\n *\n * **Why a fault-specific function.** Fault codes use a different\n * identifier shape than parameters: the catalog stores the\n * canonical `XX YY` form (\"00 21\"), but operators type compact\n * 4-digit forms (\"0021\"), the trailing 2-digit form (\"21\"), or\n * the dash-separated form (\"00-21\") interchangeably. We\n * pre-normalize the query by stripping whitespace and dashes so\n * `fuzzyRank` (which requires every term to appear as a literal\n * substring) works against a code field that we've also stripped\n * — \"0021\" matches \"00 21\" (compact: \"0021\") at the exact-match\n * tier (1000), \"21\" matches via the substring tier, and \"00-21\"\n * matches because both sides are normalized to \"0021\" before the\n * ranker runs.\n *\n * This is the same compact-field check `parameters/faults/+page.svelte`\n * uses inline; the SearchPalette calls this function so the\n * Cmd-K search experience stays consistent with the per-page\n * filter.\n *\n * Empty query returns the full list unsorted (source order).\n */\nexport function searchFaults(\n\tcatalog: Catalog,\n\tquery: string\n): FaultCode[] {\n\tconst q = normalize(query);\n\tif (!q) return [...catalog.faultCodes];\n\n\t// v0.9.28 t2 — pre-normalize the query to its digits-only form\n\t// so all of \"21\", \"00 21\", \"0021\", and \"00-21\" reduce to the\n\t// same shape for `fuzzyRank` against the code's compact form.\n\t// When the query contains non-digits (e.g. \"SBC\" or \"wiring\"),\n\t// the compact form is empty and the original query is used\n\t// instead — the per-field fuzzyRank call still works on the\n\t// name / description / reason / action fields.\n\tconst qCompact = q.replace(/[\\s-]+/g, '');\n\tconst qForCode = qCompact.length > 0 ? qCompact : q;\n\n\tconst scored: Array<{ fault: FaultCode; rank: number }> = [];\n\tfor (const fault of catalog.faultCodes) {\n\t\t// Strip whitespace + dashes from the code so \"00 21\" and\n\t\t// \"00-21\" both reduce to \"0021\" — the canonical form is\n\t\t// already whitespace-separated in the catalog JSON.\n\t\tconst codeCompact = fault.code.replace(/[\\s-]+/g, '');\n\t\tconst rank = Math.max(\n\t\t\tfuzzyRank(fault.code, q),\n\t\t\tfuzzyRank(codeCompact, qForCode),\n\t\t\tfuzzyRank(fault.name, q),\n\t\t\tfuzzyRank(fault.description, q),\n\t\t\tfuzzyRank(fault.reason, q),\n\t\t\tfuzzyRank(fault.action, q)\n\t\t);\n\t\tif (rank > 0) scored.push({ fault, rank });\n\t}\n\tscored.sort((a, b) => b.rank - a.rank);\n\treturn scored.map((s) => s.fault);\n}\n\n// ---------------------------------------------------------------------------\n// Highlight\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap matched substrings of `query` inside `text` with `<mark>` for\n * inline highlighting in search results. The function:\n *\n *   1. HTML-escapes the input text FIRST (so `<script>` becomes\n *      `&lt;script&gt;`). This makes the result safe to embed in\n *      Svelte via `{@html ...}` — a raw user input cannot inject\n *      script tags because the angle brackets are escaped before\n *      any `<mark>` is inserted.\n *   2. Then injects `<mark>` around every case-insensitive match of\n *      each whitespace-separated query term.\n *\n * **Empty query** returns the HTML-escaped text unchanged. **Multi-word\n * queries** highlight every term; overlapping matches are deduped by\n * tracking consumed character ranges so we never wrap the same span\n * twice (which would produce nested `<mark>` tags).\n *\n * **Caller contract.** Render the result with Svelte's `{@html ...}`\n * directive. Plain `{...}` would show the literal HTML markup\n * (`<mark>KDL</mark>16`) instead of highlighting it.\n *\n * @example\n *   highlightMatches(\"KDL16\", \"KDL\") // \"<mark>KDL</mark>16\"\n *   highlightMatches(\"<script>\", \"script\") // \"&lt;<mark>script</mark>&gt;\"\n */\nexport function highlightMatches(text: string, query: string): string {\n\tconst escaped = escapeHtml(text);\n\tconst q = normalize(query);\n\tif (!q) return escaped;\n\n\tconst terms = q.split(/\\s+/).filter(Boolean);\n\tif (terms.length === 0) return escaped;\n\n\t// Collect all match ranges in the escaped text, deduping overlaps.\n\t// A \"range\" is a half-open [start, end) span in the escaped string\n\t// that should be wrapped in <mark>.\n\ttype Range = { start: number; end: number };\n\tconst ranges: Range[] = [];\n\n\tfor (const term of terms) {\n\t\tconst re = new RegExp(escapeRegex(term), \"gi\");\n\t\tlet match: RegExpExecArray | null = re.exec(escaped);\n\t\twhile (match !== null) {\n\t\t\tconst start = match.index;\n\t\t\tconst end = start + match[0].length;\n\t\t\tconst overlaps = ranges.some(\n\t\t\t\t(r) => !(end <= r.start || start >= r.end)\n\t\t\t);\n\t\t\tif (!overlaps) ranges.push({ start, end });\n\t\t\t// `lastIndex` is automatically advanced by `re.exec` with\n\t\t\t// the `g` flag. `match[0]` is non-empty (terms are filtered\n\t\t\t// by `filter(Boolean)` above) so we never spin on a\n\t\t\t// zero-length match.\n\t\t\tmatch = re.exec(escaped);\n\t\t}\n\t}\n\n\tif (ranges.length === 0) return escaped;\n\n\t// Sort by start position so we can splice the result string in one\n\t// linear pass.\n\tranges.sort((a, b) => a.start - b.start);\n\n\tlet result = \"\";\n\tlet cursor = 0;\n\tfor (const r of ranges) {\n\t\tresult += escaped.slice(cursor, r.start);\n\t\tresult += `<mark>${escaped.slice(r.start, r.end)}</mark>`;\n\t\tcursor = r.end;\n\t}\n\tresult += escaped.slice(cursor);\n\n\treturn result;\n}\n"],"names":["normalize","value","escapeRegex","escapeHtml","fuzzyRank","text","query","t","q","terms","term","minRank","termRank","searchDrives","catalog","scored","drive","rank","a","b","s","toEpochMs","searchNotes","notes","sorted","note","searchSharedGuidance","items","item","searchParameters","param","searchFaults","qCompact","qForCode","fault","codeCompact","highlightMatches","escaped","ranges","re","match","start","end","r","result","cursor"],"mappings":"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"}