{"version":3,"file":"search.cjs","names":[],"sources":["../src/search.ts"],"sourcesContent":["import { IndexItem, SectionRecord } from './types';\n\nconst STOPWORDS = new Set([\n  'a',\n  'an',\n  'the',\n  'and',\n  'or',\n  'of',\n  'to',\n  'in',\n  'on',\n  'for',\n  'with',\n  'how',\n  'do',\n  'i',\n  'my',\n  'is',\n  'it',\n  'can',\n  'use',\n  'using',\n  'when',\n  'what',\n  'that',\n  'this',\n  'be',\n  'as',\n  'at',\n  'by',\n  'from',\n  'are',\n  'get',\n]);\n\nfunction normalize(value: string): string {\n  return value.trim().toLowerCase();\n}\n\nfunction tokenize(value: string): string[] {\n  return normalize(value)\n    .split(/[^a-z0-9.-]+/)\n    .filter(Boolean);\n}\n\nfunction queryTokens(query: string): string[] {\n  const raw = tokenize(query);\n  const filtered = raw.filter((token) => token.length > 2 && !STOPWORDS.has(token));\n  return filtered.length > 0 ? filtered : raw;\n}\n\n/** A scoring function pair built against a fixed corpus's document-frequency statistics */\nexport interface SearchScorer {\n  scoreItem(item: IndexItem, query: string): number;\n  scoreSection(section: SectionRecord, query: string): number;\n}\n\n/**\n * Builds an IDF-weighted scorer over a fixed corpus.\n *\n * Document frequency is computed lazily per token and cached for the lifetime of the\n * returned scorer, so a common token like \"a\" contributes almost nothing to a match while\n * a rare token like \"zod\" dominates it. Callers should rebuild the scorer whenever `index`\n * or `sections` actually change.\n */\nexport function createSearchScorer(index: IndexItem[], sections: SectionRecord[]): SearchScorer {\n  const sectionDocs = sections.map((section) => normalize(`${section.heading} ${section.snippet}`));\n  const itemDocs = index.map((item) => normalize(item.searchText));\n  const corpus = [...sectionDocs, ...itemDocs];\n  const corpusSize = corpus.length;\n  const documentFrequencyCache = new Map<string, number>();\n\n  function documentFrequency(token: string): number {\n    const cached = documentFrequencyCache.get(token);\n    if (cached !== undefined) {\n      return cached;\n    }\n\n    let count = 0;\n    for (const doc of corpus) {\n      if (doc.includes(token)) {\n        count += 1;\n      }\n    }\n\n    documentFrequencyCache.set(token, count);\n    return count;\n  }\n\n  function idf(token: string): number {\n    return Math.log((corpusSize + 1) / (documentFrequency(token) + 1));\n  }\n\n  function coverage(haystack: string, tokens: string[]): number {\n    if (tokens.length === 0) {\n      return 0;\n    }\n\n    let matchedWeight = 0;\n    let totalWeight = 0;\n\n    for (const token of tokens) {\n      const weight = idf(token);\n      totalWeight += weight;\n\n      if (haystack.includes(token)) {\n        matchedWeight += weight;\n      }\n    }\n\n    return totalWeight > 0 ? matchedWeight / totalWeight : 0;\n  }\n\n  return {\n    scoreItem(item: IndexItem, query: string): number {\n      const q = normalize(query);\n\n      if (!q) {\n        return 0;\n      }\n\n      const tokens = queryTokens(query);\n      const name = normalize(item.name);\n      const haystack = normalize(item.searchText);\n      const coverageScore = coverage(haystack, tokens);\n\n      let bonus = 0;\n      if (name === q) {\n        bonus = 60;\n      } else if (name.startsWith(q)) {\n        bonus = 40;\n      } else if (name.includes(q)) {\n        bonus = 25;\n      }\n\n      if (tokens.some((token) => name.includes(token))) {\n        bonus += 15;\n      }\n\n      return coverageScore * 55 + bonus;\n    },\n\n    scoreSection(section: SectionRecord, query: string): number {\n      const q = normalize(query);\n\n      if (!q) {\n        return 0;\n      }\n\n      const tokens = queryTokens(query);\n      const heading = normalize(section.heading);\n      const haystack = normalize(`${section.heading} ${section.snippet}`);\n      const coverageScore = coverage(haystack, tokens);\n\n      let bonus = 0;\n      if (heading === q) {\n        bonus += 40;\n      } else if (heading.includes(q)) {\n        bonus += 25;\n      }\n\n      if (tokens.some((token) => heading.includes(token))) {\n        bonus += 15;\n      }\n\n      return coverageScore * 60 + bonus;\n    },\n  };\n}\n\nfunction editDistance(a: string, b: string): number {\n  const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);\n\n  for (let j = 0; j <= b.length; j += 1) {\n    rows[0][j] = j;\n  }\n\n  for (let i = 1; i <= a.length; i += 1) {\n    for (let j = 1; j <= b.length; j += 1) {\n      const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n      rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);\n    }\n  }\n\n  return rows[a.length][b.length];\n}\n\n/** Returns names close enough to a failed lookup to be worth suggesting */\nexport function suggestNames(index: IndexItem[], query: string, limit = 3): string[] {\n  const q = normalize(query);\n  const threshold = Math.max(2, Math.floor(q.length / 3));\n\n  return index\n    .map((item) => ({ name: item.name, distance: editDistance(q, normalize(item.name)) }))\n    .filter((entry) => entry.distance <= threshold)\n    .sort((a, b) => a.distance - b.distance)\n    .slice(0, limit)\n    .map((entry) => entry.name);\n}\n"],"mappings":"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"}