kontext-engine 0.1.2 → 0.1.4

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.
package/dist/index.js CHANGED
@@ -1198,12 +1198,15 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
1198
1198
  const rows = db.prepare(
1199
1199
  `SELECT c.id, c.file_id as fileId, f.path as filePath, f.language,
1200
1200
  c.line_start as lineStart, c.line_end as lineEnd,
1201
- c.type, c.name, c.parent, c.text
1201
+ c.type, c.name, c.parent, c.text, c.exports as exports
1202
1202
  FROM chunks c
1203
1203
  JOIN files f ON f.id = c.file_id
1204
1204
  WHERE c.id IN (${placeholders})`
1205
1205
  ).all(...ids);
1206
- return rows;
1206
+ return rows.map((r) => ({
1207
+ ...r,
1208
+ exports: r.exports === 1
1209
+ }));
1207
1210
  },
1208
1211
  searchChunks(filters, limit) {
1209
1212
  const conditions = [];
@@ -1240,7 +1243,7 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
1240
1243
  const sql = `
1241
1244
  SELECT c.id, c.file_id as fileId, f.path as filePath, f.language,
1242
1245
  c.line_start as lineStart, c.line_end as lineEnd,
1243
- c.type, c.name, c.parent, c.text
1246
+ c.type, c.name, c.parent, c.text, c.exports as exports
1244
1247
  FROM chunks c
1245
1248
  JOIN files f ON f.id = c.file_id
1246
1249
  ${where}
@@ -1248,7 +1251,11 @@ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
1248
1251
  LIMIT ?
1249
1252
  `;
1250
1253
  params.push(limit);
1251
- return db.prepare(sql).all(...params);
1254
+ const rows = db.prepare(sql).all(...params);
1255
+ return rows.map((r) => ({
1256
+ ...r,
1257
+ exports: r.exports === 1
1258
+ }));
1252
1259
  },
1253
1260
  deleteChunksByFile(fileId) {
1254
1261
  const chunkRows = stmtGetChunkIdsByFile.all(fileId);
@@ -1346,6 +1353,7 @@ async function vectorSearch(db, embedder, query, limit, filters) {
1346
1353
  lineEnd: chunk.lineEnd,
1347
1354
  name: chunk.name,
1348
1355
  type: chunk.type,
1356
+ exported: chunk.exports,
1349
1357
  text: chunk.text,
1350
1358
  score: distanceToScore(vr.distance),
1351
1359
  language: chunk.language
@@ -1357,7 +1365,15 @@ async function vectorSearch(db, embedder, query, limit, filters) {
1357
1365
 
1358
1366
  // src/search/fts.ts
1359
1367
  function sanitizeFtsQuery(query) {
1360
- return query.replace(/[?()":^~{}!+\-\\]/g, " ").replace(/(?<!\w)\*/g, " ").replace(/\s+/g, " ").trim();
1368
+ const tokenized = query.replace(/[^A-Za-z0-9_*]+/g, " ").trim();
1369
+ if (tokenized.length === 0) return "";
1370
+ const sanitizedTerms = tokenized.split(/\s+/).map((term) => {
1371
+ const hasTrailingWildcard = /\*+$/.test(term);
1372
+ const base = term.replace(/\*/g, "");
1373
+ if (base.length === 0) return "";
1374
+ return hasTrailingWildcard ? `${base}*` : base;
1375
+ }).filter((term) => term.length > 0);
1376
+ return sanitizedTerms.join(" ");
1361
1377
  }
1362
1378
  function bm25ToScore(rank) {
1363
1379
  return 1 / (1 + Math.abs(rank));
@@ -1386,6 +1402,7 @@ function ftsSearch(db, query, limit, filters) {
1386
1402
  lineEnd: chunk.lineEnd,
1387
1403
  name: chunk.name,
1388
1404
  type: chunk.type,
1405
+ exported: chunk.exports,
1389
1406
  text: chunk.text,
1390
1407
  score: bm25ToScore(fts.rank),
1391
1408
  language: chunk.language
@@ -1420,6 +1437,7 @@ function astSearch(db, filters, limit) {
1420
1437
  lineEnd: chunk.lineEnd,
1421
1438
  name: chunk.name,
1422
1439
  type: chunk.type,
1440
+ exported: chunk.exports,
1423
1441
  text: chunk.text,
1424
1442
  score,
1425
1443
  language: chunk.language
@@ -1472,6 +1490,7 @@ function pathSearch(db, pattern, limit) {
1472
1490
  lineEnd: chunk.lineEnd,
1473
1491
  name: chunk.name,
1474
1492
  type: chunk.type,
1493
+ exported: chunk.exports,
1475
1494
  text: chunk.text,
1476
1495
  score: 1,
1477
1496
  language: file.language
@@ -1499,26 +1518,56 @@ function pathKeywordSearch(db, query, limit) {
1499
1518
  }
1500
1519
  if (scoredPaths.length === 0) return [];
1501
1520
  scoredPaths.sort((a, b) => b.score - a.score);
1502
- const results = [];
1521
+ const matchedFiles = [];
1503
1522
  for (const { filePath, score } of scoredPaths) {
1504
- if (results.length >= limit) break;
1505
1523
  const file = db.getFile(filePath);
1506
1524
  if (!file) continue;
1507
1525
  const chunks = db.getChunksByFile(file.id);
1508
- for (const chunk of chunks) {
1526
+ if (chunks.length === 0) continue;
1527
+ matchedFiles.push({
1528
+ filePath: file.path,
1529
+ language: file.language,
1530
+ score,
1531
+ chunks
1532
+ });
1533
+ }
1534
+ if (matchedFiles.length === 0) return [];
1535
+ const results = [];
1536
+ const pushChunk = (filePath, language, score, chunk) => {
1537
+ results.push({
1538
+ chunkId: chunk.id,
1539
+ filePath,
1540
+ lineStart: chunk.lineStart,
1541
+ lineEnd: chunk.lineEnd,
1542
+ name: chunk.name,
1543
+ type: chunk.type,
1544
+ exported: chunk.exports,
1545
+ text: chunk.text,
1546
+ score,
1547
+ language
1548
+ });
1549
+ };
1550
+ for (const matched of matchedFiles) {
1551
+ if (results.length >= limit) break;
1552
+ pushChunk(
1553
+ matched.filePath,
1554
+ matched.language,
1555
+ matched.score,
1556
+ matched.chunks[0]
1557
+ );
1558
+ }
1559
+ let offset = 1;
1560
+ while (results.length < limit) {
1561
+ let addedInRound = false;
1562
+ for (const matched of matchedFiles) {
1509
1563
  if (results.length >= limit) break;
1510
- results.push({
1511
- chunkId: chunk.id,
1512
- filePath: file.path,
1513
- lineStart: chunk.lineStart,
1514
- lineEnd: chunk.lineEnd,
1515
- name: chunk.name,
1516
- type: chunk.type,
1517
- text: chunk.text,
1518
- score,
1519
- language: file.language
1520
- });
1564
+ const chunk = matched.chunks[offset];
1565
+ if (!chunk) continue;
1566
+ pushChunk(matched.filePath, matched.language, matched.score, chunk);
1567
+ addedInRound = true;
1521
1568
  }
1569
+ if (!addedInRound) break;
1570
+ offset++;
1522
1571
  }
1523
1572
  return results;
1524
1573
  }
@@ -1563,6 +1612,7 @@ function dependencyTrace(db, chunkId, direction, depth) {
1563
1612
  lineEnd: chunk.lineEnd,
1564
1613
  name: chunk.name,
1565
1614
  type: chunk.type,
1615
+ exported: chunk.exports,
1566
1616
  text: chunk.text,
1567
1617
  score,
1568
1618
  language: chunk.language
@@ -1613,11 +1663,24 @@ var PATH_BOOST_DIR_EXACT = 1.5;
1613
1663
  var PATH_BOOST_FILENAME = 1.4;
1614
1664
  var PATH_BOOST_PARTIAL = 1.2;
1615
1665
  var IMPORT_PENALTY = 0.5;
1666
+ var TEST_FILE_PENALTY = 0.65;
1667
+ var SMALL_SNIPPET_PENALTY = 0.75;
1668
+ var PUBLIC_API_BOOST = 1.12;
1669
+ var TEST_FILE_DIRECTORY_PATTERN = /(?:^|\/)(?:tests|__tests__)(?:\/|$)/;
1670
+ var TEST_FILE_NAME_PATTERN = /(?:^|\/)[^/]*\.(?:test|spec)\.[cm]?[jt]sx?$/;
1671
+ var SMALL_SNIPPET_MAX_LINES = 3;
1672
+ function extractPathBoostTerms(query) {
1673
+ return query.split(/\s+/).map((t) => t.trim()).filter((t) => t.length >= 2);
1674
+ }
1616
1675
  function fusionMergeWithPathBoost(strategyResults, limit, pathBoostTerms) {
1617
1676
  const fused = fusionMerge(strategyResults, limit * 3);
1618
1677
  if (fused.length === 0) return [];
1619
1678
  const boosted = applyPathBoost(fused, pathBoostTerms);
1620
- const adjusted = applyImportDeprioritization(boosted);
1679
+ const importAdjusted = applyImportDeprioritization(boosted);
1680
+ const testAdjusted = applyTestFileDeprioritization(importAdjusted);
1681
+ const snippetAdjusted = applySmallSnippetDeprioritization(testAdjusted);
1682
+ const boostedApi = applyPublicApiBoost(snippetAdjusted);
1683
+ const adjusted = applyFileDiversityDiminishingReturns(boostedApi);
1621
1684
  adjusted.sort((a, b) => b.score - a.score);
1622
1685
  const sliced = adjusted.slice(0, limit);
1623
1686
  return renormalize(sliced);
@@ -1667,6 +1730,76 @@ function applyImportDeprioritization(results) {
1667
1730
  return r;
1668
1731
  });
1669
1732
  }
1733
+ function applyTestFileDeprioritization(results) {
1734
+ const hasNonTestFile = results.some((r) => !isTestFilePath(r.filePath));
1735
+ if (!hasNonTestFile) return results;
1736
+ const maxNonTestScore = Math.max(
1737
+ ...results.filter((r) => !isTestFilePath(r.filePath)).map((r) => r.score),
1738
+ 0
1739
+ );
1740
+ if (maxNonTestScore === 0) return results;
1741
+ return results.map((r) => {
1742
+ if (isTestFilePath(r.filePath)) {
1743
+ return { ...r, score: r.score * TEST_FILE_PENALTY };
1744
+ }
1745
+ return r;
1746
+ });
1747
+ }
1748
+ function applySmallSnippetDeprioritization(results) {
1749
+ const hasNonSmallSnippet = results.some((r) => !isSmallSnippet(r));
1750
+ if (!hasNonSmallSnippet) return results;
1751
+ const maxNonSmallScore = Math.max(
1752
+ ...results.filter((r) => !isSmallSnippet(r)).map((r) => r.score),
1753
+ 0
1754
+ );
1755
+ if (maxNonSmallScore === 0) return results;
1756
+ return results.map((r) => {
1757
+ if (isSmallSnippet(r)) {
1758
+ return { ...r, score: r.score * SMALL_SNIPPET_PENALTY };
1759
+ }
1760
+ return r;
1761
+ });
1762
+ }
1763
+ function applyPublicApiBoost(results) {
1764
+ return results.map((r) => {
1765
+ if (isPublicApiSymbol(r)) {
1766
+ return { ...r, score: r.score * PUBLIC_API_BOOST };
1767
+ }
1768
+ return r;
1769
+ });
1770
+ }
1771
+ function applyFileDiversityDiminishingReturns(results) {
1772
+ if (results.length <= 1) return results;
1773
+ const ranked = [...results].sort((a, b) => b.score - a.score);
1774
+ const seenPerFile = /* @__PURE__ */ new Map();
1775
+ return ranked.map((r) => {
1776
+ const count = (seenPerFile.get(r.filePath) ?? 0) + 1;
1777
+ seenPerFile.set(r.filePath, count);
1778
+ return {
1779
+ ...r,
1780
+ score: r.score * getFileDiversityFactor(count)
1781
+ };
1782
+ });
1783
+ }
1784
+ function isTestFilePath(filePath) {
1785
+ const normalizedPath = filePath.toLowerCase().replace(/\\/g, "/");
1786
+ return TEST_FILE_DIRECTORY_PATTERN.test(normalizedPath) || TEST_FILE_NAME_PATTERN.test(normalizedPath);
1787
+ }
1788
+ function isSmallSnippet(result) {
1789
+ const lineCount = Math.max(1, result.lineEnd - result.lineStart + 1);
1790
+ return lineCount <= SMALL_SNIPPET_MAX_LINES;
1791
+ }
1792
+ function isPublicApiSymbol(result) {
1793
+ if (result.exported === true) return true;
1794
+ const textStart = result.text.trimStart().toLowerCase();
1795
+ return textStart.startsWith("export ");
1796
+ }
1797
+ function getFileDiversityFactor(fileOccurrence) {
1798
+ if (fileOccurrence <= 1) return 1;
1799
+ if (fileOccurrence === 2) return 0.9;
1800
+ if (fileOccurrence === 3) return 0.8;
1801
+ return 0.7;
1802
+ }
1670
1803
  function renormalize(results) {
1671
1804
  if (results.length === 0) return results;
1672
1805
  const maxScore = Math.max(...results.map((r) => r.score));
@@ -1677,25 +1810,210 @@ function renormalize(results) {
1677
1810
  }));
1678
1811
  }
1679
1812
 
1680
- // src/steering/llm.ts
1681
- var PLAN_SYSTEM_PROMPT = `You are a code search strategy planner. Given a user query about code, output a JSON object with:
1682
- - "interpretation": a one-line summary of what the user is looking for
1683
- - "strategies": an array of search strategy objects, each with:
1684
- - "strategy": one of "vector", "fts", "ast", "path", "dependency"
1685
- - "query": the optimized query string for that strategy
1686
- - "weight": a number 0-1 indicating importance
1687
- - "reason": brief explanation of why this strategy is used
1813
+ // src/steering/prompts.ts
1814
+ var PLAN_SYSTEM_PROMPT = `You are a code-search strategy planner for a TypeScript/JavaScript codebase.
1815
+
1816
+ Given a user query, produce a JSON object with:
1817
+ - "interpretation": one sentence summarising what the user wants to find.
1818
+ - "strategies": an ordered array of search strategies (most important first).
1819
+
1820
+ Each strategy object has:
1821
+ "strategy" \u2014 one of "vector", "fts", "ast", "path", "dependency"
1822
+ "query" \u2014 the optimised search string for that strategy (see rules below)
1823
+ "weight" \u2014 importance 0\u20131 (highest-priority strategy gets 1.0)
1824
+ "reason" \u2014 one sentence explaining why this strategy helps
1825
+
1826
+ ## Strategy selection rules
1827
+
1828
+ | Signal in query | Primary strategy | Supporting strategies |
1829
+ |---|---|---|
1830
+ | Conceptual / "how does X work" / natural language | vector | fts, ast |
1831
+ | Exact keyword, identifier, or error message | fts | ast |
1832
+ | Symbol name (function, class, type, variable) | ast | fts |
1833
+ | File path, glob, or extension (e.g. "*.test.ts") | path | fts |
1834
+ | Import chain / "what depends on X" | dependency | ast, fts |
1835
+ | Mixed: natural language + code symbol | vector + ast | fts |
1836
+
1837
+ ## Query optimisation rules
1838
+ - **vector**: keep the query close to natural language; rephrase for semantic similarity.
1839
+ - **fts**: extract the most distinctive keywords/identifiers; drop stop words.
1840
+ - **ast**: use only the symbol name (camelCase, snake_case, or PascalCase). Strip surrounding prose.
1841
+ - **path**: use a glob or slash-separated path segment (e.g. "src/auth/*.ts").
1842
+ - **dependency**: use the bare module or file name being imported.
1843
+
1844
+ ## Edge cases
1845
+ - **Vague query** (e.g. "help me understand this"): use vector with the full query; add fts with any nouns present.
1846
+ - **Multi-concept query** (e.g. "authentication and rate limiting"): create separate strategies for each concept, both at high weight.
1847
+ - **Code symbol mixed with prose** (e.g. "where is the validateToken function called"): use ast for the symbol and vector for the intent.
1848
+ - **Query is just a symbol** (e.g. "createPool"): use ast at weight 1.0 and fts at weight 0.7. Skip vector.
1849
+
1850
+ ## Examples
1851
+
1852
+ User: "how does authentication work"
1853
+ \`\`\`json
1854
+ {
1855
+ "interpretation": "Understand the authentication flow and related middleware.",
1856
+ "strategies": [
1857
+ { "strategy": "vector", "query": "authentication flow middleware", "weight": 1.0, "reason": "Conceptual question best served by semantic search." },
1858
+ { "strategy": "fts", "query": "authentication middleware auth", "weight": 0.7, "reason": "Keyword fallback for auth-related identifiers." },
1859
+ { "strategy": "ast", "query": "authenticate", "weight": 0.6, "reason": "Likely function or class name." }
1860
+ ]
1861
+ }
1862
+ \`\`\`
1863
+
1864
+ User: "validateToken"
1865
+ \`\`\`json
1866
+ {
1867
+ "interpretation": "Find the validateToken symbol definition and usages.",
1868
+ "strategies": [
1869
+ { "strategy": "ast", "query": "validateToken", "weight": 1.0, "reason": "Exact symbol lookup." },
1870
+ { "strategy": "fts", "query": "validateToken", "weight": 0.7, "reason": "Catch references in comments or strings." }
1871
+ ]
1872
+ }
1873
+ \`\`\`
1874
+
1875
+ User: "where is rate limiting configured in src/middleware"
1876
+ \`\`\`json
1877
+ {
1878
+ "interpretation": "Locate rate-limiting configuration inside the middleware directory.",
1879
+ "strategies": [
1880
+ { "strategy": "path", "query": "src/middleware/*", "weight": 0.9, "reason": "Scope results to the specified directory." },
1881
+ { "strategy": "vector", "query": "rate limiting configuration", "weight": 1.0, "reason": "Semantic match for the concept." },
1882
+ { "strategy": "fts", "query": "rateLimit rateLimiter", "weight": 0.7, "reason": "Common identifier variants." }
1883
+ ]
1884
+ }
1885
+ \`\`\`
1886
+
1887
+ User: "authentication and database connection pooling"
1888
+ \`\`\`json
1889
+ {
1890
+ "interpretation": "Find code related to both authentication and database connection pooling.",
1891
+ "strategies": [
1892
+ { "strategy": "vector", "query": "authentication login", "weight": 1.0, "reason": "Semantic search for the auth concept." },
1893
+ { "strategy": "vector", "query": "database connection pool", "weight": 1.0, "reason": "Semantic search for the DB pooling concept." },
1894
+ { "strategy": "fts", "query": "auth createPool connectionPool", "weight": 0.7, "reason": "Keyword fallback for likely identifiers." }
1895
+ ]
1896
+ }
1897
+ \`\`\`
1898
+
1899
+ Output ONLY the JSON object. No markdown fences, no commentary.`;
1900
+ var SYNTHESIZE_SYSTEM_PROMPT = `You are a code-search assistant. Given a user query and ranked search results, produce a concise, actionable summary.
1901
+
1902
+ ## Output structure (plain text, no markdown)
1688
1903
 
1689
- Choose strategies based on query type:
1690
- - Conceptual/natural language \u2192 vector (semantic search)
1691
- - Keywords/identifiers \u2192 fts (full-text search)
1692
- - Symbol names (functions, classes) \u2192 ast (structural search)
1693
- - File paths or patterns \u2192 path (path glob search)
1694
- - Import/dependency chains \u2192 dependency
1904
+ 1. **Key finding** (1\u20132 sentences): the most important result or answer first.
1905
+ 2. **Supporting locations** (bulleted, max 5): each line is "filePath:lineStart \u2013 brief description".
1906
+ 3. **Additional context** (0\u20132 sentences, optional): relationships between results, patterns, or next steps.
1695
1907
 
1696
- Output ONLY valid JSON, no markdown.`;
1697
- var SYNTHESIZE_SYSTEM_PROMPT = `You are a code search assistant. Given search results, write a brief, helpful explanation of what was found. Be concise (2-4 sentences). Reference specific files and function names. Do not use markdown.`;
1908
+ ## Rules
1909
+ - Always reference file paths and line numbers from the search results.
1910
+ - Mention specific symbol names (functions, classes, types) when they appear in results.
1911
+ - If no result clearly answers the query, say so and suggest a refined search.
1912
+ - Be concise \u2014 aim for 4\u20138 lines total. Do not repeat the query back.
1913
+ - Do not use markdown formatting (no #, *, \`, or fences). Use plain text only.
1914
+ - Group related results rather than listing every result individually.
1915
+
1916
+ ## Example
1917
+
1918
+ Query: "how does token validation work"
1919
+ Results include validateToken in src/auth/tokens.ts:42 and authMiddleware in src/middleware/auth.ts:15.
1920
+
1921
+ Good output:
1922
+ Token validation is handled by validateToken (src/auth/tokens.ts:42), which decodes a JWT and checks expiry and signature against the configured secret.
1923
+
1924
+ Related locations:
1925
+ - src/auth/tokens.ts:42 \u2013 validateToken: core JWT decode + verify logic
1926
+ - src/middleware/auth.ts:15 \u2013 authMiddleware: calls validateToken on every protected route
1927
+ - src/auth/types.ts:5 \u2013 TokenPayload type definition
1928
+
1929
+ The middleware extracts the Bearer token from the Authorization header before passing it to validateToken.`;
1930
+
1931
+ // src/steering/classify.ts
1932
+ var SYMBOL_CAMEL_RE = /^[a-z][a-zA-Z0-9]*$/;
1933
+ var SYMBOL_PASCAL_RE = /^[A-Z][a-zA-Z0-9]*$/;
1934
+ var SYMBOL_SNAKE_RE = /^[a-z]+(?:_[a-z]+)+$/;
1935
+ var SYMBOL_UPPER_RE = /^[A-Z]+(?:_[A-Z]+)*$/;
1936
+ var PATH_EXTENSION_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|swift|rb|php|cs|cpp|c|h|hpp|json|yaml|yml|toml|md|sql|sh|bash)$/i;
1937
+ var QUESTION_WORDS = /* @__PURE__ */ new Set([
1938
+ "how",
1939
+ "what",
1940
+ "where",
1941
+ "why",
1942
+ "when",
1943
+ "which",
1944
+ "show",
1945
+ "explain",
1946
+ "find",
1947
+ "list"
1948
+ ]);
1698
1949
  var STOP_WORDS = /* @__PURE__ */ new Set([
1950
+ "the",
1951
+ "a",
1952
+ "an",
1953
+ "is",
1954
+ "are",
1955
+ "was",
1956
+ "were",
1957
+ "do",
1958
+ "does",
1959
+ "did",
1960
+ "to",
1961
+ "for",
1962
+ "of",
1963
+ "in",
1964
+ "on",
1965
+ "with",
1966
+ "by",
1967
+ "and",
1968
+ "or"
1969
+ ]);
1970
+ function defaultMultipliers() {
1971
+ return {
1972
+ vector: 1,
1973
+ fts: 1,
1974
+ ast: 1,
1975
+ path: 1,
1976
+ dependency: 1
1977
+ };
1978
+ }
1979
+ function isSymbolQuery(query) {
1980
+ return SYMBOL_CAMEL_RE.test(query) || SYMBOL_PASCAL_RE.test(query) || SYMBOL_SNAKE_RE.test(query) || SYMBOL_UPPER_RE.test(query);
1981
+ }
1982
+ function isPathQuery(query) {
1983
+ return query.includes("/") || PATH_EXTENSION_RE.test(query);
1984
+ }
1985
+ function isNaturalLanguageQuery(query) {
1986
+ const lower = query.toLowerCase();
1987
+ const words = lower.split(/\s+/).filter((w) => w.length > 0);
1988
+ const hasQuestionWord = words.some((w) => QUESTION_WORDS.has(w));
1989
+ const hasStopWord = words.some((w) => STOP_WORDS.has(w));
1990
+ return hasQuestionWord || words.length >= 4 && hasStopWord;
1991
+ }
1992
+ function classifyQuery(query) {
1993
+ const trimmed = query.trim();
1994
+ const multipliers = defaultMultipliers();
1995
+ if (isPathQuery(trimmed)) {
1996
+ multipliers.path = 2;
1997
+ multipliers.ast = 0.5;
1998
+ return { kind: "path", multipliers };
1999
+ }
2000
+ if (isSymbolQuery(trimmed)) {
2001
+ multipliers.ast = 1.5;
2002
+ multipliers.vector = 0.5;
2003
+ return { kind: "symbol", multipliers };
2004
+ }
2005
+ if (isNaturalLanguageQuery(trimmed)) {
2006
+ multipliers.vector = 1.5;
2007
+ multipliers.path = 1.2;
2008
+ multipliers.ast = 0.7;
2009
+ return { kind: "natural_language", multipliers };
2010
+ }
2011
+ return { kind: "keyword", multipliers };
2012
+ }
2013
+
2014
+ // src/steering/llm.ts
2015
+ var STOP_WORDS2 = /* @__PURE__ */ new Set([
2016
+ // Interrogatives & conjunctions
1699
2017
  "how",
1700
2018
  "does",
1701
2019
  "what",
@@ -1705,6 +2023,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
1705
2023
  "which",
1706
2024
  "who",
1707
2025
  "whom",
2026
+ // Be-verbs
1708
2027
  "is",
1709
2028
  "are",
1710
2029
  "was",
@@ -1712,10 +2031,12 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
1712
2031
  "be",
1713
2032
  "been",
1714
2033
  "being",
2034
+ // Do-verbs
1715
2035
  "do",
1716
2036
  "did",
1717
2037
  "doing",
1718
2038
  "done",
2039
+ // Articles, connectors, prepositions
1719
2040
  "the",
1720
2041
  "a",
1721
2042
  "an",
@@ -1734,12 +2055,30 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
1734
2055
  "by",
1735
2056
  "from",
1736
2057
  "about",
2058
+ "into",
2059
+ "through",
2060
+ "between",
2061
+ "after",
2062
+ "before",
2063
+ "during",
2064
+ // Pronouns & demonstratives
1737
2065
  "it",
1738
2066
  "its",
1739
2067
  "this",
1740
2068
  "that",
1741
2069
  "these",
1742
2070
  "those",
2071
+ "i",
2072
+ "me",
2073
+ "my",
2074
+ "we",
2075
+ "our",
2076
+ "you",
2077
+ "your",
2078
+ "he",
2079
+ "she",
2080
+ "they",
2081
+ // Modals
1743
2082
  "can",
1744
2083
  "could",
1745
2084
  "should",
@@ -1748,32 +2087,150 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
1748
2087
  "shall",
1749
2088
  "may",
1750
2089
  "might",
2090
+ // Have-verbs
1751
2091
  "has",
1752
2092
  "have",
1753
2093
  "had",
1754
2094
  "having",
1755
- "i",
1756
- "me",
1757
- "my",
1758
- "we",
1759
- "our",
1760
- "you",
1761
- "your",
1762
- "he",
1763
- "she",
1764
- "they",
2095
+ // Common imperative verbs that carry no search value
1765
2096
  "find",
1766
2097
  "show",
1767
2098
  "get",
1768
- "tell"
2099
+ "tell",
2100
+ "look",
2101
+ "give",
2102
+ "list",
2103
+ "explain",
2104
+ // Misc filler
2105
+ "all",
2106
+ "any",
2107
+ "some",
2108
+ "each",
2109
+ "every",
2110
+ "much",
2111
+ "many",
2112
+ "also",
2113
+ "just",
2114
+ "like",
2115
+ "then",
2116
+ "there",
2117
+ "here",
2118
+ "very",
2119
+ "really",
2120
+ "use",
2121
+ "used",
2122
+ "using"
1769
2123
  ]);
2124
+ var CODE_IDENT_RE = /^(?:[a-z]+(?:[A-Z][a-z]*)+|[A-Z][a-zA-Z]+|[a-z]+(?:_[a-z]+)+|[A-Z]+(?:_[A-Z]+)+)$/;
2125
+ var DOTTED_IDENT_RE = /[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)+/g;
2126
+ var COMMON_STEMS = {
2127
+ authentication: "auth",
2128
+ authorization: "auth",
2129
+ configuration: "config",
2130
+ initialization: "init",
2131
+ initialize: "init",
2132
+ initializing: "init",
2133
+ implementation: "impl",
2134
+ implements: "impl",
2135
+ implementing: "impl",
2136
+ dependency: "dep",
2137
+ dependencies: "dep",
2138
+ middleware: "middleware",
2139
+ validation: "valid",
2140
+ validator: "valid",
2141
+ serialize: "serial",
2142
+ serialization: "serial",
2143
+ deserialize: "deserial",
2144
+ database: "db",
2145
+ logging: "log",
2146
+ logger: "log",
2147
+ testing: "test",
2148
+ handler: "handle",
2149
+ handling: "handle",
2150
+ callback: "callback",
2151
+ subscriber: "subscribe",
2152
+ subscription: "subscribe",
2153
+ rendering: "render",
2154
+ renderer: "render",
2155
+ transformer: "transform",
2156
+ transformation: "transform",
2157
+ connection: "connect",
2158
+ connector: "connect",
2159
+ migration: "migrate",
2160
+ scheduling: "schedule",
2161
+ scheduler: "schedule",
2162
+ parsing: "parse",
2163
+ parser: "parse",
2164
+ routing: "route",
2165
+ router: "route",
2166
+ indexing: "index",
2167
+ indexer: "index"
2168
+ };
2169
+ var STEM_SUFFIXES = [
2170
+ "tion",
2171
+ "sion",
2172
+ "ment",
2173
+ "ness",
2174
+ "ing",
2175
+ "er",
2176
+ "or",
2177
+ "able",
2178
+ "ible",
2179
+ "ity",
2180
+ "ous",
2181
+ "ive",
2182
+ "ful",
2183
+ "less",
2184
+ "ly"
2185
+ ];
2186
+ function getStemVariant(term) {
2187
+ const lower = term.toLowerCase();
2188
+ const mapped = COMMON_STEMS[lower];
2189
+ if (mapped && mapped !== lower) return mapped;
2190
+ if (!/^[a-z][a-z0-9_]*$/.test(lower)) return null;
2191
+ for (const suffix of STEM_SUFFIXES) {
2192
+ if (!lower.endsWith(suffix)) continue;
2193
+ const stem = lower.slice(0, -suffix.length);
2194
+ if (stem.length >= 4 && stem !== lower) {
2195
+ return stem;
2196
+ }
2197
+ }
2198
+ return null;
2199
+ }
1770
2200
  function extractSearchTerms(query) {
1771
- const words = query.replace(/[^a-zA-Z0-9_\s]/g, " ").split(/\s+/).filter((w) => w.length >= 2 && !STOP_WORDS.has(w.toLowerCase()));
1772
- if (words.length === 0) {
2201
+ const terms = [];
2202
+ const seen = /* @__PURE__ */ new Set();
2203
+ const addUnique = (term) => {
2204
+ const key = term.toLowerCase();
2205
+ if (!seen.has(key)) {
2206
+ seen.add(key);
2207
+ terms.push(term);
2208
+ }
2209
+ };
2210
+ const addTermAndVariants = (term) => {
2211
+ addUnique(term);
2212
+ const variant = getStemVariant(term);
2213
+ if (variant && variant !== term.toLowerCase()) {
2214
+ addUnique(variant);
2215
+ }
2216
+ };
2217
+ const dottedMatches = query.match(DOTTED_IDENT_RE) ?? [];
2218
+ for (const m of dottedMatches) addTermAndVariants(m);
2219
+ const pathTokens = query.split(/\s+/).filter((t) => t.includes("/"));
2220
+ for (const p of pathTokens) addTermAndVariants(p.replace(/[?!,;]+$/g, ""));
2221
+ const words = query.replace(/[^a-zA-Z0-9_.\s/-]/g, " ").split(/\s+/).filter((w) => w.length >= 2);
2222
+ for (const w of words) {
2223
+ const lower = w.toLowerCase();
2224
+ if (seen.has(lower)) continue;
2225
+ if (STOP_WORDS2.has(lower) && !CODE_IDENT_RE.test(w)) continue;
2226
+ addTermAndVariants(w);
2227
+ }
2228
+ if (terms.length === 0) {
1773
2229
  const allWords = query.replace(/[^a-zA-Z0-9_\s]/g, " ").split(/\s+/).filter((w) => w.length >= 2);
1774
- return allWords.sort((a, b) => b.length - a.length)[0] ?? query;
2230
+ const longest = allWords.sort((a, b) => b.length - a.length)[0];
2231
+ return longest ?? query;
1775
2232
  }
1776
- return words.join(" ");
2233
+ return terms.join(" ");
1777
2234
  }
1778
2235
  var VALID_STRATEGIES = /* @__PURE__ */ new Set([
1779
2236
  "vector",
@@ -1783,17 +2240,42 @@ var VALID_STRATEGIES = /* @__PURE__ */ new Set([
1783
2240
  "dependency"
1784
2241
  ]);
1785
2242
  function buildFallbackPlan(query) {
1786
- const keywords = extractSearchTerms(query);
1787
- const strategies = [
1788
- { strategy: "fts", query: keywords, weight: 0.8, reason: "Full-text keyword search" },
1789
- { strategy: "ast", query: keywords, weight: 0.9, reason: "Structural symbol search" },
1790
- { strategy: "path", query: keywords, weight: 0.7, reason: "Path keyword search" }
1791
- ];
2243
+ const strategies = buildFallbackStrategies(query);
1792
2244
  return {
1793
2245
  interpretation: `Searching for: ${query}`,
1794
2246
  strategies
1795
2247
  };
1796
2248
  }
2249
+ function buildFallbackStrategies(query) {
2250
+ const keywords = extractSearchTerms(query);
2251
+ const { multipliers } = classifyQuery(query);
2252
+ return [
2253
+ {
2254
+ strategy: "vector",
2255
+ query,
2256
+ weight: 1 * multipliers.vector,
2257
+ reason: "Semantic search over natural language intent"
2258
+ },
2259
+ {
2260
+ strategy: "fts",
2261
+ query: keywords,
2262
+ weight: 0.8 * multipliers.fts,
2263
+ reason: "Full-text keyword search"
2264
+ },
2265
+ {
2266
+ strategy: "ast",
2267
+ query: keywords,
2268
+ weight: 0.9 * multipliers.ast,
2269
+ reason: "Structural symbol search"
2270
+ },
2271
+ {
2272
+ strategy: "path",
2273
+ query: keywords,
2274
+ weight: 0.7 * multipliers.path,
2275
+ reason: "Path keyword search"
2276
+ }
2277
+ ];
2278
+ }
1797
2279
  function parseSearchPlan(raw, query) {
1798
2280
  const jsonMatch = raw.match(/\{[\s\S]*\}/);
1799
2281
  if (!jsonMatch) return buildFallbackPlan(query);
@@ -1873,8 +2355,8 @@ async function steer(provider, query, limit, searchExecutor) {
1873
2355
  }
1874
2356
 
1875
2357
  // src/cli/commands/init.ts
1876
- import fs5 from "fs";
1877
- import path4 from "path";
2358
+ import fs6 from "fs";
2359
+ import path5 from "path";
1878
2360
 
1879
2361
  // src/utils/errors.ts
1880
2362
  var ErrorCode = {
@@ -1962,33 +2444,79 @@ function createLogger(options) {
1962
2444
  };
1963
2445
  }
1964
2446
 
2447
+ // src/cli/commands/config.ts
2448
+ import fs5 from "fs";
2449
+ import path4 from "path";
2450
+ var DEFAULT_CONFIG = {
2451
+ embedder: {
2452
+ provider: "local",
2453
+ model: "Xenova/all-MiniLM-L6-v2",
2454
+ dimensions: 384
2455
+ },
2456
+ search: {
2457
+ defaultLimit: 10,
2458
+ strategies: ["vector", "fts", "ast", "path"],
2459
+ weights: { vector: 1, fts: 0.8, ast: 0.9, path: 0.7, dependency: 0.6 }
2460
+ },
2461
+ watch: {
2462
+ debounceMs: 500,
2463
+ ignored: []
2464
+ },
2465
+ llm: {
2466
+ provider: null,
2467
+ model: null
2468
+ }
2469
+ };
2470
+ var VALID_EMBEDDER_PROVIDERS = /* @__PURE__ */ new Set(["local", "voyage", "openai"]);
2471
+ var VALID_LLM_PROVIDERS = /* @__PURE__ */ new Set(["gemini", "openai", "anthropic"]);
2472
+ var VALIDATION_RULES = {
2473
+ "embedder.provider": {
2474
+ validate: (v) => typeof v === "string" && VALID_EMBEDDER_PROVIDERS.has(v),
2475
+ message: `Must be one of: ${[...VALID_EMBEDDER_PROVIDERS].join(", ")}`
2476
+ },
2477
+ "embedder.dimensions": {
2478
+ validate: (v) => typeof v === "number" && v > 0 && Number.isInteger(v),
2479
+ message: "Must be a positive integer"
2480
+ },
2481
+ "search.defaultLimit": {
2482
+ validate: (v) => typeof v === "number" && v > 0 && Number.isInteger(v),
2483
+ message: "Must be a positive integer"
2484
+ },
2485
+ "watch.debounceMs": {
2486
+ validate: (v) => typeof v === "number" && v >= 0 && Number.isInteger(v),
2487
+ message: "Must be a non-negative integer"
2488
+ },
2489
+ "llm.provider": {
2490
+ validate: (v) => v === null || typeof v === "string" && VALID_LLM_PROVIDERS.has(v),
2491
+ message: `Must be null or one of: ${[...VALID_LLM_PROVIDERS].join(", ")}`
2492
+ }
2493
+ };
2494
+
1965
2495
  // src/cli/commands/init.ts
1966
2496
  var CTX_DIR = ".ctx";
1967
2497
  var DB_FILENAME = "index.db";
1968
2498
  var CONFIG_FILENAME = "config.json";
1969
2499
  var GITIGNORE_ENTRY = ".ctx/";
1970
2500
  function ensureGitignore(projectRoot) {
1971
- const gitignorePath = path4.join(projectRoot, ".gitignore");
1972
- if (fs5.existsSync(gitignorePath)) {
1973
- const content = fs5.readFileSync(gitignorePath, "utf-8");
2501
+ const gitignorePath = path5.join(projectRoot, ".gitignore");
2502
+ if (fs6.existsSync(gitignorePath)) {
2503
+ const content = fs6.readFileSync(gitignorePath, "utf-8");
1974
2504
  if (content.includes(GITIGNORE_ENTRY)) return;
1975
2505
  const suffix = content.endsWith("\n") ? "" : "\n";
1976
- fs5.writeFileSync(gitignorePath, `${content}${suffix}${GITIGNORE_ENTRY}
2506
+ fs6.writeFileSync(gitignorePath, `${content}${suffix}${GITIGNORE_ENTRY}
1977
2507
  `);
1978
2508
  } else {
1979
- fs5.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}
2509
+ fs6.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}
1980
2510
  `);
1981
2511
  }
1982
2512
  }
1983
2513
  function ensureConfig(ctxDir) {
1984
- const configPath = path4.join(ctxDir, CONFIG_FILENAME);
1985
- if (fs5.existsSync(configPath)) return;
1986
- const config = {
1987
- version: 1,
1988
- dimensions: 384,
1989
- model: "all-MiniLM-L6-v2"
1990
- };
1991
- fs5.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
2514
+ const configPath = path5.join(ctxDir, CONFIG_FILENAME);
2515
+ if (fs6.existsSync(configPath)) return;
2516
+ fs6.writeFileSync(
2517
+ configPath,
2518
+ JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n"
2519
+ );
1992
2520
  }
1993
2521
  function formatDuration(ms) {
1994
2522
  if (ms < 1e3) return `${Math.round(ms)}ms`;
@@ -2005,14 +2533,14 @@ function formatLanguageSummary(counts) {
2005
2533
  }
2006
2534
  async function runInit(projectPath, options = {}) {
2007
2535
  const log = options.log ?? console.log;
2008
- const absoluteRoot = path4.resolve(projectPath);
2536
+ const absoluteRoot = path5.resolve(projectPath);
2009
2537
  const start = performance.now();
2010
2538
  log(`Indexing ${absoluteRoot}...`);
2011
- const ctxDir = path4.join(absoluteRoot, CTX_DIR);
2012
- if (!fs5.existsSync(ctxDir)) fs5.mkdirSync(ctxDir, { recursive: true });
2539
+ const ctxDir = path5.join(absoluteRoot, CTX_DIR);
2540
+ if (!fs6.existsSync(ctxDir)) fs6.mkdirSync(ctxDir, { recursive: true });
2013
2541
  ensureGitignore(absoluteRoot);
2014
2542
  ensureConfig(ctxDir);
2015
- const dbPath = path4.join(ctxDir, DB_FILENAME);
2543
+ const dbPath = path5.join(ctxDir, DB_FILENAME);
2016
2544
  const db = createDatabase(dbPath);
2017
2545
  try {
2018
2546
  const discovered = await discoverFiles({
@@ -2117,7 +2645,7 @@ async function runInit(projectPath, options = {}) {
2117
2645
  vectorsCreated = vectors.length;
2118
2646
  }
2119
2647
  const durationMs = performance.now() - start;
2120
- const dbSize = fs5.existsSync(dbPath) ? fs5.statSync(dbPath).size : 0;
2648
+ const dbSize = fs6.existsSync(dbPath) ? fs6.statSync(dbPath).size : 0;
2121
2649
  log("");
2122
2650
  log(`\u2713 Indexed in ${formatDuration(durationMs)}`);
2123
2651
  log(
@@ -2144,8 +2672,8 @@ async function createEmbedder() {
2144
2672
  }
2145
2673
 
2146
2674
  // src/cli/commands/query.ts
2147
- import fs6 from "fs";
2148
- import path5 from "path";
2675
+ import fs7 from "fs";
2676
+ import path6 from "path";
2149
2677
  var CTX_DIR2 = ".ctx";
2150
2678
  var DB_FILENAME2 = "index.db";
2151
2679
  var SNIPPET_MAX_LENGTH = 200;
@@ -2156,6 +2684,20 @@ var STRATEGY_WEIGHTS = {
2156
2684
  path: 0.7,
2157
2685
  dependency: 0.6
2158
2686
  };
2687
+ function getEffectiveStrategyWeights(query) {
2688
+ const { multipliers } = classifyQuery(query);
2689
+ return {
2690
+ vector: STRATEGY_WEIGHTS.vector * multipliers.vector,
2691
+ fts: STRATEGY_WEIGHTS.fts * multipliers.fts,
2692
+ ast: STRATEGY_WEIGHTS.ast * multipliers.ast,
2693
+ path: STRATEGY_WEIGHTS.path * multipliers.path,
2694
+ dependency: STRATEGY_WEIGHTS.dependency * multipliers.dependency
2695
+ };
2696
+ }
2697
+ function normalizeLimit(limit) {
2698
+ if (!Number.isFinite(limit)) return 0;
2699
+ return Math.max(0, Math.trunc(limit));
2700
+ }
2159
2701
  function truncateSnippet(text) {
2160
2702
  const oneLine = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
2161
2703
  if (oneLine.length <= SNIPPET_MAX_LENGTH) return oneLine;
@@ -2195,13 +2737,11 @@ function extractSymbolNames(query) {
2195
2737
  function isPathLike(query) {
2196
2738
  return query.includes("/") || query.includes("*") || query.includes(".");
2197
2739
  }
2198
- function extractPathBoostTerms(query) {
2199
- return query.split(/\s+/).map((t) => t.trim()).filter((t) => t.length >= 2);
2200
- }
2201
2740
  async function runQuery(projectPath, query, options) {
2202
- const absoluteRoot = path5.resolve(projectPath);
2203
- const dbPath = path5.join(absoluteRoot, CTX_DIR2, DB_FILENAME2);
2204
- if (!fs6.existsSync(dbPath)) {
2741
+ const limit = normalizeLimit(options.limit);
2742
+ const absoluteRoot = path6.resolve(projectPath);
2743
+ const dbPath = path6.join(absoluteRoot, CTX_DIR2, DB_FILENAME2);
2744
+ if (!fs7.existsSync(dbPath)) {
2205
2745
  throw new KontextError(
2206
2746
  `Project not initialized. Run "ctx init" first. (${CTX_DIR2}/${DB_FILENAME2} not found)`,
2207
2747
  ErrorCode.NOT_INITIALIZED
@@ -2210,9 +2750,9 @@ async function runQuery(projectPath, query, options) {
2210
2750
  const start = performance.now();
2211
2751
  const db = createDatabase(dbPath);
2212
2752
  try {
2213
- const strategyResults = await runStrategies(db, query, options);
2753
+ const strategyResults = await runStrategies(db, query, { ...options, limit });
2214
2754
  const pathBoostTerms = extractPathBoostTerms(query);
2215
- const fused = fusionMergeWithPathBoost(strategyResults, options.limit, pathBoostTerms);
2755
+ const fused = fusionMergeWithPathBoost(strategyResults, limit, pathBoostTerms);
2216
2756
  const outputResults = fused.map(toOutputResult);
2217
2757
  const searchTimeMs = Math.round(performance.now() - start);
2218
2758
  const text = options.format === "text" ? formatTextOutput(query, outputResults) : void 0;
@@ -2234,8 +2774,9 @@ async function runStrategies(db, query, options) {
2234
2774
  const results = [];
2235
2775
  const filters = options.language ? { language: options.language } : void 0;
2236
2776
  const limit = options.limit * 3;
2777
+ const effectiveWeights = getEffectiveStrategyWeights(query);
2237
2778
  for (const strategy of options.strategies) {
2238
- const weight = STRATEGY_WEIGHTS[strategy];
2779
+ const weight = effectiveWeights[strategy];
2239
2780
  const searchResults = await executeStrategy(
2240
2781
  db,
2241
2782
  strategy,
@@ -2292,12 +2833,16 @@ async function loadEmbedder() {
2292
2833
  }
2293
2834
 
2294
2835
  // src/cli/commands/ask.ts
2295
- import fs7 from "fs";
2296
- import path6 from "path";
2836
+ import fs8 from "fs";
2837
+ import path7 from "path";
2297
2838
  var CTX_DIR3 = ".ctx";
2298
2839
  var DB_FILENAME3 = "index.db";
2299
2840
  var SNIPPET_MAX_LENGTH2 = 200;
2300
2841
  var FALLBACK_NOTICE = "No LLM provider configured. Set CTX_GEMINI_KEY, CTX_OPENAI_KEY, or CTX_ANTHROPIC_KEY. Running basic search instead.";
2842
+ function normalizeLimit2(limit) {
2843
+ if (!Number.isFinite(limit)) return 0;
2844
+ return Math.max(0, Math.trunc(limit));
2845
+ }
2301
2846
  function truncateSnippet2(text) {
2302
2847
  const oneLine = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
2303
2848
  if (oneLine.length <= SNIPPET_MAX_LENGTH2) return oneLine;
@@ -2350,7 +2895,8 @@ function formatTextOutput2(output) {
2350
2895
  );
2351
2896
  return lines.join("\n");
2352
2897
  }
2353
- function createSearchExecutor(db) {
2898
+ function createSearchExecutor(db, query) {
2899
+ const pathBoostTerms = extractPathBoostTerms(query);
2354
2900
  return async (strategies, limit) => {
2355
2901
  const strategyResults = [];
2356
2902
  const fetchLimit = limit * 3;
@@ -2364,7 +2910,7 @@ function createSearchExecutor(db) {
2364
2910
  });
2365
2911
  }
2366
2912
  }
2367
- return fusionMerge(strategyResults, limit);
2913
+ return fusionMergeWithPathBoost(strategyResults, limit, pathBoostTerms);
2368
2914
  };
2369
2915
  }
2370
2916
  function extractSymbolNames2(query) {
@@ -2412,13 +2958,8 @@ async function loadEmbedder2() {
2412
2958
  return embedderInstance2;
2413
2959
  }
2414
2960
  async function fallbackSearch(db, query, limit) {
2415
- const executor = createSearchExecutor(db);
2416
- const keywords = extractSearchTerms(query);
2417
- const fallbackStrategies = [
2418
- { strategy: "fts", query: keywords, weight: 0.8, reason: "fallback keyword search" },
2419
- { strategy: "ast", query: keywords, weight: 0.9, reason: "fallback structural search" },
2420
- { strategy: "path", query: keywords, weight: 0.7, reason: "fallback path search" }
2421
- ];
2961
+ const executor = createSearchExecutor(db, query);
2962
+ const fallbackStrategies = buildFallbackStrategies(query);
2422
2963
  const results = await executor(fallbackStrategies, limit);
2423
2964
  return {
2424
2965
  query,
@@ -2435,9 +2976,10 @@ async function fallbackSearch(db, query, limit) {
2435
2976
  };
2436
2977
  }
2437
2978
  async function runAsk(projectPath, query, options) {
2438
- const absoluteRoot = path6.resolve(projectPath);
2439
- const dbPath = path6.join(absoluteRoot, CTX_DIR3, DB_FILENAME3);
2440
- if (!fs7.existsSync(dbPath)) {
2979
+ const limit = normalizeLimit2(options.limit);
2980
+ const absoluteRoot = path7.resolve(projectPath);
2981
+ const dbPath = path7.join(absoluteRoot, CTX_DIR3, DB_FILENAME3);
2982
+ if (!fs8.existsSync(dbPath)) {
2441
2983
  throw new KontextError(
2442
2984
  `Project not initialized. Run "ctx init" first. (${CTX_DIR3}/${DB_FILENAME3} not found)`,
2443
2985
  ErrorCode.NOT_INITIALIZED
@@ -2447,25 +2989,25 @@ async function runAsk(projectPath, query, options) {
2447
2989
  try {
2448
2990
  const provider = options.provider ?? null;
2449
2991
  if (!provider) {
2450
- const output = await fallbackSearch(db, query, options.limit);
2992
+ const output = await fallbackSearch(db, query, limit);
2451
2993
  output.warning = FALLBACK_NOTICE;
2452
2994
  if (options.format === "text") {
2453
2995
  output.text = formatTextOutput2(output);
2454
2996
  }
2455
2997
  return output;
2456
2998
  }
2457
- const executor = createSearchExecutor(db);
2999
+ const executor = createSearchExecutor(db, query);
2458
3000
  if (options.noExplain) {
2459
- return await runNoExplain(provider, query, options, executor);
3001
+ return await runNoExplain(provider, query, limit, options, executor);
2460
3002
  }
2461
- return await runWithSteering(provider, query, options, executor);
3003
+ return await runWithSteering(provider, query, limit, options, executor);
2462
3004
  } finally {
2463
3005
  db.close();
2464
3006
  }
2465
3007
  }
2466
- async function runNoExplain(provider, query, options, executor) {
3008
+ async function runNoExplain(provider, query, limit, options, executor) {
2467
3009
  const plan = await planSearch(provider, query);
2468
- const results = await executor(plan.strategies, options.limit);
3010
+ const results = await executor(plan.strategies, limit);
2469
3011
  const output = {
2470
3012
  query,
2471
3013
  interpretation: plan.interpretation,
@@ -2483,8 +3025,8 @@ async function runNoExplain(provider, query, options, executor) {
2483
3025
  }
2484
3026
  return output;
2485
3027
  }
2486
- async function runWithSteering(provider, query, options, executor) {
2487
- const result = await steer(provider, query, options.limit, executor);
3028
+ async function runWithSteering(provider, query, limit, options, executor) {
3029
+ const result = await steer(provider, query, limit, executor);
2488
3030
  const output = {
2489
3031
  query,
2490
3032
  interpretation: result.interpretation,
@@ -2504,8 +3046,8 @@ async function runWithSteering(provider, query, options, executor) {
2504
3046
  }
2505
3047
 
2506
3048
  // src/cli/commands/status.ts
2507
- import fs8 from "fs";
2508
- import path7 from "path";
3049
+ import fs9 from "fs";
3050
+ import path8 from "path";
2509
3051
  var CTX_DIR4 = ".ctx";
2510
3052
  var DB_FILENAME4 = "index.db";
2511
3053
  var CONFIG_FILENAME2 = "config.json";
@@ -2524,14 +3066,15 @@ function capitalize(s) {
2524
3066
  return s.charAt(0).toUpperCase() + s.slice(1);
2525
3067
  }
2526
3068
  function readConfig(ctxDir) {
2527
- const configPath = path7.join(ctxDir, CONFIG_FILENAME2);
2528
- if (!fs8.existsSync(configPath)) return null;
3069
+ const configPath = path8.join(ctxDir, CONFIG_FILENAME2);
3070
+ if (!fs9.existsSync(configPath)) return null;
2529
3071
  try {
2530
- const raw = fs8.readFileSync(configPath, "utf-8");
3072
+ const raw = fs9.readFileSync(configPath, "utf-8");
2531
3073
  const parsed = JSON.parse(raw);
3074
+ const embedder = parsed.embedder;
2532
3075
  return {
2533
- model: parsed.model ?? "unknown",
2534
- dimensions: parsed.dimensions ?? 0
3076
+ model: embedder?.model ?? parsed.model ?? "unknown",
3077
+ dimensions: embedder?.dimensions ?? parsed.dimensions ?? 0
2535
3078
  };
2536
3079
  } catch {
2537
3080
  return null;
@@ -2580,10 +3123,10 @@ function formatStatus(projectPath, output) {
2580
3123
  return lines.join("\n");
2581
3124
  }
2582
3125
  async function runStatus(projectPath) {
2583
- const absoluteRoot = path7.resolve(projectPath);
2584
- const ctxDir = path7.join(absoluteRoot, CTX_DIR4);
2585
- const dbPath = path7.join(ctxDir, DB_FILENAME4);
2586
- if (!fs8.existsSync(dbPath)) {
3126
+ const absoluteRoot = path8.resolve(projectPath);
3127
+ const ctxDir = path8.join(absoluteRoot, CTX_DIR4);
3128
+ const dbPath = path8.join(ctxDir, DB_FILENAME4);
3129
+ if (!fs9.existsSync(dbPath)) {
2587
3130
  const output = {
2588
3131
  initialized: false,
2589
3132
  fileCount: 0,
@@ -2605,7 +3148,7 @@ async function runStatus(projectPath) {
2605
3148
  const languages = db.getLanguageBreakdown();
2606
3149
  const lastIndexed = db.getLastIndexed();
2607
3150
  const config = readConfig(ctxDir);
2608
- const dbSizeBytes = fs8.statSync(dbPath).size;
3151
+ const dbSizeBytes = fs9.statSync(dbPath).size;
2609
3152
  const output = {
2610
3153
  initialized: true,
2611
3154
  fileCount,