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/README.md +123 -83
- package/dist/cli/index.js +877 -374
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +665 -122
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
|
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/
|
|
1681
|
-
var PLAN_SYSTEM_PROMPT = `You are a code
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
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
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
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
|
-
|
|
1697
|
-
|
|
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
|
-
|
|
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
|
|
1772
|
-
|
|
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
|
-
|
|
2230
|
+
const longest = allWords.sort((a, b) => b.length - a.length)[0];
|
|
2231
|
+
return longest ?? query;
|
|
1775
2232
|
}
|
|
1776
|
-
return
|
|
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
|
|
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
|
|
1877
|
-
import
|
|
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 =
|
|
1972
|
-
if (
|
|
1973
|
-
const content =
|
|
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
|
-
|
|
2506
|
+
fs6.writeFileSync(gitignorePath, `${content}${suffix}${GITIGNORE_ENTRY}
|
|
1977
2507
|
`);
|
|
1978
2508
|
} else {
|
|
1979
|
-
|
|
2509
|
+
fs6.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}
|
|
1980
2510
|
`);
|
|
1981
2511
|
}
|
|
1982
2512
|
}
|
|
1983
2513
|
function ensureConfig(ctxDir) {
|
|
1984
|
-
const configPath =
|
|
1985
|
-
if (
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
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 =
|
|
2536
|
+
const absoluteRoot = path5.resolve(projectPath);
|
|
2009
2537
|
const start = performance.now();
|
|
2010
2538
|
log(`Indexing ${absoluteRoot}...`);
|
|
2011
|
-
const ctxDir =
|
|
2012
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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
|
|
2148
|
-
import
|
|
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
|
|
2203
|
-
const
|
|
2204
|
-
|
|
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,
|
|
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 =
|
|
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
|
|
2296
|
-
import
|
|
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
|
|
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
|
|
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
|
|
2439
|
-
const
|
|
2440
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
2508
|
-
import
|
|
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 =
|
|
2528
|
-
if (!
|
|
3069
|
+
const configPath = path8.join(ctxDir, CONFIG_FILENAME2);
|
|
3070
|
+
if (!fs9.existsSync(configPath)) return null;
|
|
2529
3071
|
try {
|
|
2530
|
-
const raw =
|
|
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 =
|
|
2584
|
-
const ctxDir =
|
|
2585
|
-
const dbPath =
|
|
2586
|
-
if (!
|
|
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 =
|
|
3151
|
+
const dbSizeBytes = fs9.statSync(dbPath).size;
|
|
2609
3152
|
const output = {
|
|
2610
3153
|
initialized: true,
|
|
2611
3154
|
fileCount,
|