sensemaking 0.24.0 → 0.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +2 -1
  2. package/dist/cjs/cli/status.js +6 -3
  3. package/dist/cjs/cli/status.js.map +1 -1
  4. package/dist/cjs/cli/watch.js +1 -1
  5. package/dist/cjs/cli/watch.js.map +1 -1
  6. package/dist/cjs/commands/related.js +2 -2
  7. package/dist/cjs/commands/related.js.map +1 -1
  8. package/dist/cjs/commands/search.js +3 -2
  9. package/dist/cjs/commands/search.js.map +1 -1
  10. package/dist/cjs/config/access.js +7 -1
  11. package/dist/cjs/config/access.js.map +1 -1
  12. package/dist/cjs/config/load.js +6 -1
  13. package/dist/cjs/config/load.js.map +1 -1
  14. package/dist/cjs/config/types.d.cts +3 -0
  15. package/dist/cjs/config/types.d.ts +3 -0
  16. package/dist/cjs/config/types.js.map +1 -1
  17. package/dist/cjs/config/validate.js +4 -0
  18. package/dist/cjs/config/validate.js.map +1 -1
  19. package/dist/cjs/embed/query.js +4 -4
  20. package/dist/cjs/embed/query.js.map +1 -1
  21. package/dist/cjs/store/cache.js +2 -1
  22. package/dist/cjs/store/cache.js.map +1 -1
  23. package/dist/cjs/store/open.js +13 -6
  24. package/dist/cjs/store/open.js.map +1 -1
  25. package/dist/cjs/store/turso/reconcile.d.cts +6 -1
  26. package/dist/cjs/store/turso/reconcile.d.ts +6 -1
  27. package/dist/cjs/store/turso/reconcile.js +32 -5
  28. package/dist/cjs/store/turso/reconcile.js.map +1 -1
  29. package/dist/cjs/watch.d.cts +1 -1
  30. package/dist/cjs/watch.d.ts +1 -1
  31. package/dist/cjs/watch.js +5 -4
  32. package/dist/cjs/watch.js.map +1 -1
  33. package/dist/esm/cli/status.js +6 -3
  34. package/dist/esm/cli/status.js.map +1 -1
  35. package/dist/esm/cli/watch.js +1 -1
  36. package/dist/esm/cli/watch.js.map +1 -1
  37. package/dist/esm/commands/related.js +2 -1
  38. package/dist/esm/commands/related.js.map +1 -1
  39. package/dist/esm/commands/search.js +2 -1
  40. package/dist/esm/commands/search.js.map +1 -1
  41. package/dist/esm/config/access.js +7 -0
  42. package/dist/esm/config/access.js.map +1 -1
  43. package/dist/esm/config/load.js +6 -1
  44. package/dist/esm/config/load.js.map +1 -1
  45. package/dist/esm/config/types.d.ts +3 -0
  46. package/dist/esm/config/types.js.map +1 -1
  47. package/dist/esm/config/validate.js +4 -0
  48. package/dist/esm/config/validate.js.map +1 -1
  49. package/dist/esm/embed/query.js +4 -4
  50. package/dist/esm/embed/query.js.map +1 -1
  51. package/dist/esm/store/cache.js +2 -1
  52. package/dist/esm/store/cache.js.map +1 -1
  53. package/dist/esm/store/open.js +14 -4
  54. package/dist/esm/store/open.js.map +1 -1
  55. package/dist/esm/store/turso/reconcile.d.ts +6 -1
  56. package/dist/esm/store/turso/reconcile.js +11 -3
  57. package/dist/esm/store/turso/reconcile.js.map +1 -1
  58. package/dist/esm/watch.d.ts +1 -1
  59. package/dist/esm/watch.js +5 -4
  60. package/dist/esm/watch.js.map +1 -1
  61. package/package.json +1 -1
  62. package/schema.json +6 -1
  63. package/skills/sense-setup/EXAMPLES.md +21 -0
  64. package/skills/sense-setup/SKILL.md +2 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands/search.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { stemmer } from 'stemmer';\nimport type { ResolvedConfig } from '../config/index.ts';\nimport { embedConfig, featureEnabled, resolveSearch } from '../config/index.ts';\nimport { localModelMissing, MODEL_FILENAMES } from '../embed/store.ts';\nimport { SenseError } from '../errors.ts';\nimport type { Row } from '../output/output.ts';\nimport { bareTermSyntaxError, searchError } from '../output/search-error.ts';\nimport type { LexicalHit, Store } from '../store/types.ts';\nimport { foldForSearch, hasUnspacedRun, searchTokens, unspacedRuns } from '../text/segment.ts';\nimport { materializeScope, narrowByWhere, rawScope, scopeHasEmbeddings } from './scope.ts';\nimport { linksCandidates, vectorsCandidates, wordsCandidates } from './signals.ts';\n\n// Default budget behind --snippet-char-limit/--snippet-count-limit: the limit is whatever the\n// caller passes, these only say what happens when they pass nothing (DESIGN.md \"Search snippets\").\nexport const SNIPPET_CHAR_LIMIT_DEFAULT = 80;\nexport const SNIPPET_COUNT_LIMIT_DEFAULT = 1;\n\n// Bare terms from an FTS5 query string: strips operators/quoting, not real terms, so the\n// snippet scan matches what the query matched -- FTS5 operators are uppercase-only, so this is too.\nfunction extractBareTerms(query: string): string[] {\n const cleaned = query\n .replace(/\"/g, ' ')\n .replace(/[()*]/g, ' ')\n .replace(/\\b(AND|OR|NOT|NEAR)\\b(\\/\\d+)?/g, ' ');\n return cleaned\n .split(/\\s+/)\n .map((tok) => tok.replace(/^[A-Za-z_]\\w*:/, '')) // column filter, e.g. title:term\n .flatMap((tok) => searchTokens(tok).map(({ text }) => text))\n .map((tok) => tok.toLowerCase().trim())\n .filter((tok) => tok.length > 0);\n}\n\n// House convention (src/chunk/group.ts): Intl.Segmenter word boundaries, not a bare \\b, which\n// is ASCII-only. Built on first use: construction is a few ms, paid only when a term matches.\nlet wordSegmenter: Intl.Segmenter | undefined;\n\nfunction findOccurrences(text: string, terms: string[]): Array<{ start: number; end: number; term: string }> {\n const occ: Array<{ start: number; end: number; term: string }> = [];\n const textLower = text.toLowerCase();\n\n // Unspaced scripts (UNSPACED_SCRIPTS) mark no word boundaries: the sense-setup skill promises\n // substring semantics there, so a matching term is found wherever it occurs in the run.\n const unspacedTerms = terms.filter(hasUnspacedRun);\n if (unspacedTerms.length > 0) {\n for (const run of unspacedRuns(text)) {\n const runLower = textLower.slice(run.start, run.end);\n for (const term of unspacedTerms) {\n let idx = 0;\n for (;;) {\n const found = runLower.indexOf(term, idx);\n if (found === -1) break;\n occ.push({ start: run.start + found, end: run.start + found + term.length, term });\n idx = found + term.length;\n }\n }\n }\n }\n\n // Spaced scripts: a word is marked when its porter stem equals a query term's stem, the same\n // algorithm both indexes tokenize with (sqlite's TOKENIZE = 'porter unicode61'; duckdb's fts default).\n const spacedTerms = terms.filter((t) => !hasUnspacedRun(t));\n if (spacedTerms.length > 0) {\n const termByStem = new Map(spacedTerms.map((t) => [stemmer(foldForSearch(t)), t]));\n const stemInitials = new Set(Array.from(termByStem.keys(), (term) => term[0]));\n for (const match of searchTokens(text)) {\n const segment = match.text;\n if (match.unspaced) continue;\n const folded = foldForSearch(segment);\n // Porter stemming changes suffixes only, so a different initial cannot match a query stem.\n if (!stemInitials.has(folded[0])) continue;\n const term = termByStem.get(stemmer(folded));\n if (term !== undefined) occ.push({ start: match.start, end: match.end, term });\n }\n }\n\n return occ.sort((a, b) => a.start - b.start || b.end - a.end);\n}\n\n// Segment boundaries of the whole text, word granularity: the only cut points a snippet edge\n// may land on. One rule for every script (DESIGN.md \"Cutting\") -- a regex \\b is ASCII-only and\n// would mis-cut Japanese, Chinese and Thai, which Intl.Segmenter divides by dictionary instead.\nfunction wordBoundaries(text: string): number[] {\n wordSegmenter ??= new Intl.Segmenter('en', { granularity: 'word' });\n const bounds = [0];\n for (const { index, segment } of wordSegmenter.segment(text)) bounds.push(index + segment.length);\n return bounds;\n}\n\n// bounds is sorted: snaps are binary searches, since each runs once per candidate window.\nfunction snapForward(bounds: number[], pos: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (bounds[mid] < pos) lo = mid + 1;\n else hi = mid;\n }\n return bounds[lo];\n}\n\nfunction snapBackward(bounds: number[], pos: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n let result = bounds[0];\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (bounds[mid] <= pos) {\n lo = mid;\n result = bounds[mid];\n } else hi = mid - 1;\n }\n return result;\n}\n\nfunction firstBoundAfter(bounds: number[], pos: number): number | undefined {\n let lo = 0;\n let hi = bounds.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (bounds[mid] <= pos) lo = mid + 1;\n else hi = mid;\n }\n return bounds[lo];\n}\n\n// A raw, unmarked upper bound: [start, end) inside [anchor, anchor+charLimit), edges on\n// boundaries. Markup (guillemets, ellipses) added on top of this can push the rendered result\n// past charLimit, which is what fitEnd() below corrects for; this is only the starting guess.\n// A single segment spanning the whole target window (a word longer than the char limit) is the\n// one case where holding the boundary rule means the budget is not a hard ceiling.\nfunction cutWindow(text: string, bounds: number[], anchor: number, charLimit: number): { start: number; end: number } {\n const rawEnd = Math.min(text.length, anchor + charLimit);\n const start = snapForward(bounds, anchor);\n let end = snapBackward(bounds, rawEnd);\n if (end <= start) end = firstBoundAfter(bounds, start) ?? text.length;\n return { start, end: Math.min(end, text.length) };\n}\n\n// The largest boundary <= upperBound, > floor, whose rendered [*, end) fits charLimit. Markup\n// only ever adds characters as end grows, so rendered length is non-decreasing in end and\n// shrinking from upperBound is guaranteed to find the true fit rather than guess at it.\n// `floor` is the one end this never shrinks past (the anchor's own marked word, or the first\n// word in the unmarked fallback): DESIGN.md's budget is a ceiling except here, where returning\n// nothing is worse than a single word that overruns it.\nfunction fitEnd(render: (end: number) => string, bounds: number[], floor: number, upperBound: number, charLimit: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (bounds[mid] <= upperBound) lo = mid;\n else hi = mid - 1;\n }\n for (; lo >= 0 && bounds[lo] > floor; lo--) {\n if (render(bounds[lo]).length <= charLimit) return bounds[lo];\n }\n return floor;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\ninterface SnippetWindow extends Span {\n distinct: number;\n count: number;\n}\n\n// occ is start-sorted: a window's first candidate occurrence is a binary search, not a walk.\nfunction firstOccAtOrAfter(occ: Array<{ start: number; end: number; term: string }>, pos: number): number {\n let lo = 0;\n let hi = occ.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (occ[mid].start < pos) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\n\nfunction renderWindow(text: string, occ: Array<{ start: number; end: number; term: string }>, win: Span): string {\n let out = '';\n let cursor = win.start;\n for (let i = firstOccAtOrAfter(occ, win.start); i < occ.length && occ[i].start < win.end; i++) {\n const o = occ[i];\n if (o.end > win.end) continue;\n // Occurrences of duplicate or substring-overlapping terms (\"test tests\") can overlap;\n // emitting each would duplicate document text. Keep the first, absorb the rest.\n if (o.start < cursor) continue;\n out += `${text.slice(cursor, o.start)}«${text.slice(o.start, o.end)}»`;\n cursor = o.end;\n }\n out += text.slice(cursor, win.end);\n const prefix = win.start > 0 ? '…' : '';\n const suffix = win.end < text.length ? '…' : '';\n return `${prefix}${out.replace(/\\s+/g, ' ')}${suffix}`;\n}\n\n// Scores every occurrence-anchored candidate window: most distinct terms wins, ties broken by\n// most total hits -- coverage now that occurrences are real words, not the false-fragment counts\n// a substring scan produced. Selection itself is unevaluated (DESIGN.md \"Selection, unsettled\").\n// The window's `end` is fitted to the *rendered* text, markup included, not the raw span: a\n// window dense with marks (every guillemet pair adds two characters) shrinks to fit the same\n// budget a sparse one fills almost exactly.\nfunction scoreWindows(occ: Array<{ start: number; end: number; term: string }>, bounds: number[], text: string, charLimit: number): SnippetWindow[] {\n const candidates: SnippetWindow[] = [];\n for (let i = 0; i < occ.length; i++) {\n const winEnd = occ[i].start + charLimit;\n const seen = new Set<string>();\n let count = 0;\n for (let j = i; j < occ.length && occ[j].start < winEnd; j++) {\n seen.add(occ[j].term);\n count++;\n }\n const start = snapForward(bounds, occ[i].start);\n const floor = Math.max(start, occ[i].end);\n const upperBound = Math.max(floor, cutWindow(text, bounds, occ[i].start, charLimit).end);\n const end = fitEnd((e) => renderWindow(text, occ, { start, end: e }), bounds, floor, upperBound, charLimit);\n candidates.push({ start, end, distinct: seen.size, count });\n }\n return candidates;\n}\n\nfunction overlaps(a: Span, b: Span): boolean {\n return a.start < b.end && b.start < a.end;\n}\n\n// Same scoring function as a single snippet, kept rather than discarded (DESIGN.md \"Several\n// snippets per note\"): rank by score, take the top non-overlapping windows, then reorder to the\n// note's own order, since several passages from one note read as an abridged note.\nfunction selectWindows(candidates: SnippetWindow[], countLimit: number): { chosen: SnippetWindow[]; best: SnippetWindow } {\n const ranked = candidates.slice().sort((a, b) => b.distinct - a.distinct || b.count - a.count);\n const chosen: SnippetWindow[] = [];\n for (const c of ranked) {\n if (chosen.length >= countLimit) break;\n if (chosen.some((s) => overlaps(s, c))) continue;\n chosen.push(c);\n }\n chosen.sort((a, b) => a.start - b.start);\n return { chosen, best: ranked[0] };\n}\n\n// Builds bounded marked passages and returns the best window's offset for section lookup.\nexport function computeSnippets(text: string, terms: string[], charLimit: number, countLimit: number): { snippets: string[]; offset: number } {\n const occ = findOccurrences(text, terms);\n const bounds = wordBoundaries(text);\n if (occ.length === 0) {\n // A term matched only via title/summary, not this body text, finds no occurrence here;\n // fall back to the doc's start, unmarked.\n const render = (end: number): string => `${text.slice(0, end).replace(/\\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`;\n const floor = bounds.find((b) => b > 0) ?? text.length;\n const upperBound = Math.max(floor, cutWindow(text, bounds, 0, charLimit).end);\n const end = fitEnd(render, bounds, floor, upperBound, charLimit);\n return { snippets: [render(end)], offset: 0 };\n }\n const candidates = scoreWindows(occ, bounds, text, charLimit);\n const { chosen, best } = selectWindows(candidates, countLimit);\n return { snippets: chosen.map((win) => renderWindow(text, occ, win)), offset: best.start };\n}\n\n// Converts an offset in the original text to its one-based line number.\nexport function lineNumberAt(text: string, offset: number): number {\n let line = 1;\n for (let i = 0; i < offset; i++) if (text.charCodeAt(i) === 10) line++;\n return line;\n}\n\nasync function lineRangeFor(store: Store, path: string, line: number): Promise<string | null> {\n const stmt = await store.prepare('SELECT start_line, end_line FROM sections WHERE \"path\" = ? AND start_line <= ? AND end_line >= ? ORDER BY start_line DESC LIMIT 1');\n const row = (await stmt.get(path, line, line)) as { start_line: number; end_line: number } | undefined;\n return row ? `L${row.start_line}-${row.end_line}` : null;\n}\n\nexport interface SearchOptions {\n k?: number;\n where?: string; // SQL fragment against frontmatter alias `f`, e.g. \"f.status = 'active'\"\n preset?: string; // named preset; unknown name throws listing declared presets, undefined -> \"default\"\n include?: string[]; // ad hoc scope override (repeatable --include); independent of exclude\n exclude?: string[]; // ad hoc scope override (repeatable --exclude); independent of include\n noExclude?: boolean; // --no-exclude: drop the preset's exclude for this command\n snippetCharLimit?: number; // --snippet-char-limit; defaults to SNIPPET_CHAR_LIMIT_DEFAULT\n snippetCountLimit?: number; // --snippet-count-limit; defaults to SNIPPET_COUNT_LIMIT_DEFAULT\n}\n\n// Hydrates already-ranked rows with snippets and containing-section ranges without reranking.\nexport async function hydrateSearchRows(store: Store, cfg: ResolvedConfig, rows: Row[], matchedPaths: Set<string>, terms: string, opts: Pick<SearchOptions, 'snippetCharLimit' | 'snippetCountLimit'> = {}): Promise<void> {\n if (matchedPaths.size === 0) return;\n const bareTerms = extractBareTerms(terms);\n const charLimit = opts.snippetCharLimit ?? SNIPPET_CHAR_LIMIT_DEFAULT;\n const countLimit = opts.snippetCountLimit ?? SNIPPET_COUNT_LIMIT_DEFAULT;\n for (const row of rows) {\n if (!matchedPaths.has(row.path as string)) continue;\n let text: string;\n try {\n text = readFileSync(join(cfg.baseDir, row.path as string), 'utf8');\n } catch {\n continue; // vanished since the match; leave snippets/lines empty rather than throw\n }\n const { snippets, offset } = computeSnippets(text, bareTerms, charLimit, countLimit);\n row.snippets = snippets;\n if (row.lines == null) row.lines = featureEnabled(cfg, 'sections') ? await lineRangeFor(store, row.path as string, lineNumberAt(text, offset)) : null;\n }\n}\n\n// The declared or defaulted signals compose via RRF; `via` names which ones produced each row.\n// `opts` arrives already resolved (config.ts:resolveSearch).\nexport async function search(store: Store, cfg: ResolvedConfig, terms: string, opts: SearchOptions = {}): Promise<Row[]> {\n const effective = resolveSearch(cfg, opts);\n const { k, signals } = effective;\n\n const allPaths = ((await (await store.prepare('SELECT \"path\" FROM frontmatter')).all()) as Array<{ path: string }>).map((r) => r.path);\n const scopePaths = await rawScope(store, cfg, opts, allPaths);\n const scopeActive = scopePaths.size < allPaths.length;\n // The set every candidate pool must be filtered to before truncation: scope narrowed by\n // --where, the same composition scopedPaths() gives the other commands.\n let allowedPaths: Set<string>;\n try {\n allowedPaths = await narrowByWhere(store, scopePaths, effective.where);\n } catch (err) {\n throw searchError(err as Error, terms, effective.where);\n }\n const fetch = Math.max(k * 3, 30);\n\n // A downloadable HF id proceeds -- getProvider fetches it lazily on consent. Only a\n // local path with missing files errors here, since nothing will ever fetch it for itself.\n const wantsVectors = signals.vectors !== undefined;\n if (wantsVectors) {\n const e = embedConfig(cfg); // validate.ts guarantees this is set whenever \"vectors\" is declared\n if (localModelMissing(e)) {\n throw new SenseError('EMBED_MODEL_MISSING', `preset \"${effective.presetName}\" searches with vectors, but the local model path \"${e.model}\" is missing ${MODEL_FILENAMES}; point embed.model at a directory containing them, or drop \"vectors\" from that preset's signals to search without them`);\n }\n }\n const semanticEnabled = wantsVectors && (await scopeHasEmbeddings(store, cfg, allowedPaths));\n\n // --where applies inside the candidate query (a post-filter would drop matches ranked past\n // the pool) and again on the final select, for link-derived rows.\n const scope = effective.where;\n const whereJoin = scope ? `JOIN frontmatter f ON f.\"path\" = content.path` : '';\n const whereCond = scope ? `AND (${scope})` : '';\n // Filtered before LIMIT, not after, or scoped notes ranking below the global top-`fetch`\n // never reach the filter; joined against a temp table since real scopes exceed SQLITE_MAX_VARIABLE_NUMBER.\n if (scopeActive) await materializeScope(store, '_search_scope', scopePaths);\n const scopeCond = scopeActive ? `AND content.path IN (SELECT \"path\" FROM _search_scope)` : '';\n\n const candidates = new Map<string, { score: number; via: string }>();\n let matchRows: LexicalHit[] = [];\n const hasEmptyPhrase = [...terms.matchAll(/\"([^\"]*)\"/g)].some((match) => !/[\\p{L}\\p{N}_]/u.test(match[1]));\n const hasBareSyntaxOnly = /[()]/u.test(terms.replace(/\"[^\"]*\"/g, ' '));\n // A query that is only whitespace or punctuation has no lexical rows; other declared signals\n // compose normally. In particular, unspaced-script punctuation can rewrite to an empty MATCH.\n if (signals.words !== undefined && terms.trim() !== '' && (/[\\p{L}\\p{N}_]/u.test(terms) || hasBareSyntaxOnly) && !hasEmptyPhrase) {\n const syntaxError = bareTermSyntaxError(terms);\n if (syntaxError) throw syntaxError;\n try {\n matchRows = await wordsCandidates(store, candidates, terms, whereJoin, whereCond, scopeCond, fetch, signals.words);\n } catch (err) {\n throw searchError(err as Error, terms, scope);\n }\n }\n // Every store's lexical `snippets` is NULL; the snippet text is always computed below.\n // Distinguish from via='link' rows, which never appear in matchRows.\n const matchedPaths = new Set(matchRows.map((r) => r.path));\n\n if (signals.links !== undefined) await linksCandidates(store, candidates, matchRows, allPaths, allowedPaths, fetch, signals.links);\n\n let chunkLines = new Map<string, string>();\n let chunkSimilarity = new Map<string, number>();\n if (semanticEnabled) {\n ({ chunkLines, chunkSimilarity } = await vectorsCandidates(store, cfg, candidates, terms, fetch, allowedPaths, signals.vectors as number));\n }\n\n await store.exec('DROP TABLE IF EXISTS _search');\n // DOUBLE, not REAL: sqlite's REAL is an 8-byte double but duckdb's is a 4-byte float, and the\n // rounded score/similarity are printed at full precision in rows.\n await store.exec('CREATE TEMP TABLE _search (\"path\" TEXT PRIMARY KEY, score DOUBLE, via TEXT, lines TEXT, similarity DOUBLE)');\n if (candidates.size > 0) {\n await store.runBatch(\n 'INSERT INTO _search (\"path\", score, via, lines, similarity) VALUES (?, ?, ?, ?, ?)',\n [...candidates].map(([path, c]) => [path, c.score, c.via, chunkLines.get(path) ?? null, chunkSimilarity.get(path) ?? null])\n );\n }\n\n // Reapplies --where even though _search is already scope+where filtered, since the join to\n // frontmatter is already needed for the path column.\n const where = scope ? `WHERE (${scope})` : '';\n // lines: semantic rows carry their chunk's range, lexical rows gain one below,\n // everything else stays null; similarity stays semantic-only.\n const similarityCol = semanticEnabled ? ', _search.similarity' : '';\n // NULL AS snippets holds this column's position (between summary and via) for every row;\n // overwritten below, always to an array, never left null (DESIGN.md).\n const selectStmt = await store.prepare(\n `SELECT f.\"path\" AS path, content.title, content.summary, NULL AS snippets, _search.via, round(_search.score, 4) AS score, _search.lines${similarityCol}\n FROM _search JOIN frontmatter f ON f.\"path\" = _search.\"path\" JOIN content ON content.path = _search.\"path\"\n ${where} ORDER BY _search.score DESC LIMIT ?`\n );\n const rows = (await selectStmt.all(k)) as Row[];\n for (const row of rows) row.snippets = [];\n\n await hydrateSearchRows(store, cfg, rows, matchedPaths, terms, opts);\n\n return rows;\n}\n"],"names":["readFileSync","join","stemmer","embedConfig","featureEnabled","resolveSearch","localModelMissing","MODEL_FILENAMES","SenseError","bareTermSyntaxError","searchError","foldForSearch","hasUnspacedRun","searchTokens","unspacedRuns","materializeScope","narrowByWhere","rawScope","scopeHasEmbeddings","linksCandidates","vectorsCandidates","wordsCandidates","SNIPPET_CHAR_LIMIT_DEFAULT","SNIPPET_COUNT_LIMIT_DEFAULT","extractBareTerms","query","cleaned","replace","split","map","tok","flatMap","text","toLowerCase","trim","filter","length","wordSegmenter","findOccurrences","terms","occ","textLower","unspacedTerms","run","runLower","slice","start","end","term","idx","found","indexOf","push","spacedTerms","t","termByStem","Map","stemInitials","Set","Array","from","keys","match","segment","unspaced","folded","has","get","undefined","sort","a","b","wordBoundaries","Intl","Segmenter","granularity","bounds","index","snapForward","pos","lo","hi","mid","snapBackward","result","firstBoundAfter","cutWindow","anchor","charLimit","rawEnd","Math","min","fitEnd","render","floor","upperBound","firstOccAtOrAfter","renderWindow","win","out","cursor","i","o","prefix","suffix","scoreWindows","candidates","winEnd","seen","count","j","add","max","e","distinct","size","overlaps","selectWindows","countLimit","ranked","chosen","c","some","s","best","computeSnippets","find","snippets","offset","lineNumberAt","line","charCodeAt","lineRangeFor","store","path","stmt","prepare","row","start_line","end_line","hydrateSearchRows","cfg","rows","matchedPaths","opts","bareTerms","snippetCharLimit","snippetCountLimit","baseDir","lines","search","effective","k","signals","allPaths","all","r","scopePaths","scopeActive","allowedPaths","where","err","fetch","wantsVectors","vectors","presetName","model","semanticEnabled","scope","whereJoin","whereCond","scopeCond","matchRows","hasEmptyPhrase","matchAll","test","hasBareSyntaxOnly","words","syntaxError","links","chunkLines","chunkSimilarity","exec","runBatch","score","via","similarityCol","selectStmt"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands/search.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { stemmer } from 'stemmer';\nimport type { ResolvedConfig } from '../config/index.ts';\nimport { embedConfig, featureEnabled, resolveSearch } from '../config/index.ts';\nimport { localModelMissing, MODEL_FILENAMES } from '../embed/store.ts';\nimport { SenseError } from '../errors.ts';\nimport type { Row } from '../output/output.ts';\nimport { bareTermSyntaxError, searchError } from '../output/search-error.ts';\nimport type { LexicalHit, Store } from '../store/types.ts';\nimport { foldForSearch, hasUnspacedRun, searchTokens, unspacedRuns } from '../text/segment.ts';\nimport { materializeScope, narrowByWhere, rawScope, scopeHasEmbeddings } from './scope.ts';\nimport { linksCandidates, vectorsCandidates, wordsCandidates } from './signals.ts';\n\n// Default budget behind --snippet-char-limit/--snippet-count-limit: the limit is whatever the\n// caller passes, these only say what happens when they pass nothing (DESIGN.md \"Search snippets\").\nexport const SNIPPET_CHAR_LIMIT_DEFAULT = 80;\nexport const SNIPPET_COUNT_LIMIT_DEFAULT = 1;\n\n// Bare terms from an FTS5 query string: strips operators/quoting, not real terms, so the\n// snippet scan matches what the query matched -- FTS5 operators are uppercase-only, so this is too.\nfunction extractBareTerms(query: string): string[] {\n const cleaned = query\n .replace(/\"/g, ' ')\n .replace(/[()*]/g, ' ')\n .replace(/\\b(AND|OR|NOT|NEAR)\\b(\\/\\d+)?/g, ' ');\n return cleaned\n .split(/\\s+/)\n .map((tok) => tok.replace(/^[A-Za-z_]\\w*:/, '')) // column filter, e.g. title:term\n .flatMap((tok) => searchTokens(tok).map(({ text }) => text))\n .map((tok) => tok.toLowerCase().trim())\n .filter((tok) => tok.length > 0);\n}\n\n// House convention (src/chunk/group.ts): Intl.Segmenter word boundaries, not a bare \\b, which\n// is ASCII-only. Built on first use: construction is a few ms, paid only when a term matches.\nlet wordSegmenter: Intl.Segmenter | undefined;\n\nfunction findOccurrences(text: string, terms: string[]): Array<{ start: number; end: number; term: string }> {\n const occ: Array<{ start: number; end: number; term: string }> = [];\n const textLower = text.toLowerCase();\n\n // Unspaced scripts (UNSPACED_SCRIPTS) mark no word boundaries: the sense-setup skill promises\n // substring semantics there, so a matching term is found wherever it occurs in the run.\n const unspacedTerms = terms.filter(hasUnspacedRun);\n if (unspacedTerms.length > 0) {\n for (const run of unspacedRuns(text)) {\n const runLower = textLower.slice(run.start, run.end);\n for (const term of unspacedTerms) {\n let idx = 0;\n for (;;) {\n const found = runLower.indexOf(term, idx);\n if (found === -1) break;\n occ.push({ start: run.start + found, end: run.start + found + term.length, term });\n idx = found + term.length;\n }\n }\n }\n }\n\n // Spaced scripts: a word is marked when its porter stem equals a query term's stem, the same\n // algorithm both indexes tokenize with (sqlite's TOKENIZE = 'porter unicode61'; duckdb's fts default).\n const spacedTerms = terms.filter((t) => !hasUnspacedRun(t));\n if (spacedTerms.length > 0) {\n const termByStem = new Map(spacedTerms.map((t) => [stemmer(foldForSearch(t)), t]));\n const stemInitials = new Set(Array.from(termByStem.keys(), (term) => term[0]));\n for (const match of searchTokens(text)) {\n const segment = match.text;\n if (match.unspaced) continue;\n const folded = foldForSearch(segment);\n // Porter stemming changes suffixes only, so a different initial cannot match a query stem.\n if (!stemInitials.has(folded[0])) continue;\n const term = termByStem.get(stemmer(folded));\n if (term !== undefined) occ.push({ start: match.start, end: match.end, term });\n }\n }\n\n return occ.sort((a, b) => a.start - b.start || b.end - a.end);\n}\n\n// Segment boundaries of the whole text, word granularity: the only cut points a snippet edge\n// may land on. One rule for every script (DESIGN.md \"Cutting\") -- a regex \\b is ASCII-only and\n// would mis-cut Japanese, Chinese and Thai, which Intl.Segmenter divides by dictionary instead.\nfunction wordBoundaries(text: string): number[] {\n wordSegmenter ??= new Intl.Segmenter('en', { granularity: 'word' });\n const bounds = [0];\n for (const { index, segment } of wordSegmenter.segment(text)) bounds.push(index + segment.length);\n return bounds;\n}\n\n// bounds is sorted: snaps are binary searches, since each runs once per candidate window.\nfunction snapForward(bounds: number[], pos: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (bounds[mid] < pos) lo = mid + 1;\n else hi = mid;\n }\n return bounds[lo];\n}\n\nfunction snapBackward(bounds: number[], pos: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n let result = bounds[0];\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (bounds[mid] <= pos) {\n lo = mid;\n result = bounds[mid];\n } else hi = mid - 1;\n }\n return result;\n}\n\nfunction firstBoundAfter(bounds: number[], pos: number): number | undefined {\n let lo = 0;\n let hi = bounds.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (bounds[mid] <= pos) lo = mid + 1;\n else hi = mid;\n }\n return bounds[lo];\n}\n\n// A raw, unmarked upper bound: [start, end) inside [anchor, anchor+charLimit), edges on\n// boundaries. Markup (guillemets, ellipses) added on top of this can push the rendered result\n// past charLimit, which is what fitEnd() below corrects for; this is only the starting guess.\n// A single segment spanning the whole target window (a word longer than the char limit) is the\n// one case where holding the boundary rule means the budget is not a hard ceiling.\nfunction cutWindow(text: string, bounds: number[], anchor: number, charLimit: number): { start: number; end: number } {\n const rawEnd = Math.min(text.length, anchor + charLimit);\n const start = snapForward(bounds, anchor);\n let end = snapBackward(bounds, rawEnd);\n if (end <= start) end = firstBoundAfter(bounds, start) ?? text.length;\n return { start, end: Math.min(end, text.length) };\n}\n\n// The largest boundary <= upperBound, > floor, whose rendered [*, end) fits charLimit. Markup\n// only ever adds characters as end grows, so rendered length is non-decreasing in end and\n// shrinking from upperBound is guaranteed to find the true fit rather than guess at it.\n// `floor` is the one end this never shrinks past (the anchor's own marked word, or the first\n// word in the unmarked fallback): DESIGN.md's budget is a ceiling except here, where returning\n// nothing is worse than a single word that overruns it.\nfunction fitEnd(render: (end: number) => string, bounds: number[], floor: number, upperBound: number, charLimit: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (bounds[mid] <= upperBound) lo = mid;\n else hi = mid - 1;\n }\n for (; lo >= 0 && bounds[lo] > floor; lo--) {\n if (render(bounds[lo]).length <= charLimit) return bounds[lo];\n }\n return floor;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\ninterface SnippetWindow extends Span {\n distinct: number;\n count: number;\n}\n\n// occ is start-sorted: a window's first candidate occurrence is a binary search, not a walk.\nfunction firstOccAtOrAfter(occ: Array<{ start: number; end: number; term: string }>, pos: number): number {\n let lo = 0;\n let hi = occ.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (occ[mid].start < pos) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\n\nfunction renderWindow(text: string, occ: Array<{ start: number; end: number; term: string }>, win: Span): string {\n let out = '';\n let cursor = win.start;\n for (let i = firstOccAtOrAfter(occ, win.start); i < occ.length && occ[i].start < win.end; i++) {\n const o = occ[i];\n if (o.end > win.end) continue;\n // Occurrences of duplicate or substring-overlapping terms (\"test tests\") can overlap;\n // emitting each would duplicate document text. Keep the first, absorb the rest.\n if (o.start < cursor) continue;\n out += `${text.slice(cursor, o.start)}«${text.slice(o.start, o.end)}»`;\n cursor = o.end;\n }\n out += text.slice(cursor, win.end);\n const prefix = win.start > 0 ? '…' : '';\n const suffix = win.end < text.length ? '…' : '';\n return `${prefix}${out.replace(/\\s+/g, ' ')}${suffix}`;\n}\n\n// Scores every occurrence-anchored candidate window: most distinct terms wins, ties broken by\n// most total hits -- coverage now that occurrences are real words, not the false-fragment counts\n// a substring scan produced. Selection itself is unevaluated (DESIGN.md \"Selection, unsettled\").\n// The window's `end` is fitted to the *rendered* text, markup included, not the raw span: a\n// window dense with marks (every guillemet pair adds two characters) shrinks to fit the same\n// budget a sparse one fills almost exactly.\nfunction scoreWindows(occ: Array<{ start: number; end: number; term: string }>, bounds: number[], text: string, charLimit: number): SnippetWindow[] {\n const candidates: SnippetWindow[] = [];\n for (let i = 0; i < occ.length; i++) {\n const winEnd = occ[i].start + charLimit;\n const seen = new Set<string>();\n let count = 0;\n for (let j = i; j < occ.length && occ[j].start < winEnd; j++) {\n seen.add(occ[j].term);\n count++;\n }\n const start = snapForward(bounds, occ[i].start);\n const floor = Math.max(start, occ[i].end);\n const upperBound = Math.max(floor, cutWindow(text, bounds, occ[i].start, charLimit).end);\n const end = fitEnd((e) => renderWindow(text, occ, { start, end: e }), bounds, floor, upperBound, charLimit);\n candidates.push({ start, end, distinct: seen.size, count });\n }\n return candidates;\n}\n\nfunction overlaps(a: Span, b: Span): boolean {\n return a.start < b.end && b.start < a.end;\n}\n\n// Same scoring function as a single snippet, kept rather than discarded (DESIGN.md \"Several\n// snippets per note\"): rank by score, take the top non-overlapping windows, then reorder to the\n// note's own order, since several passages from one note read as an abridged note.\nfunction selectWindows(candidates: SnippetWindow[], countLimit: number): { chosen: SnippetWindow[]; best: SnippetWindow } {\n const ranked = candidates.slice().sort((a, b) => b.distinct - a.distinct || b.count - a.count);\n const chosen: SnippetWindow[] = [];\n for (const c of ranked) {\n if (chosen.length >= countLimit) break;\n if (chosen.some((s) => overlaps(s, c))) continue;\n chosen.push(c);\n }\n chosen.sort((a, b) => a.start - b.start);\n return { chosen, best: ranked[0] };\n}\n\n// Builds bounded marked passages and returns the best window's offset for section lookup.\nexport function computeSnippets(text: string, terms: string[], charLimit: number, countLimit: number): { snippets: string[]; offset: number } {\n const occ = findOccurrences(text, terms);\n const bounds = wordBoundaries(text);\n if (occ.length === 0) {\n // A term matched only via title/summary, not this body text, finds no occurrence here;\n // fall back to the doc's start, unmarked.\n const render = (end: number): string => `${text.slice(0, end).replace(/\\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`;\n const floor = bounds.find((b) => b > 0) ?? text.length;\n const upperBound = Math.max(floor, cutWindow(text, bounds, 0, charLimit).end);\n const end = fitEnd(render, bounds, floor, upperBound, charLimit);\n return { snippets: [render(end)], offset: 0 };\n }\n const candidates = scoreWindows(occ, bounds, text, charLimit);\n const { chosen, best } = selectWindows(candidates, countLimit);\n return { snippets: chosen.map((win) => renderWindow(text, occ, win)), offset: best.start };\n}\n\n// Converts an offset in the original text to its one-based line number.\nexport function lineNumberAt(text: string, offset: number): number {\n let line = 1;\n for (let i = 0; i < offset; i++) if (text.charCodeAt(i) === 10) line++;\n return line;\n}\n\nasync function lineRangeFor(store: Store, path: string, line: number): Promise<string | null> {\n const stmt = await store.prepare('SELECT start_line, end_line FROM sections WHERE \"path\" = ? AND start_line <= ? AND end_line >= ? ORDER BY start_line DESC LIMIT 1');\n const row = (await stmt.get(path, line, line)) as { start_line: number; end_line: number } | undefined;\n return row ? `L${row.start_line}-${row.end_line}` : null;\n}\n\nexport interface SearchOptions {\n k?: number;\n where?: string; // SQL fragment against frontmatter alias `f`, e.g. \"f.status = 'active'\"\n preset?: string; // named preset; unknown name throws listing declared presets, undefined -> \"default\"\n include?: string[]; // ad hoc scope override (repeatable --include); independent of exclude\n exclude?: string[]; // ad hoc scope override (repeatable --exclude); independent of include\n noExclude?: boolean; // --no-exclude: drop the preset's exclude for this command\n snippetCharLimit?: number; // --snippet-char-limit; defaults to SNIPPET_CHAR_LIMIT_DEFAULT\n snippetCountLimit?: number; // --snippet-count-limit; defaults to SNIPPET_COUNT_LIMIT_DEFAULT\n}\n\n// Hydrates already-ranked rows with snippets and containing-section ranges without reranking.\nexport async function hydrateSearchRows(store: Store, cfg: ResolvedConfig, rows: Row[], matchedPaths: Set<string>, terms: string, opts: Pick<SearchOptions, 'snippetCharLimit' | 'snippetCountLimit'> = {}): Promise<void> {\n if (matchedPaths.size === 0) return;\n const bareTerms = extractBareTerms(terms);\n const charLimit = opts.snippetCharLimit ?? SNIPPET_CHAR_LIMIT_DEFAULT;\n const countLimit = opts.snippetCountLimit ?? SNIPPET_COUNT_LIMIT_DEFAULT;\n for (const row of rows) {\n if (!matchedPaths.has(row.path as string)) continue;\n let text: string;\n try {\n text = readFileSync(join(cfg.rootDir ?? cfg.baseDir, row.path as string), 'utf8');\n } catch {\n continue; // vanished since the match; leave snippets/lines empty rather than throw\n }\n const { snippets, offset } = computeSnippets(text, bareTerms, charLimit, countLimit);\n row.snippets = snippets;\n if (row.lines == null) row.lines = featureEnabled(cfg, 'sections') ? await lineRangeFor(store, row.path as string, lineNumberAt(text, offset)) : null;\n }\n}\n\n// The declared or defaulted signals compose via RRF; `via` names which ones produced each row.\n// `opts` arrives already resolved (config.ts:resolveSearch).\nexport async function search(store: Store, cfg: ResolvedConfig, terms: string, opts: SearchOptions = {}): Promise<Row[]> {\n const effective = resolveSearch(cfg, opts);\n const { k, signals } = effective;\n\n const allPaths = ((await (await store.prepare('SELECT \"path\" FROM frontmatter')).all()) as Array<{ path: string }>).map((r) => r.path);\n const scopePaths = await rawScope(store, cfg, opts, allPaths);\n const scopeActive = scopePaths.size < allPaths.length;\n // The set every candidate pool must be filtered to before truncation: scope narrowed by\n // --where, the same composition scopedPaths() gives the other commands.\n let allowedPaths: Set<string>;\n try {\n allowedPaths = await narrowByWhere(store, scopePaths, effective.where);\n } catch (err) {\n throw searchError(err as Error, terms, effective.where);\n }\n const fetch = Math.max(k * 3, 30);\n\n // A downloadable HF id proceeds -- getProvider fetches it lazily on consent. Only a\n // local path with missing files errors here, since nothing will ever fetch it for itself.\n const wantsVectors = signals.vectors !== undefined;\n if (wantsVectors) {\n const e = embedConfig(cfg); // validate.ts guarantees this is set whenever \"vectors\" is declared\n if (localModelMissing(e)) {\n throw new SenseError('EMBED_MODEL_MISSING', `preset \"${effective.presetName}\" searches with vectors, but the local model path \"${e.model}\" is missing ${MODEL_FILENAMES}; point embed.model at a directory containing them, or drop \"vectors\" from that preset's signals to search without them`);\n }\n }\n const semanticEnabled = wantsVectors && (await scopeHasEmbeddings(store, cfg, allowedPaths));\n\n // --where applies inside the candidate query (a post-filter would drop matches ranked past\n // the pool) and again on the final select, for link-derived rows.\n const scope = effective.where;\n const whereJoin = scope ? `JOIN frontmatter f ON f.\"path\" = content.path` : '';\n const whereCond = scope ? `AND (${scope})` : '';\n // Filtered before LIMIT, not after, or scoped notes ranking below the global top-`fetch`\n // never reach the filter; joined against a temp table since real scopes exceed SQLITE_MAX_VARIABLE_NUMBER.\n if (scopeActive) await materializeScope(store, '_search_scope', scopePaths);\n const scopeCond = scopeActive ? `AND content.path IN (SELECT \"path\" FROM _search_scope)` : '';\n\n const candidates = new Map<string, { score: number; via: string }>();\n let matchRows: LexicalHit[] = [];\n const hasEmptyPhrase = [...terms.matchAll(/\"([^\"]*)\"/g)].some((match) => !/[\\p{L}\\p{N}_]/u.test(match[1]));\n const hasBareSyntaxOnly = /[()]/u.test(terms.replace(/\"[^\"]*\"/g, ' '));\n // A query that is only whitespace or punctuation has no lexical rows; other declared signals\n // compose normally. In particular, unspaced-script punctuation can rewrite to an empty MATCH.\n if (signals.words !== undefined && terms.trim() !== '' && (/[\\p{L}\\p{N}_]/u.test(terms) || hasBareSyntaxOnly) && !hasEmptyPhrase) {\n const syntaxError = bareTermSyntaxError(terms);\n if (syntaxError) throw syntaxError;\n try {\n matchRows = await wordsCandidates(store, candidates, terms, whereJoin, whereCond, scopeCond, fetch, signals.words);\n } catch (err) {\n throw searchError(err as Error, terms, scope);\n }\n }\n // Every store's lexical `snippets` is NULL; the snippet text is always computed below.\n // Distinguish from via='link' rows, which never appear in matchRows.\n const matchedPaths = new Set(matchRows.map((r) => r.path));\n\n if (signals.links !== undefined) await linksCandidates(store, candidates, matchRows, allPaths, allowedPaths, fetch, signals.links);\n\n let chunkLines = new Map<string, string>();\n let chunkSimilarity = new Map<string, number>();\n if (semanticEnabled) {\n ({ chunkLines, chunkSimilarity } = await vectorsCandidates(store, cfg, candidates, terms, fetch, allowedPaths, signals.vectors as number));\n }\n\n await store.exec('DROP TABLE IF EXISTS _search');\n // DOUBLE, not REAL: sqlite's REAL is an 8-byte double but duckdb's is a 4-byte float, and the\n // rounded score/similarity are printed at full precision in rows.\n await store.exec('CREATE TEMP TABLE _search (\"path\" TEXT PRIMARY KEY, score DOUBLE, via TEXT, lines TEXT, similarity DOUBLE)');\n if (candidates.size > 0) {\n await store.runBatch(\n 'INSERT INTO _search (\"path\", score, via, lines, similarity) VALUES (?, ?, ?, ?, ?)',\n [...candidates].map(([path, c]) => [path, c.score, c.via, chunkLines.get(path) ?? null, chunkSimilarity.get(path) ?? null])\n );\n }\n\n // Reapplies --where even though _search is already scope+where filtered, since the join to\n // frontmatter is already needed for the path column.\n const where = scope ? `WHERE (${scope})` : '';\n // lines: semantic rows carry their chunk's range, lexical rows gain one below,\n // everything else stays null; similarity stays semantic-only.\n const similarityCol = semanticEnabled ? ', _search.similarity' : '';\n // NULL AS snippets holds this column's position (between summary and via) for every row;\n // overwritten below, always to an array, never left null (DESIGN.md).\n const selectStmt = await store.prepare(\n `SELECT f.\"path\" AS path, content.title, content.summary, NULL AS snippets, _search.via, round(_search.score, 4) AS score, _search.lines${similarityCol}\n FROM _search JOIN frontmatter f ON f.\"path\" = _search.\"path\" JOIN content ON content.path = _search.\"path\"\n ${where} ORDER BY _search.score DESC LIMIT ?`\n );\n const rows = (await selectStmt.all(k)) as Row[];\n for (const row of rows) row.snippets = [];\n\n await hydrateSearchRows(store, cfg, rows, matchedPaths, terms, opts);\n\n return rows;\n}\n"],"names":["readFileSync","join","stemmer","embedConfig","featureEnabled","resolveSearch","localModelMissing","MODEL_FILENAMES","SenseError","bareTermSyntaxError","searchError","foldForSearch","hasUnspacedRun","searchTokens","unspacedRuns","materializeScope","narrowByWhere","rawScope","scopeHasEmbeddings","linksCandidates","vectorsCandidates","wordsCandidates","SNIPPET_CHAR_LIMIT_DEFAULT","SNIPPET_COUNT_LIMIT_DEFAULT","extractBareTerms","query","cleaned","replace","split","map","tok","flatMap","text","toLowerCase","trim","filter","length","wordSegmenter","findOccurrences","terms","occ","textLower","unspacedTerms","run","runLower","slice","start","end","term","idx","found","indexOf","push","spacedTerms","t","termByStem","Map","stemInitials","Set","Array","from","keys","match","segment","unspaced","folded","has","get","undefined","sort","a","b","wordBoundaries","Intl","Segmenter","granularity","bounds","index","snapForward","pos","lo","hi","mid","snapBackward","result","firstBoundAfter","cutWindow","anchor","charLimit","rawEnd","Math","min","fitEnd","render","floor","upperBound","firstOccAtOrAfter","renderWindow","win","out","cursor","i","o","prefix","suffix","scoreWindows","candidates","winEnd","seen","count","j","add","max","e","distinct","size","overlaps","selectWindows","countLimit","ranked","chosen","c","some","s","best","computeSnippets","find","snippets","offset","lineNumberAt","line","charCodeAt","lineRangeFor","store","path","stmt","prepare","row","start_line","end_line","hydrateSearchRows","cfg","rows","matchedPaths","opts","bareTerms","snippetCharLimit","snippetCountLimit","rootDir","baseDir","lines","search","effective","k","signals","allPaths","all","r","scopePaths","scopeActive","allowedPaths","where","err","fetch","wantsVectors","vectors","presetName","model","semanticEnabled","scope","whereJoin","whereCond","scopeCond","matchRows","hasEmptyPhrase","matchAll","test","hasBareSyntaxOnly","words","syntaxError","links","chunkLines","chunkSimilarity","exec","runBatch","score","via","similarityCol","selectStmt"],"mappings":"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"}
@@ -95,9 +95,16 @@ export function featureSignature(cfg, features) {
95
95
  ].sort().join('+');
96
96
  return `preset:${name}:${include}:${exclude}:${presetHasSignal(cfg, name, 'vectors') ? 'on' : 'off'}`;
97
97
  }).join('|');
98
+ // A configured root changes every filesystem input while retaining the cache under the
99
+ // config directory. Its own segment therefore forces the safe full-rebuild path. Omission
100
+ // intentionally preserves the pre-root signature and cache behavior.
101
+ const rootPart = cfg.root === undefined ? [] : [
102
+ `root:${cfg.root}`
103
+ ];
98
104
  return [
99
105
  ...togglesPart,
100
106
  ...featureParts,
107
+ ...rootPart,
101
108
  presetsPart
102
109
  ].join('|');
103
110
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/access.ts"],"sourcesContent":["import type { Feature } from '../features/types.ts';\nimport type { SignalName, SignalWeights } from './signals.ts';\nimport { type Config, type EmbedConfig, FEATURE_NAMES, type FeatureName, type StoreName } from './types.ts';\n\nexport function presetNames(cfg: Config): string[] {\n return Object.keys(cfg.presets);\n}\n\n// The backing store engine this tree uses; absent means the default.\nexport function storeName(cfg: Config): StoreName {\n return cfg.store ?? 'sqlite';\n}\n\n// Whether the tree names a model at all.\nexport function embedEnabled(cfg: Config): boolean {\n return typeof cfg.embed?.model === 'string' && cfg.embed.model.length > 0;\n}\n\n// A preset's effective signal weights: declared exhaustively when `signals` is present, else\n// every signal whose prerequisite holds at weight 1 (words always, links when on, vectors when a model is named).\nexport function presetSignals(cfg: Config, name: string): SignalWeights {\n const declared = cfg.presets[name]?.signals;\n if (declared) return declared;\n const signals: SignalWeights = { words: 1 };\n if (cfg.features?.links !== false) signals.links = 1;\n if (embedEnabled(cfg)) signals.vectors = 1;\n return signals;\n}\n\nexport function presetHasSignal(cfg: Config, name: string, signal: SignalName): boolean {\n return presetSignals(cfg, name)[signal] !== undefined;\n}\n\n// Whether embedding needs to run: a model is named AND some preset's effective signals include vectors.\nexport function anyPresetEmbeds(cfg: Config): boolean {\n return presetNames(cfg).some((name) => presetHasSignal(cfg, name, 'vectors'));\n}\n\n// Opt-out features (default on): absent block or key means enabled. `rank` additionally requires\n// `links`. `embed` is derived (not a features-block member): on iff some preset's signals include vectors.\nexport function featureEnabled(cfg: Config, name: FeatureName): boolean {\n if (name === 'embed') return anyPresetEmbeds(cfg);\n const enabled = cfg.features?.[name] !== false;\n if (name === 'rank') return enabled && featureEnabled(cfg, 'links');\n return enabled;\n}\n\nexport function enabledFeatures(cfg: Config): FeatureName[] {\n return FEATURE_NAMES.filter((name) => featureEnabled(cfg, name));\n}\n\n// Feature states, so \"off\" and \"empty\" stay distinguishable in output. `embed` is excluded:\n// it is not a features-block toggle, and `status` reports it on its own line.\nexport function featureStates(cfg: Config): { on: FeatureName[]; off: FeatureName[] } {\n const reported = FEATURE_NAMES.filter((name) => name !== 'embed');\n return {\n on: reported.filter((name) => featureEnabled(cfg, name)),\n off: reported.filter((name) => !featureEnabled(cfg, name)),\n };\n}\n\n// Resolved embed provider settings, or null when the config names no model. No fallback to\n// DEFAULT_EMBED_MODEL: that's a template constant `init` and the v4 migration write into the file, never a runtime default.\nexport function embedConfig(cfg: Config): { model: string; provider: 'static' | 'openai' | 'cohere'; url?: string; key?: string; languages?: string[]; chunkTokens?: number } | null {\n if (!embedEnabled(cfg)) return null;\n const e = cfg.embed as EmbedConfig;\n return { model: e.model as string, provider: e.provider ?? 'static', url: e.url, key: e.key, languages: e.languages, chunkTokens: e.chunkTokens };\n}\n\n// Cache key over everything indexing derives from, so any change to those inputs rebuilds.\n// Segment order is fixed (one `feature:<name>:on|off` per FEATURE_NAMES entry but embed, then\n// `features` in registry order, then presets) since reordering alone would fire a spurious\n// rebuild; presets stay here because no Feature module owns it.\nexport function featureSignature(cfg: Config, features: Feature[]): string {\n // One segment per toggle (not one lumped csv) so store/signature.ts's changedSignatureKeys can\n // tell which feature moved -- store/feature-scope.ts routes a recognised toggle to a narrow\n // per-table invalidation instead of the full clear.\n const togglesPart = FEATURE_NAMES.filter((name) => name !== 'embed').map((name) => `feature:${name}:${featureEnabled(cfg, name) ? 'on' : 'off'}`);\n const featureParts = features.map((feature) => feature.signature?.(cfg)).filter((part): part is string => part !== undefined);\n // One keyed segment per preset so a rebuild notice can name exactly which preset moved.\n const presetsPart = [...presetNames(cfg)]\n .sort()\n .map((name) => {\n const p = cfg.presets[name];\n const include = [...p.include].sort().join('+');\n const exclude = [...(p.exclude ?? [])].sort().join('+');\n return `preset:${name}:${include}:${exclude}:${presetHasSignal(cfg, name, 'vectors') ? 'on' : 'off'}`;\n })\n .join('|');\n return [...togglesPart, ...featureParts, presetsPart].join('|');\n}\n"],"names":["FEATURE_NAMES","presetNames","cfg","Object","keys","presets","storeName","store","embedEnabled","embed","model","length","presetSignals","name","declared","signals","words","features","links","vectors","presetHasSignal","signal","undefined","anyPresetEmbeds","some","featureEnabled","enabled","enabledFeatures","filter","featureStates","reported","on","off","embedConfig","e","provider","url","key","languages","chunkTokens","featureSignature","togglesPart","map","featureParts","feature","signature","part","presetsPart","sort","p","include","join","exclude"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/access.ts"],"sourcesContent":["import type { Feature } from '../features/types.ts';\nimport type { SignalName, SignalWeights } from './signals.ts';\nimport { type Config, type EmbedConfig, FEATURE_NAMES, type FeatureName, type StoreName } from './types.ts';\n\nexport function presetNames(cfg: Config): string[] {\n return Object.keys(cfg.presets);\n}\n\n// The backing store engine this tree uses; absent means the default.\nexport function storeName(cfg: Config): StoreName {\n return cfg.store ?? 'sqlite';\n}\n\n// Whether the tree names a model at all.\nexport function embedEnabled(cfg: Config): boolean {\n return typeof cfg.embed?.model === 'string' && cfg.embed.model.length > 0;\n}\n\n// A preset's effective signal weights: declared exhaustively when `signals` is present, else\n// every signal whose prerequisite holds at weight 1 (words always, links when on, vectors when a model is named).\nexport function presetSignals(cfg: Config, name: string): SignalWeights {\n const declared = cfg.presets[name]?.signals;\n if (declared) return declared;\n const signals: SignalWeights = { words: 1 };\n if (cfg.features?.links !== false) signals.links = 1;\n if (embedEnabled(cfg)) signals.vectors = 1;\n return signals;\n}\n\nexport function presetHasSignal(cfg: Config, name: string, signal: SignalName): boolean {\n return presetSignals(cfg, name)[signal] !== undefined;\n}\n\n// Whether embedding needs to run: a model is named AND some preset's effective signals include vectors.\nexport function anyPresetEmbeds(cfg: Config): boolean {\n return presetNames(cfg).some((name) => presetHasSignal(cfg, name, 'vectors'));\n}\n\n// Opt-out features (default on): absent block or key means enabled. `rank` additionally requires\n// `links`. `embed` is derived (not a features-block member): on iff some preset's signals include vectors.\nexport function featureEnabled(cfg: Config, name: FeatureName): boolean {\n if (name === 'embed') return anyPresetEmbeds(cfg);\n const enabled = cfg.features?.[name] !== false;\n if (name === 'rank') return enabled && featureEnabled(cfg, 'links');\n return enabled;\n}\n\nexport function enabledFeatures(cfg: Config): FeatureName[] {\n return FEATURE_NAMES.filter((name) => featureEnabled(cfg, name));\n}\n\n// Feature states, so \"off\" and \"empty\" stay distinguishable in output. `embed` is excluded:\n// it is not a features-block toggle, and `status` reports it on its own line.\nexport function featureStates(cfg: Config): { on: FeatureName[]; off: FeatureName[] } {\n const reported = FEATURE_NAMES.filter((name) => name !== 'embed');\n return {\n on: reported.filter((name) => featureEnabled(cfg, name)),\n off: reported.filter((name) => !featureEnabled(cfg, name)),\n };\n}\n\n// Resolved embed provider settings, or null when the config names no model. No fallback to\n// DEFAULT_EMBED_MODEL: that's a template constant `init` and the v4 migration write into the file, never a runtime default.\nexport function embedConfig(cfg: Config): { model: string; provider: 'static' | 'openai' | 'cohere'; url?: string; key?: string; languages?: string[]; chunkTokens?: number } | null {\n if (!embedEnabled(cfg)) return null;\n const e = cfg.embed as EmbedConfig;\n return { model: e.model as string, provider: e.provider ?? 'static', url: e.url, key: e.key, languages: e.languages, chunkTokens: e.chunkTokens };\n}\n\n// Cache key over everything indexing derives from, so any change to those inputs rebuilds.\n// Segment order is fixed (one `feature:<name>:on|off` per FEATURE_NAMES entry but embed, then\n// `features` in registry order, then presets) since reordering alone would fire a spurious\n// rebuild; presets stay here because no Feature module owns it.\nexport function featureSignature(cfg: Config, features: Feature[]): string {\n // One segment per toggle (not one lumped csv) so store/signature.ts's changedSignatureKeys can\n // tell which feature moved -- store/feature-scope.ts routes a recognised toggle to a narrow\n // per-table invalidation instead of the full clear.\n const togglesPart = FEATURE_NAMES.filter((name) => name !== 'embed').map((name) => `feature:${name}:${featureEnabled(cfg, name) ? 'on' : 'off'}`);\n const featureParts = features.map((feature) => feature.signature?.(cfg)).filter((part): part is string => part !== undefined);\n // One keyed segment per preset so a rebuild notice can name exactly which preset moved.\n const presetsPart = [...presetNames(cfg)]\n .sort()\n .map((name) => {\n const p = cfg.presets[name];\n const include = [...p.include].sort().join('+');\n const exclude = [...(p.exclude ?? [])].sort().join('+');\n return `preset:${name}:${include}:${exclude}:${presetHasSignal(cfg, name, 'vectors') ? 'on' : 'off'}`;\n })\n .join('|');\n // A configured root changes every filesystem input while retaining the cache under the\n // config directory. Its own segment therefore forces the safe full-rebuild path. Omission\n // intentionally preserves the pre-root signature and cache behavior.\n const rootPart = cfg.root === undefined ? [] : [`root:${cfg.root}`];\n return [...togglesPart, ...featureParts, ...rootPart, presetsPart].join('|');\n}\n"],"names":["FEATURE_NAMES","presetNames","cfg","Object","keys","presets","storeName","store","embedEnabled","embed","model","length","presetSignals","name","declared","signals","words","features","links","vectors","presetHasSignal","signal","undefined","anyPresetEmbeds","some","featureEnabled","enabled","enabledFeatures","filter","featureStates","reported","on","off","embedConfig","e","provider","url","key","languages","chunkTokens","featureSignature","togglesPart","map","featureParts","feature","signature","part","presetsPart","sort","p","include","join","exclude","rootPart","root"],"mappings":"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"}
@@ -239,6 +239,7 @@ export function findConfigPath(startDir) {
239
239
  }
240
240
  }
241
241
  export function loadConfig(explicitPath) {
242
+ var _cfg_root;
242
243
  let configPath;
243
244
  if (explicitPath) {
244
245
  configPath = resolve(process.cwd(), explicitPath);
@@ -277,9 +278,13 @@ export function loadConfig(explicitPath) {
277
278
  cfg = validateConfig(parsed, configPath);
278
279
  }
279
280
  const unknownKeys = unknownConfigKeys(cfg);
281
+ const configDir = dirname(configPath);
282
+ const rootDir = resolve(configDir, (_cfg_root = cfg.root) !== null && _cfg_root !== void 0 ? _cfg_root : '.');
280
283
  return {
281
284
  ...cfg,
282
- baseDir: dirname(configPath),
285
+ configDir,
286
+ rootDir,
287
+ baseDir: rootDir,
283
288
  configPath,
284
289
  migratedFrom,
285
290
  unknownKeys: unknownKeys.length > 0 ? unknownKeys : undefined
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/load.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from '../errors.ts';\nimport { writeFileAtomic } from '../lib/atomic-write.ts';\nimport type { Config, ResolvedConfig } from './types.ts';\nimport { CONFIG_FILENAME, DEFAULT_EMBED_MODEL, SUPPORTED_CONFIG_VERSION } from './types.ts';\nimport { unknownConfigKeys, validateConfig, validateLegacyScan } from './validate.ts';\n\n// v1 and v2 were `scan`/`find`-shaped; v3 introduced `presets`. Only the former need the\n// legacy shape check before migrating.\nconst FIRST_PRESET_VERSION = 3;\n\n// v1 -> v2 migration only: the features v2 introduced as opt-out (embed was opt-in then).\nconst V2_OPT_OUT_NAMES = ['links', 'sections', 'rank'] as const;\n\n// Pure per-version steps, chained by loadConfig. Intermediate shapes predate the Config type,\n// so steps are loosely typed and only the final result is cast back.\nconst MIGRATIONS: Record<number, (cfg: Record<string, unknown>) => Record<string, unknown>> = {\n // v1 -> v2: features block introduced, opt-out features enabled (matches the old implicit\n // behavior of `links` etc. not existing). embed stays absent -- opt-in.\n 1: (cfg) => ({ ...cfg, version: 2, features: Object.fromEntries(V2_OPT_OUT_NAMES.map((name) => [name, true])) }),\n // v2 -> v3: mechanical-minimal by decision -- only what keeps an existing config loading (no\n // restructuring, no preset inference, no query rewriting); real trees get hand-migrated to use presets.\n 2: (cfg) => {\n const { scan, defaults, features, checks, queries, ...rest } = cfg;\n const scanInclude = (scan as { include: string[] }).include;\n\n const nextQueries: Record<string, unknown> = {};\n for (const [name, value] of Object.entries((queries as Record<string, unknown> | undefined) ?? {})) {\n if (typeof value === 'string') {\n nextQueries[name] = value;\n continue;\n }\n const entry = value as { find: string; k?: number; where?: string; semantic?: boolean };\n // Loosely typed on purpose: this step emits the v3 shape, which still had `semantic`.\n // The v3 -> v4 step below strips it.\n const search: Record<string, unknown> = { search: entry.find };\n if (entry.k !== undefined) search.k = entry.k;\n if (entry.where !== undefined) search.where = entry.where;\n // semantic: true was opt-in; it is now the default, so it drops. semantic: false is kept.\n if (entry.semantic === false) search.semantic = false;\n nextQueries[name] = search;\n }\n\n const prevDefaults = defaults as { find?: { where?: string } } | undefined;\n const defaultWhere = prevDefaults?.find?.where;\n\n // features.embed false becomes semantic:false on every preset produced here.\n const prevFeatures = (features as Record<string, unknown> | undefined) ?? {};\n const embedWasOff = prevFeatures.embed === false;\n // Object form carries provider settings (model/type/url/key), not a toggle -- they move to\n // the v3 top-level `embed` block verbatim; dropping them would silently switch an api tree to the static model.\n const embedProvider = typeof prevFeatures.embed === 'object' && prevFeatures.embed !== null ? prevFeatures.embed : undefined;\n const { embed: _embed, ...restFeatures } = prevFeatures;\n\n const defaultPreset: Record<string, unknown> = { include: scanInclude };\n if (defaultWhere !== undefined) defaultPreset.where = defaultWhere;\n if (embedWasOff) defaultPreset.semantic = false;\n\n if (checks !== undefined) {\n console.error('sense: v2 \"checks\" was removed in v3 (sense check no longer asserts on saved queries); its queries are carried over under \"queries\" -- a returned row set is now the reader\\'s judgment');\n }\n\n const result: Record<string, unknown> = { ...rest, version: 3, presets: { default: defaultPreset }, queries: nextQueries };\n if (embedProvider !== undefined) result.embed = embedProvider;\n if (Object.keys(restFeatures).length > 0) result.features = restFeatures;\n return result;\n },\n // v3 -> v4: write the model into the file, wrap bare-string queries as { sql }, drop a saved\n // search's `semantic` (its preset decides). Same effective model, so nothing re-embeds.\n 3: (cfg) => {\n const presets = cfg.presets as Record<string, Record<string, unknown>>;\n const embedded = Object.values(presets).some((p) => p.semantic !== false);\n\n const nextQueries: Record<string, unknown> = {};\n for (const [name, value] of Object.entries((cfg.queries as Record<string, unknown> | undefined) ?? {})) {\n // v3's bare string meant SQL by inference; v4 makes every entry name its verb.\n if (typeof value === 'string') {\n nextQueries[name] = { sql: value };\n continue;\n }\n if (typeof value !== 'object' || value === null || !('search' in value)) {\n nextQueries[name] = value;\n continue;\n }\n const { semantic: _semantic, ...rest } = value as Record<string, unknown>;\n nextQueries[name] = rest;\n }\n\n const prev = (cfg.embed as Record<string, unknown> | undefined) ?? {};\n const result: Record<string, unknown> = { ...cfg, version: 4, queries: nextQueries };\n if (embedded) {\n result.embed = { model: (prev.model as string | undefined) ?? DEFAULT_EMBED_MODEL, type: (prev.type as string | undefined) ?? 'static', ...(prev.url !== undefined ? { url: prev.url } : {}), ...(prev.key !== undefined ? { key: prev.key } : {}) };\n } else {\n delete result.embed;\n // Dropping settings silently would look like a bug in a tree that configured an api endpoint\n // then turned vectors off; say it, since v4 has no way to carry provider settings for a tree that doesn't embed.\n if (Object.keys(prev).length > 0) console.error('sense: every preset has \"semantic\": false, so v4 drops the \"embed\" block (vectors stay off); re-add it, or set a preset\\'s semantic back to true, to turn them on');\n }\n return result;\n },\n // v4 -> v5: embed.type renames to embed.provider (\"api\" migrates to \"openai\", the wire protocol\n // it always was; other values pass through). A preset's `semantic` also migrates to `signals` (false -> explicit weights, true/absent -> no key).\n 4: (cfg) => {\n const prevEmbed = cfg.embed as Record<string, unknown> | undefined;\n const result: Record<string, unknown> = { ...cfg, version: 5 };\n if (prevEmbed) {\n const { type, ...rest } = prevEmbed;\n result.embed = type === undefined ? rest : { ...rest, provider: type === 'api' ? 'openai' : type };\n }\n\n const linksOn = (cfg.features as Record<string, unknown> | undefined)?.links !== false;\n const presets = cfg.presets as Record<string, Record<string, unknown>> | undefined;\n if (presets) {\n const nextPresets: Record<string, unknown> = {};\n for (const [name, preset] of Object.entries(presets)) {\n const { semantic, ...rest } = preset;\n if (semantic === undefined) {\n nextPresets[name] = preset;\n continue;\n }\n if (typeof semantic !== 'boolean') {\n throw new SenseError('CONFIG_INVALID', `presets.${name}.semantic must be a boolean`);\n }\n nextPresets[name] = semantic === false ? { ...rest, signals: linksOn ? { words: 1, links: 1 } : { words: 1 } } : rest;\n }\n result.presets = nextPresets;\n }\n return result;\n },\n};\n\nexport function migrateConfig(cfg: Config): { cfg: Config; from: number } {\n const from = cfg.version ?? 1;\n let current: Record<string, unknown> = cfg as unknown as Record<string, unknown>;\n for (let v = from; v < SUPPORTED_CONFIG_VERSION; v++) {\n const step = MIGRATIONS[v];\n if (!step) throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `no migration from config version ${v}`);\n current = step(current);\n }\n return { cfg: current as unknown as Config, from };\n}\n\n// `sense init`'s flags map onto these three `embed` keys one-to-one; an absent field\n// keeps the built-in default rather than merging partial input.\nexport interface InitOverrides {\n model?: string;\n provider?: string;\n url?: string;\n}\n\nfunction starterConfig(overrides?: InitOverrides): Config {\n const embed: Record<string, unknown> = { model: DEFAULT_EMBED_MODEL, provider: 'static' };\n if (overrides?.model !== undefined) embed.model = overrides.model;\n if (overrides?.provider !== undefined) embed.provider = overrides.provider;\n if (overrides?.url !== undefined) embed.url = overrides.url;\n return {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: SUPPORTED_CONFIG_VERSION,\n presets: {\n default: { include: ['**/*.md'], k: 10 },\n large: { include: ['**/*.md'], k: 20 },\n },\n // Written out rather than defaulted in code: this line is what makes `sense download`\n // fetch a model, so it belongs where it can be read and changed.\n embed: embed as Config['embed'],\n queries: {},\n };\n}\n\n// Refuses to overwrite an existing config. `overrides` come from `sense init`'s flags; an\n// invalid --provider fails validateConfig's own shape check before anything is written.\nexport function initConfig(dir: string, overrides?: InitOverrides): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n const cfg = starterConfig(overrides);\n validateConfig(cfg, configPath);\n writeFileAtomic(configPath, `${JSON.stringify(cfg, null, 2)}\\n`);\n return configPath;\n}\n\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nexport function loadConfig(explicitPath?: string): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n\n // Ahead of both the version gate and shape checks: a non-object file (or array -- typeof []\n // is also 'object') is malformed at every version, so it gets one error regardless of branch.\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);\n }\n\n // Version gate before shape validation: a config written by a newer sense should fail\n // with \"requires a newer sense\", not with shape errors its own version may not have.\n const version = typeof parsed === 'object' && parsed !== null && typeof (parsed as { version?: unknown }).version === 'number' ? (parsed as { version: number }).version : 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n // validateConfig only knows the current shape, so scan-shaped files (pre-v3) get a minimal\n // check, migrate, then the full one on the result.\n let cfg: Config;\n let migratedFrom: number | undefined;\n if (version < SUPPORTED_CONFIG_VERSION) {\n if (version < FIRST_PRESET_VERSION) validateLegacyScan(parsed, configPath);\n const result = migrateConfig(parsed as unknown as Config);\n cfg = validateConfig(result.cfg, configPath);\n migratedFrom = result.from;\n writeFileAtomic(configPath, `${JSON.stringify(cfg, null, 2)}\\n`);\n } else {\n cfg = validateConfig(parsed, configPath);\n }\n\n const unknownKeys = unknownConfigKeys(cfg as unknown as Record<string, unknown>);\n return { ...cfg, baseDir: dirname(configPath), configPath, migratedFrom, unknownKeys: unknownKeys.length > 0 ? unknownKeys : undefined };\n}\n"],"names":["existsSync","readFileSync","dirname","join","resolve","SenseError","writeFileAtomic","CONFIG_FILENAME","DEFAULT_EMBED_MODEL","SUPPORTED_CONFIG_VERSION","unknownConfigKeys","validateConfig","validateLegacyScan","FIRST_PRESET_VERSION","V2_OPT_OUT_NAMES","MIGRATIONS","cfg","version","features","Object","fromEntries","map","name","prevDefaults","scan","defaults","checks","queries","rest","scanInclude","include","nextQueries","value","entries","entry","search","find","k","undefined","where","semantic","defaultWhere","prevFeatures","embedWasOff","embed","embedProvider","_embed","restFeatures","defaultPreset","console","error","result","presets","default","keys","length","embedded","values","some","p","sql","_semantic","prev","model","type","url","key","prevEmbed","provider","linksOn","links","nextPresets","preset","signals","words","migrateConfig","from","current","v","step","starterConfig","overrides","$schema","large","initConfig","dir","configPath","JSON","stringify","findConfigPath","startDir","candidate","parent","loadConfig","explicitPath","process","cwd","found","raw","parsed","parse","Array","isArray","migratedFrom","unknownKeys","baseDir"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/load.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from '../errors.ts';\nimport { writeFileAtomic } from '../lib/atomic-write.ts';\nimport type { Config, ResolvedConfig } from './types.ts';\nimport { CONFIG_FILENAME, DEFAULT_EMBED_MODEL, SUPPORTED_CONFIG_VERSION } from './types.ts';\nimport { unknownConfigKeys, validateConfig, validateLegacyScan } from './validate.ts';\n\n// v1 and v2 were `scan`/`find`-shaped; v3 introduced `presets`. Only the former need the\n// legacy shape check before migrating.\nconst FIRST_PRESET_VERSION = 3;\n\n// v1 -> v2 migration only: the features v2 introduced as opt-out (embed was opt-in then).\nconst V2_OPT_OUT_NAMES = ['links', 'sections', 'rank'] as const;\n\n// Pure per-version steps, chained by loadConfig. Intermediate shapes predate the Config type,\n// so steps are loosely typed and only the final result is cast back.\nconst MIGRATIONS: Record<number, (cfg: Record<string, unknown>) => Record<string, unknown>> = {\n // v1 -> v2: features block introduced, opt-out features enabled (matches the old implicit\n // behavior of `links` etc. not existing). embed stays absent -- opt-in.\n 1: (cfg) => ({ ...cfg, version: 2, features: Object.fromEntries(V2_OPT_OUT_NAMES.map((name) => [name, true])) }),\n // v2 -> v3: mechanical-minimal by decision -- only what keeps an existing config loading (no\n // restructuring, no preset inference, no query rewriting); real trees get hand-migrated to use presets.\n 2: (cfg) => {\n const { scan, defaults, features, checks, queries, ...rest } = cfg;\n const scanInclude = (scan as { include: string[] }).include;\n\n const nextQueries: Record<string, unknown> = {};\n for (const [name, value] of Object.entries((queries as Record<string, unknown> | undefined) ?? {})) {\n if (typeof value === 'string') {\n nextQueries[name] = value;\n continue;\n }\n const entry = value as { find: string; k?: number; where?: string; semantic?: boolean };\n // Loosely typed on purpose: this step emits the v3 shape, which still had `semantic`.\n // The v3 -> v4 step below strips it.\n const search: Record<string, unknown> = { search: entry.find };\n if (entry.k !== undefined) search.k = entry.k;\n if (entry.where !== undefined) search.where = entry.where;\n // semantic: true was opt-in; it is now the default, so it drops. semantic: false is kept.\n if (entry.semantic === false) search.semantic = false;\n nextQueries[name] = search;\n }\n\n const prevDefaults = defaults as { find?: { where?: string } } | undefined;\n const defaultWhere = prevDefaults?.find?.where;\n\n // features.embed false becomes semantic:false on every preset produced here.\n const prevFeatures = (features as Record<string, unknown> | undefined) ?? {};\n const embedWasOff = prevFeatures.embed === false;\n // Object form carries provider settings (model/type/url/key), not a toggle -- they move to\n // the v3 top-level `embed` block verbatim; dropping them would silently switch an api tree to the static model.\n const embedProvider = typeof prevFeatures.embed === 'object' && prevFeatures.embed !== null ? prevFeatures.embed : undefined;\n const { embed: _embed, ...restFeatures } = prevFeatures;\n\n const defaultPreset: Record<string, unknown> = { include: scanInclude };\n if (defaultWhere !== undefined) defaultPreset.where = defaultWhere;\n if (embedWasOff) defaultPreset.semantic = false;\n\n if (checks !== undefined) {\n console.error('sense: v2 \"checks\" was removed in v3 (sense check no longer asserts on saved queries); its queries are carried over under \"queries\" -- a returned row set is now the reader\\'s judgment');\n }\n\n const result: Record<string, unknown> = { ...rest, version: 3, presets: { default: defaultPreset }, queries: nextQueries };\n if (embedProvider !== undefined) result.embed = embedProvider;\n if (Object.keys(restFeatures).length > 0) result.features = restFeatures;\n return result;\n },\n // v3 -> v4: write the model into the file, wrap bare-string queries as { sql }, drop a saved\n // search's `semantic` (its preset decides). Same effective model, so nothing re-embeds.\n 3: (cfg) => {\n const presets = cfg.presets as Record<string, Record<string, unknown>>;\n const embedded = Object.values(presets).some((p) => p.semantic !== false);\n\n const nextQueries: Record<string, unknown> = {};\n for (const [name, value] of Object.entries((cfg.queries as Record<string, unknown> | undefined) ?? {})) {\n // v3's bare string meant SQL by inference; v4 makes every entry name its verb.\n if (typeof value === 'string') {\n nextQueries[name] = { sql: value };\n continue;\n }\n if (typeof value !== 'object' || value === null || !('search' in value)) {\n nextQueries[name] = value;\n continue;\n }\n const { semantic: _semantic, ...rest } = value as Record<string, unknown>;\n nextQueries[name] = rest;\n }\n\n const prev = (cfg.embed as Record<string, unknown> | undefined) ?? {};\n const result: Record<string, unknown> = { ...cfg, version: 4, queries: nextQueries };\n if (embedded) {\n result.embed = { model: (prev.model as string | undefined) ?? DEFAULT_EMBED_MODEL, type: (prev.type as string | undefined) ?? 'static', ...(prev.url !== undefined ? { url: prev.url } : {}), ...(prev.key !== undefined ? { key: prev.key } : {}) };\n } else {\n delete result.embed;\n // Dropping settings silently would look like a bug in a tree that configured an api endpoint\n // then turned vectors off; say it, since v4 has no way to carry provider settings for a tree that doesn't embed.\n if (Object.keys(prev).length > 0) console.error('sense: every preset has \"semantic\": false, so v4 drops the \"embed\" block (vectors stay off); re-add it, or set a preset\\'s semantic back to true, to turn them on');\n }\n return result;\n },\n // v4 -> v5: embed.type renames to embed.provider (\"api\" migrates to \"openai\", the wire protocol\n // it always was; other values pass through). A preset's `semantic` also migrates to `signals` (false -> explicit weights, true/absent -> no key).\n 4: (cfg) => {\n const prevEmbed = cfg.embed as Record<string, unknown> | undefined;\n const result: Record<string, unknown> = { ...cfg, version: 5 };\n if (prevEmbed) {\n const { type, ...rest } = prevEmbed;\n result.embed = type === undefined ? rest : { ...rest, provider: type === 'api' ? 'openai' : type };\n }\n\n const linksOn = (cfg.features as Record<string, unknown> | undefined)?.links !== false;\n const presets = cfg.presets as Record<string, Record<string, unknown>> | undefined;\n if (presets) {\n const nextPresets: Record<string, unknown> = {};\n for (const [name, preset] of Object.entries(presets)) {\n const { semantic, ...rest } = preset;\n if (semantic === undefined) {\n nextPresets[name] = preset;\n continue;\n }\n if (typeof semantic !== 'boolean') {\n throw new SenseError('CONFIG_INVALID', `presets.${name}.semantic must be a boolean`);\n }\n nextPresets[name] = semantic === false ? { ...rest, signals: linksOn ? { words: 1, links: 1 } : { words: 1 } } : rest;\n }\n result.presets = nextPresets;\n }\n return result;\n },\n};\n\nexport function migrateConfig(cfg: Config): { cfg: Config; from: number } {\n const from = cfg.version ?? 1;\n let current: Record<string, unknown> = cfg as unknown as Record<string, unknown>;\n for (let v = from; v < SUPPORTED_CONFIG_VERSION; v++) {\n const step = MIGRATIONS[v];\n if (!step) throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `no migration from config version ${v}`);\n current = step(current);\n }\n return { cfg: current as unknown as Config, from };\n}\n\n// `sense init`'s flags map onto these three `embed` keys one-to-one; an absent field\n// keeps the built-in default rather than merging partial input.\nexport interface InitOverrides {\n model?: string;\n provider?: string;\n url?: string;\n}\n\nfunction starterConfig(overrides?: InitOverrides): Config {\n const embed: Record<string, unknown> = { model: DEFAULT_EMBED_MODEL, provider: 'static' };\n if (overrides?.model !== undefined) embed.model = overrides.model;\n if (overrides?.provider !== undefined) embed.provider = overrides.provider;\n if (overrides?.url !== undefined) embed.url = overrides.url;\n return {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: SUPPORTED_CONFIG_VERSION,\n presets: {\n default: { include: ['**/*.md'], k: 10 },\n large: { include: ['**/*.md'], k: 20 },\n },\n // Written out rather than defaulted in code: this line is what makes `sense download`\n // fetch a model, so it belongs where it can be read and changed.\n embed: embed as Config['embed'],\n queries: {},\n };\n}\n\n// Refuses to overwrite an existing config. `overrides` come from `sense init`'s flags; an\n// invalid --provider fails validateConfig's own shape check before anything is written.\nexport function initConfig(dir: string, overrides?: InitOverrides): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n const cfg = starterConfig(overrides);\n validateConfig(cfg, configPath);\n writeFileAtomic(configPath, `${JSON.stringify(cfg, null, 2)}\\n`);\n return configPath;\n}\n\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nexport function loadConfig(explicitPath?: string): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n\n // Ahead of both the version gate and shape checks: a non-object file (or array -- typeof []\n // is also 'object') is malformed at every version, so it gets one error regardless of branch.\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);\n }\n\n // Version gate before shape validation: a config written by a newer sense should fail\n // with \"requires a newer sense\", not with shape errors its own version may not have.\n const version = typeof parsed === 'object' && parsed !== null && typeof (parsed as { version?: unknown }).version === 'number' ? (parsed as { version: number }).version : 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n // validateConfig only knows the current shape, so scan-shaped files (pre-v3) get a minimal\n // check, migrate, then the full one on the result.\n let cfg: Config;\n let migratedFrom: number | undefined;\n if (version < SUPPORTED_CONFIG_VERSION) {\n if (version < FIRST_PRESET_VERSION) validateLegacyScan(parsed, configPath);\n const result = migrateConfig(parsed as unknown as Config);\n cfg = validateConfig(result.cfg, configPath);\n migratedFrom = result.from;\n writeFileAtomic(configPath, `${JSON.stringify(cfg, null, 2)}\\n`);\n } else {\n cfg = validateConfig(parsed, configPath);\n }\n\n const unknownKeys = unknownConfigKeys(cfg as unknown as Record<string, unknown>);\n const configDir = dirname(configPath);\n const rootDir = resolve(configDir, cfg.root ?? '.');\n return { ...cfg, configDir, rootDir, baseDir: rootDir, configPath, migratedFrom, unknownKeys: unknownKeys.length > 0 ? unknownKeys : undefined };\n}\n"],"names":["existsSync","readFileSync","dirname","join","resolve","SenseError","writeFileAtomic","CONFIG_FILENAME","DEFAULT_EMBED_MODEL","SUPPORTED_CONFIG_VERSION","unknownConfigKeys","validateConfig","validateLegacyScan","FIRST_PRESET_VERSION","V2_OPT_OUT_NAMES","MIGRATIONS","cfg","version","features","Object","fromEntries","map","name","prevDefaults","scan","defaults","checks","queries","rest","scanInclude","include","nextQueries","value","entries","entry","search","find","k","undefined","where","semantic","defaultWhere","prevFeatures","embedWasOff","embed","embedProvider","_embed","restFeatures","defaultPreset","console","error","result","presets","default","keys","length","embedded","values","some","p","sql","_semantic","prev","model","type","url","key","prevEmbed","provider","linksOn","links","nextPresets","preset","signals","words","migrateConfig","from","current","v","step","starterConfig","overrides","$schema","large","initConfig","dir","configPath","JSON","stringify","findConfigPath","startDir","candidate","parent","loadConfig","explicitPath","process","cwd","found","raw","parsed","parse","Array","isArray","migratedFrom","unknownKeys","configDir","rootDir","root","baseDir"],"mappings":"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"}
@@ -36,6 +36,7 @@ export type SavedQuery = {
36
36
  export interface Config {
37
37
  $schema?: string;
38
38
  version?: number;
39
+ root?: string;
39
40
  presets: Record<string, Preset>;
40
41
  features?: {
41
42
  links?: boolean;
@@ -48,6 +49,8 @@ export interface Config {
48
49
  queries: Record<string, SavedQuery>;
49
50
  }
50
51
  export interface ResolvedConfig extends Config {
52
+ configDir?: string;
53
+ rootDir?: string;
51
54
  baseDir: string;
52
55
  configPath: string | null;
53
56
  unknownKeys?: string[];
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/types.ts"],"sourcesContent":["import type { SignalWeights } from './signals.ts';\n\nexport const CONFIG_FILENAME = 'sense.config.json';\nexport const STATE_DIR = '.sense';\n\n// Highest sense.config.json `version` this build understands. Older versions auto-migrate on load.\nexport const SUPPORTED_CONFIG_VERSION = 5;\n\n// Each feature owns its tables, parse-time extraction, and reconcile step; commands degrade when one is off.\n// links/sections/tags/rank are opt-out toggles in `features`; embed is on exactly when an `embed` block names a model.\nexport const FEATURE_NAMES = ['links', 'sections', 'tags', 'rank', 'embed'] as const;\nexport type FeatureName = (typeof FEATURE_NAMES)[number];\n\n// `embed` names the model and gives the tree vectors at all; a preset's `signals` says which\n// engines that scope's searches use.\nexport interface EmbedConfig {\n model?: string;\n provider?: 'static' | 'openai' | 'cohere';\n url?: string; // openai provider: OpenAI-compatible base URL, e.g. http://localhost:11434/v1\n key?: string; // openai/cohere provider: name of the env var holding the bearer token, if any\n languages?: string[]; // owner-declared model languages; enables the language-fit check where no card exists\n chunkTokens?: number; // chunk size ceiling in estimated tokens for small-context models; default 500\n}\n\nexport const DEFAULT_EMBED_MODEL = 'minishlab/potion-retrieval-32M';\n\n// The single declaration every other store name derives from (Store.name, the store/index.ts\n// registry, validate.ts's accepted set). An array, not a bare union, so runtime checks read the same list.\nexport const STORE_NAMES = ['sqlite', 'duckdb', 'turso'] as const;\nexport type StoreName = (typeof STORE_NAMES)[number];\n\n// A named, self-contained file-selection scope: include/exclude are globby patterns resolved\n// relative to the config file. No inheritance between presets; they may overlap freely (views, not partitions).\nexport interface Preset {\n include: string[];\n exclude?: string[];\n k?: number; // result count for `search` scoped to this preset; default 10\n // Exhaustive when present: every signal this preset's searches compose, each mapped to its RRF\n // weight (finite > 0; presence is enablement). Absent = every signal whose prerequisite holds, at weight 1 (words always, links when on, vectors when a model is named).\n signals?: SignalWeights;\n where?: string; // standing SQL condition against frontmatter alias `f`\n}\n\n// A saved `search` invocation: `sense <name>` runs like\n// `sense search <search> [--preset] [--include] [--exclude] [--where] [--k]`.\nexport interface SavedSearch {\n search: string;\n preset?: string;\n include?: string[];\n exclude?: string[];\n where?: string;\n k?: number;\n}\n\n// An entry names the verb it runs, one to one with the commands: { sql } runs like\n// `sense sql`, { search } like `sense search`.\nexport type SavedQuery = { sql: string } | SavedSearch;\n\nexport interface Config {\n // Editor-only pointer to schema.json; never read by sense.\n $schema?: string;\n version?: number;\n // File selection, index-time, and per-preset search defaults. A file is indexed iff any\n // preset's include/exclude covers it (union). `default` is used when a command names no preset.\n presets: Record<string, Preset>;\n // Global feature defaults; absent block or key means enabled. embed is not a member here --\n // see the `embed` block.\n features?: { links?: boolean; sections?: boolean; tags?: boolean; rank?: boolean };\n // Names the embedding model. Present means every indexed file gets vectors; absent means none do.\n embed?: EmbedConfig;\n // Backing store engine: \"sqlite\" (default, zero-dependency), \"duckdb\", or \"turso\" (optional\n // dependency, installed on first use). What differs per store is declared through CAPABILITIES (store/types.ts).\n store?: StoreName;\n queries: Record<string, SavedQuery>;\n}\n\nexport interface ResolvedConfig extends Config {\n baseDir: string;\n configPath: string | null;\n // Keys the file declares that this build does not read; cli reports them.\n unknownKeys?: string[];\n // Set when loadConfig auto-migrated the file on disk; cli reports it.\n migratedFrom?: number;\n}\n\nexport interface SearchOverrides {\n preset?: string;\n k?: number;\n where?: string;\n include?: string[];\n exclude?: string[];\n noExclude?: boolean; // drop the preset's exclude for this command; --exclude still applies\n}\n\nexport interface EffectiveSearch {\n presetName: string;\n k: number;\n where?: string;\n include: string[];\n exclude?: string[];\n signals: SignalWeights; // this preset's effective signal weights\n}\n"],"names":["CONFIG_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","FEATURE_NAMES","DEFAULT_EMBED_MODEL","STORE_NAMES"],"mappings":"AAEA,OAAO,MAAMA,kBAAkB,oBAAoB;AACnD,OAAO,MAAMC,YAAY,SAAS;AAElC,mGAAmG;AACnG,OAAO,MAAMC,2BAA2B,EAAE;AAE1C,6GAA6G;AAC7G,uHAAuH;AACvH,OAAO,MAAMC,gBAAgB;IAAC;IAAS;IAAY;IAAQ;IAAQ;CAAQ,CAAU;AAcrF,OAAO,MAAMC,sBAAsB,iCAAiC;AAEpE,6FAA6F;AAC7F,2GAA2G;AAC3G,OAAO,MAAMC,cAAc;IAAC;IAAU;IAAU;CAAQ,CAAU"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/types.ts"],"sourcesContent":["import type { SignalWeights } from './signals.ts';\n\nexport const CONFIG_FILENAME = 'sense.config.json';\nexport const STATE_DIR = '.sense';\n\n// Highest sense.config.json `version` this build understands. Older versions auto-migrate on load.\nexport const SUPPORTED_CONFIG_VERSION = 5;\n\n// Each feature owns its tables, parse-time extraction, and reconcile step; commands degrade when one is off.\n// links/sections/tags/rank are opt-out toggles in `features`; embed is on exactly when an `embed` block names a model.\nexport const FEATURE_NAMES = ['links', 'sections', 'tags', 'rank', 'embed'] as const;\nexport type FeatureName = (typeof FEATURE_NAMES)[number];\n\n// `embed` names the model and gives the tree vectors at all; a preset's `signals` says which\n// engines that scope's searches use.\nexport interface EmbedConfig {\n model?: string;\n provider?: 'static' | 'openai' | 'cohere';\n url?: string; // openai provider: OpenAI-compatible base URL, e.g. http://localhost:11434/v1\n key?: string; // openai/cohere provider: name of the env var holding the bearer token, if any\n languages?: string[]; // owner-declared model languages; enables the language-fit check where no card exists\n chunkTokens?: number; // chunk size ceiling in estimated tokens for small-context models; default 500\n}\n\nexport const DEFAULT_EMBED_MODEL = 'minishlab/potion-retrieval-32M';\n\n// The single declaration every other store name derives from (Store.name, the store/index.ts\n// registry, validate.ts's accepted set). An array, not a bare union, so runtime checks read the same list.\nexport const STORE_NAMES = ['sqlite', 'duckdb', 'turso'] as const;\nexport type StoreName = (typeof STORE_NAMES)[number];\n\n// A named, self-contained file-selection scope: include/exclude are globby patterns resolved\n// relative to the config file. No inheritance between presets; they may overlap freely (views, not partitions).\nexport interface Preset {\n include: string[];\n exclude?: string[];\n k?: number; // result count for `search` scoped to this preset; default 10\n // Exhaustive when present: every signal this preset's searches compose, each mapped to its RRF\n // weight (finite > 0; presence is enablement). Absent = every signal whose prerequisite holds, at weight 1 (words always, links when on, vectors when a model is named).\n signals?: SignalWeights;\n where?: string; // standing SQL condition against frontmatter alias `f`\n}\n\n// A saved `search` invocation: `sense <name>` runs like\n// `sense search <search> [--preset] [--include] [--exclude] [--where] [--k]`.\nexport interface SavedSearch {\n search: string;\n preset?: string;\n include?: string[];\n exclude?: string[];\n where?: string;\n k?: number;\n}\n\n// An entry names the verb it runs, one to one with the commands: { sql } runs like\n// `sense sql`, { search } like `sense search`.\nexport type SavedQuery = { sql: string } | SavedSearch;\n\nexport interface Config {\n // Editor-only pointer to schema.json; never read by sense.\n $schema?: string;\n version?: number;\n // Markdown tree to index. Relative paths resolve from the configuration file's directory;\n // omitted keeps the historic colocated-config behavior.\n root?: string;\n // File selection, index-time, and per-preset search defaults. A file is indexed iff any\n // preset's include/exclude covers it (union). `default` is used when a command names no preset.\n presets: Record<string, Preset>;\n // Global feature defaults; absent block or key means enabled. embed is not a member here --\n // see the `embed` block.\n features?: { links?: boolean; sections?: boolean; tags?: boolean; rank?: boolean };\n // Names the embedding model. Present means every indexed file gets vectors; absent means none do.\n embed?: EmbedConfig;\n // Backing store engine: \"sqlite\" (default, zero-dependency), \"duckdb\", or \"turso\" (optional\n // dependency, installed on first use). What differs per store is declared through CAPABILITIES (store/types.ts).\n store?: StoreName;\n queries: Record<string, SavedQuery>;\n}\n\nexport interface ResolvedConfig extends Config {\n // Configuration and state ownership are deliberately separate from the indexed tree.\n configDir?: string;\n rootDir?: string;\n // Compatibility for callers that construct a ResolvedConfig for `open()` directly. New code\n // uses rootDir/configDir; openStore normalizes this alias to both when necessary.\n baseDir: string;\n configPath: string | null;\n // Keys the file declares that this build does not read; cli reports them.\n unknownKeys?: string[];\n // Set when loadConfig auto-migrated the file on disk; cli reports it.\n migratedFrom?: number;\n}\n\nexport interface SearchOverrides {\n preset?: string;\n k?: number;\n where?: string;\n include?: string[];\n exclude?: string[];\n noExclude?: boolean; // drop the preset's exclude for this command; --exclude still applies\n}\n\nexport interface EffectiveSearch {\n presetName: string;\n k: number;\n where?: string;\n include: string[];\n exclude?: string[];\n signals: SignalWeights; // this preset's effective signal weights\n}\n"],"names":["CONFIG_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","FEATURE_NAMES","DEFAULT_EMBED_MODEL","STORE_NAMES"],"mappings":"AAEA,OAAO,MAAMA,kBAAkB,oBAAoB;AACnD,OAAO,MAAMC,YAAY,SAAS;AAElC,mGAAmG;AACnG,OAAO,MAAMC,2BAA2B,EAAE;AAE1C,6GAA6G;AAC7G,uHAAuH;AACvH,OAAO,MAAMC,gBAAgB;IAAC;IAAS;IAAY;IAAQ;IAAQ;CAAQ,CAAU;AAcrF,OAAO,MAAMC,sBAAsB,iCAAiC;AAEpE,6FAA6F;AAC7F,2GAA2G;AAC3G,OAAO,MAAMC,cAAc;IAAC;IAAU;IAAU;CAAQ,CAAU"}
@@ -6,6 +6,7 @@ import { STORE_NAMES } from './types.js';
6
6
  const KNOWN_KEYS = new Set([
7
7
  '$schema',
8
8
  'version',
9
+ 'root',
9
10
  'presets',
10
11
  'features',
11
12
  'embed',
@@ -222,6 +223,9 @@ export function validateConfig(parsed, configPath) {
222
223
  throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);
223
224
  }
224
225
  const cfg = parsed;
226
+ if (cfg.root !== undefined && (typeof cfg.root !== 'string' || cfg.root.length === 0)) {
227
+ throw new SenseError('CONFIG_INVALID', `${configPath}: root must be a non-empty path string`);
228
+ }
225
229
  // `checks` is rejected by name, not warned: silence would hide that assertions are gone.
226
230
  if (cfg.checks !== undefined) {
227
231
  throw new SenseError('CONFIG_INVALID', `${configPath}: checks was removed in v3 -- sense check no longer asserts on saved queries; a returned row set is the reader's judgment`);
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/validate.ts"],"sourcesContent":["import { SenseError } from '../errors.ts';\nimport { SIGNAL_NAMES, SIGNAL_PREREQUISITES, type SignalName } from './signals.ts';\nimport { type Config, STORE_NAMES, type StoreName } from './types.ts';\n\n// Shape check for hand-edited files, so a typo names itself instead of surfacing as a\n// TypeError. Unknown top-level keys warn (forward compat); unknown keys inside a block error.\nconst KNOWN_KEYS = new Set(['$schema', 'version', 'presets', 'features', 'embed', 'store', 'queries']);\nconst KNOWN_PRESET_KEYS = new Set(['include', 'exclude', 'k', 'signals', 'where']);\nconst KNOWN_FEATURE_KEYS = new Set(['links', 'sections', 'tags', 'rank']);\n// The one source of truth for what the embed block accepts. schema.json carries an editor-facing\n// copy for autocomplete; nothing at runtime reads it.\nexport const KNOWN_EMBED_KEYS = new Set(['model', 'provider', 'url', 'key', 'languages', 'chunkTokens']);\nconst SAVED_SEARCH_KEYS = new Set(['search', 'preset', 'include', 'exclude', 'where', 'k']);\n\nexport function unknownConfigKeys(cfg: Record<string, unknown>): string[] {\n return Object.keys(cfg).filter((k) => !KNOWN_KEYS.has(k));\n}\n\nfunction isNonEmptyStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.length > 0 && value.every((g) => typeof g === 'string' && g.length > 0);\n}\n\n// Pre-v3 shape check, just enough for migrateConfig to run safely (it reads scan.include\n// directly). The full v3 shape is checked by validateConfig once migration has produced it.\nexport function validateLegacyScan(parsed: unknown, configPath: string): void {\n const cfg = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : null;\n const scan = cfg?.scan as { include?: unknown } | undefined;\n if (!cfg || !scan || !isNonEmptyStringArray(scan.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: scan.include must be a non-empty array of glob strings`);\n }\n}\n\nfunction validateStoreKey(value: unknown, configPath: string): void {\n if (!STORE_NAMES.includes(value as StoreName)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: store must be ${STORE_NAMES.map((name) => `\"${name}\"`).join(' or ')}`);\n }\n}\n\nfunction validateFeaturesBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features must be an object of name -> boolean`);\n }\n const block = value as Record<string, unknown>;\n const unknown = Object.keys(block).filter((k) => !KNOWN_FEATURE_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features has unknown key(s) ${unknown.join(', ')}; embed is not a features key -- see the top-level \"embed\" block`);\n }\n for (const [name, v] of Object.entries(block)) {\n if (typeof v !== 'boolean') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features.${name} must be a boolean`);\n }\n }\n}\n\nfunction validateEmbedBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed must be an object of { model?, provider?: \"static\"|\"openai\"|\"cohere\", url?, key? }`);\n }\n const embed = value as Record<string, unknown>;\n const unknown = Object.keys(embed).filter((k) => !KNOWN_EMBED_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed has unknown key(s) ${unknown.join(', ')}`);\n }\n if (embed.model !== undefined && typeof embed.model !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.model must be a string`);\n }\n // The block exists to name a model; provider settings without one would read as \"vectors\n // configured\" while embedding stays off, which is the ambiguity v4 removed.\n if (embed.model === undefined || embed.model === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.model is required when the \"embed\" block is present (it is what turns vectors on); remove the block to index without vectors`);\n }\n if (embed.provider !== undefined && embed.provider !== 'static' && embed.provider !== 'openai' && embed.provider !== 'cohere') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.provider must be \"static\", \"openai\", or \"cohere\"`);\n }\n if (embed.url !== undefined && typeof embed.url !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.url must be a string`);\n }\n if (embed.key !== undefined && typeof embed.key !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.key must be a string`);\n }\n // openai has no default host, so a missing url would otherwise only surface at the first\n // vector search; a config error belongs at config time.\n if (embed.provider === 'openai' && (embed.url === undefined || embed.url === '')) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.provider \"openai\" requires embed.url (e.g. \"http://localhost:11434/v1\" for Ollama)`);\n }\n if (embed.languages !== undefined && (!Array.isArray(embed.languages) || embed.languages.length === 0 || embed.languages.some((l) => typeof l !== 'string' || l === ''))) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.languages must be a non-empty array of language tags (e.g. [\"en\", \"zh\"])`);\n }\n if (embed.chunkTokens !== undefined && (typeof embed.chunkTokens !== 'number' || !Number.isInteger(embed.chunkTokens) || embed.chunkTokens <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.chunkTokens must be a positive integer`);\n }\n}\n\nfunction isSignalName(value: unknown): value is SignalName {\n return typeof value === 'string' && (SIGNAL_NAMES as readonly string[]).includes(value);\n}\n\nfunction isSignalWeight(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0;\n}\n\n// Shape (an object of signal name -> weight) plus the one cross-signal prerequisite that doesn't\n// need the top-level embed block: links is seeded by word-match rows, so declaring it without words is always empty.\nfunction validatePresetSignals(name: string, value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value) || Object.keys(value).length === 0) {\n throw new SenseError(\n 'CONFIG_INVALID',\n `${configPath}: presets.${name}.signals must be a non-empty object of signal name -> weight (a finite number > 0), naming ${SIGNAL_NAMES.join(', ')} -- a preset that searches nothing is a mistake; name at least \"words\", or omit \"signals\" to use every signal whose prerequisites hold at weight 1`\n );\n }\n const entries = Object.entries(value as Record<string, unknown>);\n const unknown = entries.filter(([k]) => !isSignalName(k)).map(([k]) => k);\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals names unknown signal(s) ${unknown.join(', ')}; valid signals are ${SIGNAL_NAMES.join(', ')}`);\n }\n for (const [signal, weight] of entries) {\n if (!isSignalWeight(weight)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals.${signal} must be a finite number > 0`);\n }\n }\n const names = new Set(entries.map(([k]) => k));\n for (const [signal, requires] of Object.entries(SIGNAL_PREREQUISITES) as Array<[SignalName, SignalName]>) {\n if (names.has(signal) && !names.has(requires)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals has \"${signal}\" without \"${requires}\"; add \"${requires}\" to presets.${name}.signals, or drop \"${signal}\"`);\n }\n }\n}\n\nfunction validatePreset(name: string, value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} must be an object`);\n }\n const preset = value as Record<string, unknown>;\n const unknown = Object.keys(preset).filter((k) => !KNOWN_PRESET_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} has unknown key(s) ${unknown.join(', ')}; a preset takes include, exclude, k, signals, where`);\n }\n if (!isNonEmptyStringArray(preset.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.include must be a non-empty array of glob strings`);\n }\n if (preset.exclude !== undefined && !isNonEmptyStringArray(preset.exclude)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.exclude must be a non-empty array of glob strings`);\n }\n if (preset.signals !== undefined) validatePresetSignals(name, preset.signals, configPath);\n if (preset.k !== undefined && (typeof preset.k !== 'number' || !Number.isInteger(preset.k) || preset.k <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.k must be a positive integer`);\n }\n if (preset.where !== undefined && typeof preset.where !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.where must be a SQL condition string`);\n }\n}\n\n// A queries.<name> entry: { sql } or a saved search { search, preset?, include?, exclude?, where?, k? }.\nfunction validateSavedQuery(name: string, value: unknown, configPath: string): void {\n // A bare string is rejected rather than inferred to be SQL: an entry says which of the two\n // verbs it runs, the same choice the CLI makes explicit.\n if (typeof value === 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must say which verb it runs: { \"sql\": ${JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value)} } to run it as SQL, or { \"search\": \"...\" } for a ranked search`);\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be { sql } or { search, preset?, include?, exclude?, where?, k? }`);\n }\n const entry = value as Record<string, unknown>;\n if ('sql' in entry) {\n const unknown = Object.keys(entry).filter((k) => k !== 'sql');\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a { sql } query takes only sql`);\n }\n if (typeof entry.sql !== 'string' || entry.sql.trim() === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.sql must be a non-empty string`);\n }\n return;\n }\n if ('search' in entry) {\n const unknown = Object.keys(entry).filter((k) => !SAVED_SEARCH_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a saved search takes search, preset, include, exclude, where, k`);\n }\n // A saved entry saves a question; a scope without a question is just flags.\n if (typeof entry.search !== 'string' || entry.search.trim() === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.search must be non-empty text`);\n }\n if (entry.preset !== undefined && (typeof entry.preset !== 'string' || entry.preset.length === 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.preset must be a preset name`);\n }\n if (entry.include !== undefined && !isNonEmptyStringArray(entry.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.include must be a non-empty array of glob strings`);\n }\n if (entry.exclude !== undefined && !isNonEmptyStringArray(entry.exclude)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.exclude must be a non-empty array of glob strings`);\n }\n if (entry.where !== undefined && typeof entry.where !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.where must be a SQL condition string`);\n }\n if (entry.k !== undefined && (typeof entry.k !== 'number' || !Number.isInteger(entry.k) || entry.k <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.k must be a positive integer`);\n }\n return;\n }\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be { sql } or { search, preset?, include?, exclude?, where?, k? }`);\n}\n\nexport function validateConfig(parsed: unknown, configPath: string): Config {\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);\n }\n const cfg = parsed as Record<string, unknown>;\n\n // `checks` is rejected by name, not warned: silence would hide that assertions are gone.\n if (cfg.checks !== undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: checks was removed in v3 -- sense check no longer asserts on saved queries; a returned row set is the reader's judgment`);\n }\n\n // `content` (content.tokenize) is rejected by name for the same reason: every outcome it\n // selected (unspaced-script search, stemming, quoted-phrase substring matching) is now\n // automatic on every store, so a config that still sets it would silently get nothing back.\n if (cfg.content !== undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: content.tokenize was removed -- every outcome it selected is now automatic on every store; remove the \"content\" block`);\n }\n\n const presets = cfg.presets as Record<string, unknown> | undefined;\n if (!presets || typeof presets !== 'object' || Array.isArray(presets) || Object.keys(presets).length === 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets must be a non-empty object of preset name -> { include, exclude?, k?, signals?, where? }`);\n }\n for (const [name, value] of Object.entries(presets)) {\n validatePreset(name, value, configPath);\n }\n if (presets.default === undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets must include a \"default\" preset`);\n }\n\n if (cfg.queries === undefined) cfg.queries = {};\n if (typeof cfg.queries !== 'object' || cfg.queries === null || Array.isArray(cfg.queries)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries must be an object of name -> { sql } or { search }`);\n }\n const queries = cfg.queries as Record<string, unknown>;\n for (const [name, value] of Object.entries(queries)) {\n validateSavedQuery(name, value, configPath);\n }\n\n if (cfg.features !== undefined) {\n validateFeaturesBlock(cfg.features, configPath);\n }\n if (cfg.embed !== undefined) {\n validateEmbedBlock(cfg.embed, configPath);\n }\n if (cfg.store !== undefined) {\n validateStoreKey(cfg.store, configPath);\n }\n\n // vectors' prerequisite is a named model, checked here once embed is known valid (or absent).\n if (cfg.embed === undefined) {\n for (const [name, value] of Object.entries(presets)) {\n const signals = (value as Record<string, unknown>).signals as Record<string, unknown> | undefined;\n if (signals && 'vectors' in signals) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals includes \"vectors\", but no \"embed\" block names a model; add one, or drop \"vectors\" from presets.${name}.signals`);\n }\n }\n }\n\n return cfg as unknown as Config;\n}\n"],"names":["SenseError","SIGNAL_NAMES","SIGNAL_PREREQUISITES","STORE_NAMES","KNOWN_KEYS","Set","KNOWN_PRESET_KEYS","KNOWN_FEATURE_KEYS","KNOWN_EMBED_KEYS","SAVED_SEARCH_KEYS","unknownConfigKeys","cfg","Object","keys","filter","k","has","isNonEmptyStringArray","value","Array","isArray","length","every","g","validateLegacyScan","parsed","configPath","scan","include","validateStoreKey","includes","map","name","join","validateFeaturesBlock","block","unknown","v","entries","validateEmbedBlock","embed","model","undefined","provider","url","key","languages","some","l","chunkTokens","Number","isInteger","isSignalName","isSignalWeight","isFinite","validatePresetSignals","signal","weight","names","requires","validatePreset","preset","exclude","signals","where","validateSavedQuery","JSON","stringify","slice","entry","sql","trim","search","validateConfig","checks","content","presets","default","queries","features","store"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/validate.ts"],"sourcesContent":["import { SenseError } from '../errors.ts';\nimport { SIGNAL_NAMES, SIGNAL_PREREQUISITES, type SignalName } from './signals.ts';\nimport { type Config, STORE_NAMES, type StoreName } from './types.ts';\n\n// Shape check for hand-edited files, so a typo names itself instead of surfacing as a\n// TypeError. Unknown top-level keys warn (forward compat); unknown keys inside a block error.\nconst KNOWN_KEYS = new Set(['$schema', 'version', 'root', 'presets', 'features', 'embed', 'store', 'queries']);\nconst KNOWN_PRESET_KEYS = new Set(['include', 'exclude', 'k', 'signals', 'where']);\nconst KNOWN_FEATURE_KEYS = new Set(['links', 'sections', 'tags', 'rank']);\n// The one source of truth for what the embed block accepts. schema.json carries an editor-facing\n// copy for autocomplete; nothing at runtime reads it.\nexport const KNOWN_EMBED_KEYS = new Set(['model', 'provider', 'url', 'key', 'languages', 'chunkTokens']);\nconst SAVED_SEARCH_KEYS = new Set(['search', 'preset', 'include', 'exclude', 'where', 'k']);\n\nexport function unknownConfigKeys(cfg: Record<string, unknown>): string[] {\n return Object.keys(cfg).filter((k) => !KNOWN_KEYS.has(k));\n}\n\nfunction isNonEmptyStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.length > 0 && value.every((g) => typeof g === 'string' && g.length > 0);\n}\n\n// Pre-v3 shape check, just enough for migrateConfig to run safely (it reads scan.include\n// directly). The full v3 shape is checked by validateConfig once migration has produced it.\nexport function validateLegacyScan(parsed: unknown, configPath: string): void {\n const cfg = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : null;\n const scan = cfg?.scan as { include?: unknown } | undefined;\n if (!cfg || !scan || !isNonEmptyStringArray(scan.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: scan.include must be a non-empty array of glob strings`);\n }\n}\n\nfunction validateStoreKey(value: unknown, configPath: string): void {\n if (!STORE_NAMES.includes(value as StoreName)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: store must be ${STORE_NAMES.map((name) => `\"${name}\"`).join(' or ')}`);\n }\n}\n\nfunction validateFeaturesBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features must be an object of name -> boolean`);\n }\n const block = value as Record<string, unknown>;\n const unknown = Object.keys(block).filter((k) => !KNOWN_FEATURE_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features has unknown key(s) ${unknown.join(', ')}; embed is not a features key -- see the top-level \"embed\" block`);\n }\n for (const [name, v] of Object.entries(block)) {\n if (typeof v !== 'boolean') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: features.${name} must be a boolean`);\n }\n }\n}\n\nfunction validateEmbedBlock(value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed must be an object of { model?, provider?: \"static\"|\"openai\"|\"cohere\", url?, key? }`);\n }\n const embed = value as Record<string, unknown>;\n const unknown = Object.keys(embed).filter((k) => !KNOWN_EMBED_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed has unknown key(s) ${unknown.join(', ')}`);\n }\n if (embed.model !== undefined && typeof embed.model !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.model must be a string`);\n }\n // The block exists to name a model; provider settings without one would read as \"vectors\n // configured\" while embedding stays off, which is the ambiguity v4 removed.\n if (embed.model === undefined || embed.model === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.model is required when the \"embed\" block is present (it is what turns vectors on); remove the block to index without vectors`);\n }\n if (embed.provider !== undefined && embed.provider !== 'static' && embed.provider !== 'openai' && embed.provider !== 'cohere') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.provider must be \"static\", \"openai\", or \"cohere\"`);\n }\n if (embed.url !== undefined && typeof embed.url !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.url must be a string`);\n }\n if (embed.key !== undefined && typeof embed.key !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.key must be a string`);\n }\n // openai has no default host, so a missing url would otherwise only surface at the first\n // vector search; a config error belongs at config time.\n if (embed.provider === 'openai' && (embed.url === undefined || embed.url === '')) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.provider \"openai\" requires embed.url (e.g. \"http://localhost:11434/v1\" for Ollama)`);\n }\n if (embed.languages !== undefined && (!Array.isArray(embed.languages) || embed.languages.length === 0 || embed.languages.some((l) => typeof l !== 'string' || l === ''))) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.languages must be a non-empty array of language tags (e.g. [\"en\", \"zh\"])`);\n }\n if (embed.chunkTokens !== undefined && (typeof embed.chunkTokens !== 'number' || !Number.isInteger(embed.chunkTokens) || embed.chunkTokens <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: embed.chunkTokens must be a positive integer`);\n }\n}\n\nfunction isSignalName(value: unknown): value is SignalName {\n return typeof value === 'string' && (SIGNAL_NAMES as readonly string[]).includes(value);\n}\n\nfunction isSignalWeight(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0;\n}\n\n// Shape (an object of signal name -> weight) plus the one cross-signal prerequisite that doesn't\n// need the top-level embed block: links is seeded by word-match rows, so declaring it without words is always empty.\nfunction validatePresetSignals(name: string, value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value) || Object.keys(value).length === 0) {\n throw new SenseError(\n 'CONFIG_INVALID',\n `${configPath}: presets.${name}.signals must be a non-empty object of signal name -> weight (a finite number > 0), naming ${SIGNAL_NAMES.join(', ')} -- a preset that searches nothing is a mistake; name at least \"words\", or omit \"signals\" to use every signal whose prerequisites hold at weight 1`\n );\n }\n const entries = Object.entries(value as Record<string, unknown>);\n const unknown = entries.filter(([k]) => !isSignalName(k)).map(([k]) => k);\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals names unknown signal(s) ${unknown.join(', ')}; valid signals are ${SIGNAL_NAMES.join(', ')}`);\n }\n for (const [signal, weight] of entries) {\n if (!isSignalWeight(weight)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals.${signal} must be a finite number > 0`);\n }\n }\n const names = new Set(entries.map(([k]) => k));\n for (const [signal, requires] of Object.entries(SIGNAL_PREREQUISITES) as Array<[SignalName, SignalName]>) {\n if (names.has(signal) && !names.has(requires)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals has \"${signal}\" without \"${requires}\"; add \"${requires}\" to presets.${name}.signals, or drop \"${signal}\"`);\n }\n }\n}\n\nfunction validatePreset(name: string, value: unknown, configPath: string): void {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} must be an object`);\n }\n const preset = value as Record<string, unknown>;\n const unknown = Object.keys(preset).filter((k) => !KNOWN_PRESET_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name} has unknown key(s) ${unknown.join(', ')}; a preset takes include, exclude, k, signals, where`);\n }\n if (!isNonEmptyStringArray(preset.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.include must be a non-empty array of glob strings`);\n }\n if (preset.exclude !== undefined && !isNonEmptyStringArray(preset.exclude)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.exclude must be a non-empty array of glob strings`);\n }\n if (preset.signals !== undefined) validatePresetSignals(name, preset.signals, configPath);\n if (preset.k !== undefined && (typeof preset.k !== 'number' || !Number.isInteger(preset.k) || preset.k <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.k must be a positive integer`);\n }\n if (preset.where !== undefined && typeof preset.where !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.where must be a SQL condition string`);\n }\n}\n\n// A queries.<name> entry: { sql } or a saved search { search, preset?, include?, exclude?, where?, k? }.\nfunction validateSavedQuery(name: string, value: unknown, configPath: string): void {\n // A bare string is rejected rather than inferred to be SQL: an entry says which of the two\n // verbs it runs, the same choice the CLI makes explicit.\n if (typeof value === 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must say which verb it runs: { \"sql\": ${JSON.stringify(value.length > 40 ? `${value.slice(0, 37)}...` : value)} } to run it as SQL, or { \"search\": \"...\" } for a ranked search`);\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be { sql } or { search, preset?, include?, exclude?, where?, k? }`);\n }\n const entry = value as Record<string, unknown>;\n if ('sql' in entry) {\n const unknown = Object.keys(entry).filter((k) => k !== 'sql');\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a { sql } query takes only sql`);\n }\n if (typeof entry.sql !== 'string' || entry.sql.trim() === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.sql must be a non-empty string`);\n }\n return;\n }\n if ('search' in entry) {\n const unknown = Object.keys(entry).filter((k) => !SAVED_SEARCH_KEYS.has(k));\n if (unknown.length > 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} has unknown key(s) ${unknown.join(', ')}; a saved search takes search, preset, include, exclude, where, k`);\n }\n // A saved entry saves a question; a scope without a question is just flags.\n if (typeof entry.search !== 'string' || entry.search.trim() === '') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.search must be non-empty text`);\n }\n if (entry.preset !== undefined && (typeof entry.preset !== 'string' || entry.preset.length === 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.preset must be a preset name`);\n }\n if (entry.include !== undefined && !isNonEmptyStringArray(entry.include)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.include must be a non-empty array of glob strings`);\n }\n if (entry.exclude !== undefined && !isNonEmptyStringArray(entry.exclude)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.exclude must be a non-empty array of glob strings`);\n }\n if (entry.where !== undefined && typeof entry.where !== 'string') {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.where must be a SQL condition string`);\n }\n if (entry.k !== undefined && (typeof entry.k !== 'number' || !Number.isInteger(entry.k) || entry.k <= 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name}.k must be a positive integer`);\n }\n return;\n }\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries.${name} must be { sql } or { search, preset?, include?, exclude?, where?, k? }`);\n}\n\nexport function validateConfig(parsed: unknown, configPath: string): Config {\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);\n }\n const cfg = parsed as Record<string, unknown>;\n\n if (cfg.root !== undefined && (typeof cfg.root !== 'string' || cfg.root.length === 0)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: root must be a non-empty path string`);\n }\n\n // `checks` is rejected by name, not warned: silence would hide that assertions are gone.\n if (cfg.checks !== undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: checks was removed in v3 -- sense check no longer asserts on saved queries; a returned row set is the reader's judgment`);\n }\n\n // `content` (content.tokenize) is rejected by name for the same reason: every outcome it\n // selected (unspaced-script search, stemming, quoted-phrase substring matching) is now\n // automatic on every store, so a config that still sets it would silently get nothing back.\n if (cfg.content !== undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: content.tokenize was removed -- every outcome it selected is now automatic on every store; remove the \"content\" block`);\n }\n\n const presets = cfg.presets as Record<string, unknown> | undefined;\n if (!presets || typeof presets !== 'object' || Array.isArray(presets) || Object.keys(presets).length === 0) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets must be a non-empty object of preset name -> { include, exclude?, k?, signals?, where? }`);\n }\n for (const [name, value] of Object.entries(presets)) {\n validatePreset(name, value, configPath);\n }\n if (presets.default === undefined) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets must include a \"default\" preset`);\n }\n\n if (cfg.queries === undefined) cfg.queries = {};\n if (typeof cfg.queries !== 'object' || cfg.queries === null || Array.isArray(cfg.queries)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: queries must be an object of name -> { sql } or { search }`);\n }\n const queries = cfg.queries as Record<string, unknown>;\n for (const [name, value] of Object.entries(queries)) {\n validateSavedQuery(name, value, configPath);\n }\n\n if (cfg.features !== undefined) {\n validateFeaturesBlock(cfg.features, configPath);\n }\n if (cfg.embed !== undefined) {\n validateEmbedBlock(cfg.embed, configPath);\n }\n if (cfg.store !== undefined) {\n validateStoreKey(cfg.store, configPath);\n }\n\n // vectors' prerequisite is a named model, checked here once embed is known valid (or absent).\n if (cfg.embed === undefined) {\n for (const [name, value] of Object.entries(presets)) {\n const signals = (value as Record<string, unknown>).signals as Record<string, unknown> | undefined;\n if (signals && 'vectors' in signals) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: presets.${name}.signals includes \"vectors\", but no \"embed\" block names a model; add one, or drop \"vectors\" from presets.${name}.signals`);\n }\n }\n }\n\n return cfg as unknown as Config;\n}\n"],"names":["SenseError","SIGNAL_NAMES","SIGNAL_PREREQUISITES","STORE_NAMES","KNOWN_KEYS","Set","KNOWN_PRESET_KEYS","KNOWN_FEATURE_KEYS","KNOWN_EMBED_KEYS","SAVED_SEARCH_KEYS","unknownConfigKeys","cfg","Object","keys","filter","k","has","isNonEmptyStringArray","value","Array","isArray","length","every","g","validateLegacyScan","parsed","configPath","scan","include","validateStoreKey","includes","map","name","join","validateFeaturesBlock","block","unknown","v","entries","validateEmbedBlock","embed","model","undefined","provider","url","key","languages","some","l","chunkTokens","Number","isInteger","isSignalName","isSignalWeight","isFinite","validatePresetSignals","signal","weight","names","requires","validatePreset","preset","exclude","signals","where","validateSavedQuery","JSON","stringify","slice","entry","sql","trim","search","validateConfig","root","checks","content","presets","default","queries","features","store"],"mappings":"AAAA,SAASA,UAAU,QAAQ,eAAe;AAC1C,SAASC,YAAY,EAAEC,oBAAoB,QAAyB,eAAe;AACnF,SAAsBC,WAAW,QAAwB,aAAa;AAEtE,sFAAsF;AACtF,8FAA8F;AAC9F,MAAMC,aAAa,IAAIC,IAAI;IAAC;IAAW;IAAW;IAAQ;IAAW;IAAY;IAAS;IAAS;CAAU;AAC7G,MAAMC,oBAAoB,IAAID,IAAI;IAAC;IAAW;IAAW;IAAK;IAAW;CAAQ;AACjF,MAAME,qBAAqB,IAAIF,IAAI;IAAC;IAAS;IAAY;IAAQ;CAAO;AACxE,iGAAiG;AACjG,sDAAsD;AACtD,OAAO,MAAMG,mBAAmB,IAAIH,IAAI;IAAC;IAAS;IAAY;IAAO;IAAO;IAAa;CAAc,EAAE;AACzG,MAAMI,oBAAoB,IAAIJ,IAAI;IAAC;IAAU;IAAU;IAAW;IAAW;IAAS;CAAI;AAE1F,OAAO,SAASK,kBAAkBC,GAA4B;IAC5D,OAAOC,OAAOC,IAAI,CAACF,KAAKG,MAAM,CAAC,CAACC,IAAM,CAACX,WAAWY,GAAG,CAACD;AACxD;AAEA,SAASE,sBAAsBC,KAAc;IAC3C,OAAOC,MAAMC,OAAO,CAACF,UAAUA,MAAMG,MAAM,GAAG,KAAKH,MAAMI,KAAK,CAAC,CAACC,IAAM,OAAOA,MAAM,YAAYA,EAAEF,MAAM,GAAG;AAC5G;AAEA,yFAAyF;AACzF,4FAA4F;AAC5F,OAAO,SAASG,mBAAmBC,MAAe,EAAEC,UAAkB;IACpE,MAAMf,MAAM,OAAOc,WAAW,YAAYA,WAAW,QAAQ,CAACN,MAAMC,OAAO,CAACK,UAAWA,SAAqC;IAC5H,MAAME,OAAOhB,gBAAAA,0BAAAA,IAAKgB,IAAI;IACtB,IAAI,CAAChB,OAAO,CAACgB,QAAQ,CAACV,sBAAsBU,KAAKC,OAAO,GAAG;QACzD,MAAM,IAAI5B,WAAW,kBAAkB,GAAG0B,WAAW,wDAAwD,CAAC;IAChH;AACF;AAEA,SAASG,iBAAiBX,KAAc,EAAEQ,UAAkB;IAC1D,IAAI,CAACvB,YAAY2B,QAAQ,CAACZ,QAAqB;QAC7C,MAAM,IAAIlB,WAAW,kBAAkB,GAAG0B,WAAW,gBAAgB,EAAEvB,YAAY4B,GAAG,CAAC,CAACC,OAAS,CAAC,CAAC,EAAEA,KAAK,CAAC,CAAC,EAAEC,IAAI,CAAC,SAAS;IAC9H;AACF;AAEA,SAASC,sBAAsBhB,KAAc,EAAEQ,UAAkB;IAC/D,IAAI,OAAOR,UAAU,YAAYA,UAAU,QAAQC,MAAMC,OAAO,CAACF,QAAQ;QACvE,MAAM,IAAIlB,WAAW,kBAAkB,GAAG0B,WAAW,+CAA+C,CAAC;IACvG;IACA,MAAMS,QAAQjB;IACd,MAAMkB,UAAUxB,OAAOC,IAAI,CAACsB,OAAOrB,MAAM,CAAC,CAACC,IAAM,CAACR,mBAAmBS,GAAG,CAACD;IACzE,IAAIqB,QAAQf,MAAM,GAAG,GAAG;QACtB,MAAM,IAAIrB,WAAW,kBAAkB,GAAG0B,WAAW,8BAA8B,EAAEU,QAAQH,IAAI,CAAC,MAAM,gEAAgE,CAAC;IAC3K;IACA,KAAK,MAAM,CAACD,MAAMK,EAAE,IAAIzB,OAAO0B,OAAO,CAACH,OAAQ;QAC7C,IAAI,OAAOE,MAAM,WAAW;YAC1B,MAAM,IAAIrC,WAAW,kBAAkB,GAAG0B,WAAW,WAAW,EAAEM,KAAK,kBAAkB,CAAC;QAC5F;IACF;AACF;AAEA,SAASO,mBAAmBrB,KAAc,EAAEQ,UAAkB;IAC5D,IAAI,OAAOR,UAAU,YAAYA,UAAU,QAAQC,MAAMC,OAAO,CAACF,QAAQ;QACvE,MAAM,IAAIlB,WAAW,kBAAkB,GAAG0B,WAAW,0FAA0F,CAAC;IAClJ;IACA,MAAMc,QAAQtB;IACd,MAAMkB,UAAUxB,OAAOC,IAAI,CAAC2B,OAAO1B,MAAM,CAAC,CAACC,IAAM,CAACP,iBAAiBQ,GAAG,CAACD;IACvE,IAAIqB,QAAQf,MAAM,GAAG,GAAG;QACtB,MAAM,IAAIrB,WAAW,kBAAkB,GAAG0B,WAAW,2BAA2B,EAAEU,QAAQH,IAAI,CAAC,OAAO;IACxG;IACA,IAAIO,MAAMC,KAAK,KAAKC,aAAa,OAAOF,MAAMC,KAAK,KAAK,UAAU;QAChE,MAAM,IAAIzC,WAAW,kBAAkB,GAAG0B,WAAW,8BAA8B,CAAC;IACtF;IACA,yFAAyF;IACzF,4EAA4E;IAC5E,IAAIc,MAAMC,KAAK,KAAKC,aAAaF,MAAMC,KAAK,KAAK,IAAI;QACnD,MAAM,IAAIzC,WAAW,kBAAkB,GAAG0B,WAAW,oIAAoI,CAAC;IAC5L;IACA,IAAIc,MAAMG,QAAQ,KAAKD,aAAaF,MAAMG,QAAQ,KAAK,YAAYH,MAAMG,QAAQ,KAAK,YAAYH,MAAMG,QAAQ,KAAK,UAAU;QAC7H,MAAM,IAAI3C,WAAW,kBAAkB,GAAG0B,WAAW,wDAAwD,CAAC;IAChH;IACA,IAAIc,MAAMI,GAAG,KAAKF,aAAa,OAAOF,MAAMI,GAAG,KAAK,UAAU;QAC5D,MAAM,IAAI5C,WAAW,kBAAkB,GAAG0B,WAAW,4BAA4B,CAAC;IACpF;IACA,IAAIc,MAAMK,GAAG,KAAKH,aAAa,OAAOF,MAAMK,GAAG,KAAK,UAAU;QAC5D,MAAM,IAAI7C,WAAW,kBAAkB,GAAG0B,WAAW,4BAA4B,CAAC;IACpF;IACA,yFAAyF;IACzF,wDAAwD;IACxD,IAAIc,MAAMG,QAAQ,KAAK,YAAaH,CAAAA,MAAMI,GAAG,KAAKF,aAAaF,MAAMI,GAAG,KAAK,EAAC,GAAI;QAChF,MAAM,IAAI5C,WAAW,kBAAkB,GAAG0B,WAAW,0FAA0F,CAAC;IAClJ;IACA,IAAIc,MAAMM,SAAS,KAAKJ,aAAc,CAAA,CAACvB,MAAMC,OAAO,CAACoB,MAAMM,SAAS,KAAKN,MAAMM,SAAS,CAACzB,MAAM,KAAK,KAAKmB,MAAMM,SAAS,CAACC,IAAI,CAAC,CAACC,IAAM,OAAOA,MAAM,YAAYA,MAAM,GAAE,GAAI;QACxK,MAAM,IAAIhD,WAAW,kBAAkB,GAAG0B,WAAW,gFAAgF,CAAC;IACxI;IACA,IAAIc,MAAMS,WAAW,KAAKP,aAAc,CAAA,OAAOF,MAAMS,WAAW,KAAK,YAAY,CAACC,OAAOC,SAAS,CAACX,MAAMS,WAAW,KAAKT,MAAMS,WAAW,IAAI,CAAA,GAAI;QAChJ,MAAM,IAAIjD,WAAW,kBAAkB,GAAG0B,WAAW,8CAA8C,CAAC;IACtG;AACF;AAEA,SAAS0B,aAAalC,KAAc;IAClC,OAAO,OAAOA,UAAU,YAAY,AAACjB,aAAmC6B,QAAQ,CAACZ;AACnF;AAEA,SAASmC,eAAenC,KAAc;IACpC,OAAO,OAAOA,UAAU,YAAYgC,OAAOI,QAAQ,CAACpC,UAAUA,QAAQ;AACxE;AAEA,iGAAiG;AACjG,qHAAqH;AACrH,SAASqC,sBAAsBvB,IAAY,EAAEd,KAAc,EAAEQ,UAAkB;IAC7E,IAAI,OAAOR,UAAU,YAAYA,UAAU,QAAQC,MAAMC,OAAO,CAACF,UAAUN,OAAOC,IAAI,CAACK,OAAOG,MAAM,KAAK,GAAG;QAC1G,MAAM,IAAIrB,WACR,kBACA,GAAG0B,WAAW,UAAU,EAAEM,KAAK,2FAA2F,EAAE/B,aAAagC,IAAI,CAAC,MAAM,kJAAkJ,CAAC;IAE3S;IACA,MAAMK,UAAU1B,OAAO0B,OAAO,CAACpB;IAC/B,MAAMkB,UAAUE,QAAQxB,MAAM,CAAC,CAAC,CAACC,EAAE,GAAK,CAACqC,aAAarC,IAAIgB,GAAG,CAAC,CAAC,CAAChB,EAAE,GAAKA;IACvE,IAAIqB,QAAQf,MAAM,GAAG,GAAG;QACtB,MAAM,IAAIrB,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,iCAAiC,EAAEI,QAAQH,IAAI,CAAC,MAAM,oBAAoB,EAAEhC,aAAagC,IAAI,CAAC,OAAO;IAC7K;IACA,KAAK,MAAM,CAACuB,QAAQC,OAAO,IAAInB,QAAS;QACtC,IAAI,CAACe,eAAeI,SAAS;YAC3B,MAAM,IAAIzD,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,SAAS,EAAEwB,OAAO,4BAA4B,CAAC;QACvH;IACF;IACA,MAAME,QAAQ,IAAIrD,IAAIiC,QAAQP,GAAG,CAAC,CAAC,CAAChB,EAAE,GAAKA;IAC3C,KAAK,MAAM,CAACyC,QAAQG,SAAS,IAAI/C,OAAO0B,OAAO,CAACpC,sBAA0D;QACxG,IAAIwD,MAAM1C,GAAG,CAACwC,WAAW,CAACE,MAAM1C,GAAG,CAAC2C,WAAW;YAC7C,MAAM,IAAI3D,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,cAAc,EAAEwB,OAAO,WAAW,EAAEG,SAAS,QAAQ,EAAEA,SAAS,aAAa,EAAE3B,KAAK,mBAAmB,EAAEwB,OAAO,CAAC,CAAC;QAC1L;IACF;AACF;AAEA,SAASI,eAAe5B,IAAY,EAAEd,KAAc,EAAEQ,UAAkB;IACtE,IAAI,OAAOR,UAAU,YAAYA,UAAU,QAAQC,MAAMC,OAAO,CAACF,QAAQ;QACvE,MAAM,IAAIlB,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,kBAAkB,CAAC;IAC3F;IACA,MAAM6B,SAAS3C;IACf,MAAMkB,UAAUxB,OAAOC,IAAI,CAACgD,QAAQ/C,MAAM,CAAC,CAACC,IAAM,CAACT,kBAAkBU,GAAG,CAACD;IACzE,IAAIqB,QAAQf,MAAM,GAAG,GAAG;QACtB,MAAM,IAAIrB,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,oBAAoB,EAAEI,QAAQH,IAAI,CAAC,MAAM,oDAAoD,CAAC;IACtK;IACA,IAAI,CAAChB,sBAAsB4C,OAAOjC,OAAO,GAAG;QAC1C,MAAM,IAAI5B,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,kDAAkD,CAAC;IAC3H;IACA,IAAI6B,OAAOC,OAAO,KAAKpB,aAAa,CAACzB,sBAAsB4C,OAAOC,OAAO,GAAG;QAC1E,MAAM,IAAI9D,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,kDAAkD,CAAC;IAC3H;IACA,IAAI6B,OAAOE,OAAO,KAAKrB,WAAWa,sBAAsBvB,MAAM6B,OAAOE,OAAO,EAAErC;IAC9E,IAAImC,OAAO9C,CAAC,KAAK2B,aAAc,CAAA,OAAOmB,OAAO9C,CAAC,KAAK,YAAY,CAACmC,OAAOC,SAAS,CAACU,OAAO9C,CAAC,KAAK8C,OAAO9C,CAAC,IAAI,CAAA,GAAI;QAC5G,MAAM,IAAIf,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,6BAA6B,CAAC;IACtG;IACA,IAAI6B,OAAOG,KAAK,KAAKtB,aAAa,OAAOmB,OAAOG,KAAK,KAAK,UAAU;QAClE,MAAM,IAAIhE,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,qCAAqC,CAAC;IAC9G;AACF;AAEA,yGAAyG;AACzG,SAASiC,mBAAmBjC,IAAY,EAAEd,KAAc,EAAEQ,UAAkB;IAC1E,2FAA2F;IAC3F,yDAAyD;IACzD,IAAI,OAAOR,UAAU,UAAU;QAC7B,MAAM,IAAIlB,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,uCAAuC,EAAEkC,KAAKC,SAAS,CAACjD,MAAMG,MAAM,GAAG,KAAK,GAAGH,MAAMkD,KAAK,CAAC,GAAG,IAAI,GAAG,CAAC,GAAGlD,OAAO,+DAA+D,CAAC;IACxP;IACA,IAAI,OAAOA,UAAU,YAAYA,UAAU,QAAQC,MAAMC,OAAO,CAACF,QAAQ;QACvE,MAAM,IAAIlB,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,uEAAuE,CAAC;IAChJ;IACA,MAAMqC,QAAQnD;IACd,IAAI,SAASmD,OAAO;QAClB,MAAMjC,UAAUxB,OAAOC,IAAI,CAACwD,OAAOvD,MAAM,CAAC,CAACC,IAAMA,MAAM;QACvD,IAAIqB,QAAQf,MAAM,GAAG,GAAG;YACtB,MAAM,IAAIrB,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,oBAAoB,EAAEI,QAAQH,IAAI,CAAC,MAAM,gCAAgC,CAAC;QAClJ;QACA,IAAI,OAAOoC,MAAMC,GAAG,KAAK,YAAYD,MAAMC,GAAG,CAACC,IAAI,OAAO,IAAI;YAC5D,MAAM,IAAIvE,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,+BAA+B,CAAC;QACxG;QACA;IACF;IACA,IAAI,YAAYqC,OAAO;QACrB,MAAMjC,UAAUxB,OAAOC,IAAI,CAACwD,OAAOvD,MAAM,CAAC,CAACC,IAAM,CAACN,kBAAkBO,GAAG,CAACD;QACxE,IAAIqB,QAAQf,MAAM,GAAG,GAAG;YACtB,MAAM,IAAIrB,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,oBAAoB,EAAEI,QAAQH,IAAI,CAAC,MAAM,iEAAiE,CAAC;QACnL;QACA,4EAA4E;QAC5E,IAAI,OAAOoC,MAAMG,MAAM,KAAK,YAAYH,MAAMG,MAAM,CAACD,IAAI,OAAO,IAAI;YAClE,MAAM,IAAIvE,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,8BAA8B,CAAC;QACvG;QACA,IAAIqC,MAAMR,MAAM,KAAKnB,aAAc,CAAA,OAAO2B,MAAMR,MAAM,KAAK,YAAYQ,MAAMR,MAAM,CAACxC,MAAM,KAAK,CAAA,GAAI;YACjG,MAAM,IAAIrB,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,6BAA6B,CAAC;QACtG;QACA,IAAIqC,MAAMzC,OAAO,KAAKc,aAAa,CAACzB,sBAAsBoD,MAAMzC,OAAO,GAAG;YACxE,MAAM,IAAI5B,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,kDAAkD,CAAC;QAC3H;QACA,IAAIqC,MAAMP,OAAO,KAAKpB,aAAa,CAACzB,sBAAsBoD,MAAMP,OAAO,GAAG;YACxE,MAAM,IAAI9D,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,kDAAkD,CAAC;QAC3H;QACA,IAAIqC,MAAML,KAAK,KAAKtB,aAAa,OAAO2B,MAAML,KAAK,KAAK,UAAU;YAChE,MAAM,IAAIhE,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,qCAAqC,CAAC;QAC9G;QACA,IAAIqC,MAAMtD,CAAC,KAAK2B,aAAc,CAAA,OAAO2B,MAAMtD,CAAC,KAAK,YAAY,CAACmC,OAAOC,SAAS,CAACkB,MAAMtD,CAAC,KAAKsD,MAAMtD,CAAC,IAAI,CAAA,GAAI;YACxG,MAAM,IAAIf,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,6BAA6B,CAAC;QACtG;QACA;IACF;IACA,MAAM,IAAIhC,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,uEAAuE,CAAC;AAChJ;AAEA,OAAO,SAASyC,eAAehD,MAAe,EAAEC,UAAkB;IAChE,IAAI,OAAOD,WAAW,YAAYA,WAAW,QAAQN,MAAMC,OAAO,CAACK,SAAS;QAC1E,MAAM,IAAIzB,WAAW,kBAAkB,GAAG0B,WAAW,8BAA8B,CAAC;IACtF;IACA,MAAMf,MAAMc;IAEZ,IAAId,IAAI+D,IAAI,KAAKhC,aAAc,CAAA,OAAO/B,IAAI+D,IAAI,KAAK,YAAY/D,IAAI+D,IAAI,CAACrD,MAAM,KAAK,CAAA,GAAI;QACrF,MAAM,IAAIrB,WAAW,kBAAkB,GAAG0B,WAAW,sCAAsC,CAAC;IAC9F;IAEA,yFAAyF;IACzF,IAAIf,IAAIgE,MAAM,KAAKjC,WAAW;QAC5B,MAAM,IAAI1C,WAAW,kBAAkB,GAAG0B,WAAW,yHAAyH,CAAC;IACjL;IAEA,yFAAyF;IACzF,uFAAuF;IACvF,4FAA4F;IAC5F,IAAIf,IAAIiE,OAAO,KAAKlC,WAAW;QAC7B,MAAM,IAAI1C,WAAW,kBAAkB,GAAG0B,WAAW,uHAAuH,CAAC;IAC/K;IAEA,MAAMmD,UAAUlE,IAAIkE,OAAO;IAC3B,IAAI,CAACA,WAAW,OAAOA,YAAY,YAAY1D,MAAMC,OAAO,CAACyD,YAAYjE,OAAOC,IAAI,CAACgE,SAASxD,MAAM,KAAK,GAAG;QAC1G,MAAM,IAAIrB,WAAW,kBAAkB,GAAG0B,WAAW,kGAAkG,CAAC;IAC1J;IACA,KAAK,MAAM,CAACM,MAAMd,MAAM,IAAIN,OAAO0B,OAAO,CAACuC,SAAU;QACnDjB,eAAe5B,MAAMd,OAAOQ;IAC9B;IACA,IAAImD,QAAQC,OAAO,KAAKpC,WAAW;QACjC,MAAM,IAAI1C,WAAW,kBAAkB,GAAG0B,WAAW,yCAAyC,CAAC;IACjG;IAEA,IAAIf,IAAIoE,OAAO,KAAKrC,WAAW/B,IAAIoE,OAAO,GAAG,CAAC;IAC9C,IAAI,OAAOpE,IAAIoE,OAAO,KAAK,YAAYpE,IAAIoE,OAAO,KAAK,QAAQ5D,MAAMC,OAAO,CAACT,IAAIoE,OAAO,GAAG;QACzF,MAAM,IAAI/E,WAAW,kBAAkB,GAAG0B,WAAW,4DAA4D,CAAC;IACpH;IACA,MAAMqD,UAAUpE,IAAIoE,OAAO;IAC3B,KAAK,MAAM,CAAC/C,MAAMd,MAAM,IAAIN,OAAO0B,OAAO,CAACyC,SAAU;QACnDd,mBAAmBjC,MAAMd,OAAOQ;IAClC;IAEA,IAAIf,IAAIqE,QAAQ,KAAKtC,WAAW;QAC9BR,sBAAsBvB,IAAIqE,QAAQ,EAAEtD;IACtC;IACA,IAAIf,IAAI6B,KAAK,KAAKE,WAAW;QAC3BH,mBAAmB5B,IAAI6B,KAAK,EAAEd;IAChC;IACA,IAAIf,IAAIsE,KAAK,KAAKvC,WAAW;QAC3Bb,iBAAiBlB,IAAIsE,KAAK,EAAEvD;IAC9B;IAEA,8FAA8F;IAC9F,IAAIf,IAAI6B,KAAK,KAAKE,WAAW;QAC3B,KAAK,MAAM,CAACV,MAAMd,MAAM,IAAIN,OAAO0B,OAAO,CAACuC,SAAU;YACnD,MAAMd,UAAU,AAAC7C,MAAkC6C,OAAO;YAC1D,IAAIA,WAAW,aAAaA,SAAS;gBACnC,MAAM,IAAI/D,WAAW,kBAAkB,GAAG0B,WAAW,UAAU,EAAEM,KAAK,yGAAyG,EAAEA,KAAK,QAAQ,CAAC;YACjM;QACF;IACF;IAEA,OAAOrB;AACT"}