sverklo 0.2.12 → 0.2.14
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 +102 -13
- package/dist/bin/sverklo.js +69 -1
- package/dist/bin/sverklo.js.map +1 -1
- package/dist/src/audit-prompt.d.ts +2 -0
- package/dist/src/audit-prompt.js +91 -0
- package/dist/src/audit-prompt.js.map +1 -0
- package/dist/src/doctor.js +55 -0
- package/dist/src/doctor.js.map +1 -1
- package/dist/src/indexer/embedding-providers.d.ts +34 -0
- package/dist/src/indexer/embedding-providers.js +215 -0
- package/dist/src/indexer/embedding-providers.js.map +1 -0
- package/dist/src/indexer/embedding-providers.test.d.ts +1 -0
- package/dist/src/indexer/embedding-providers.test.js +83 -0
- package/dist/src/indexer/embedding-providers.test.js.map +1 -0
- package/dist/src/indexer/indexer-freshness.test.d.ts +1 -0
- package/dist/src/indexer/indexer-freshness.test.js +109 -0
- package/dist/src/indexer/indexer-freshness.test.js.map +1 -0
- package/dist/src/indexer/indexer-provider-integration.test.d.ts +1 -0
- package/dist/src/indexer/indexer-provider-integration.test.js +142 -0
- package/dist/src/indexer/indexer-provider-integration.test.js.map +1 -0
- package/dist/src/indexer/indexer.d.ts +44 -0
- package/dist/src/indexer/indexer.js +153 -10
- package/dist/src/indexer/indexer.js.map +1 -1
- package/dist/src/indexer/migration.test.d.ts +1 -0
- package/dist/src/indexer/migration.test.js +144 -0
- package/dist/src/indexer/migration.test.js.map +1 -0
- package/dist/src/indexer/symbol-extractor.js +17 -5
- package/dist/src/indexer/symbol-extractor.js.map +1 -1
- package/dist/src/indexer/symbol-extractor.test.d.ts +1 -0
- package/dist/src/indexer/symbol-extractor.test.js +88 -0
- package/dist/src/indexer/symbol-extractor.test.js.map +1 -0
- package/dist/src/indexer/watcher.js +4 -0
- package/dist/src/indexer/watcher.js.map +1 -1
- package/dist/src/memory/journal.d.ts +62 -0
- package/dist/src/memory/journal.js +136 -0
- package/dist/src/memory/journal.js.map +1 -0
- package/dist/src/memory/journal.test.d.ts +1 -0
- package/dist/src/memory/journal.test.js +98 -0
- package/dist/src/memory/journal.test.js.map +1 -0
- package/dist/src/modes.d.ts +16 -0
- package/dist/src/modes.js +104 -0
- package/dist/src/modes.js.map +1 -0
- package/dist/src/modes.test.d.ts +1 -0
- package/dist/src/modes.test.js +48 -0
- package/dist/src/modes.test.js.map +1 -0
- package/dist/src/search/hybrid-search.d.ts +22 -0
- package/dist/src/search/hybrid-search.js +169 -7
- package/dist/src/search/hybrid-search.js.map +1 -1
- package/dist/src/search/hybrid-search.test.d.ts +1 -0
- package/dist/src/search/hybrid-search.test.js +112 -0
- package/dist/src/search/hybrid-search.test.js.map +1 -0
- package/dist/src/search/token-budget.d.ts +1 -0
- package/dist/src/search/token-budget.js +34 -6
- package/dist/src/search/token-budget.js.map +1 -1
- package/dist/src/server/mcp-server.js +32 -25
- package/dist/src/server/mcp-server.js.map +1 -1
- package/dist/src/server/tool-overrides.d.ts +16 -0
- package/dist/src/server/tool-overrides.js +102 -0
- package/dist/src/server/tool-overrides.js.map +1 -0
- package/dist/src/server/tool-overrides.test.d.ts +1 -0
- package/dist/src/server/tool-overrides.test.js +124 -0
- package/dist/src/server/tool-overrides.test.js.map +1 -0
- package/dist/src/server/tools/context-budget.test.d.ts +1 -0
- package/dist/src/server/tools/context-budget.test.js +150 -0
- package/dist/src/server/tools/context-budget.test.js.map +1 -0
- package/dist/src/server/tools/context.d.ts +11 -1
- package/dist/src/server/tools/context.js +202 -8
- package/dist/src/server/tools/context.js.map +1 -1
- package/dist/src/server/tools/diff-heuristics.d.ts +28 -0
- package/dist/src/server/tools/diff-heuristics.js +213 -0
- package/dist/src/server/tools/diff-heuristics.js.map +1 -0
- package/dist/src/server/tools/diff-heuristics.test.d.ts +1 -0
- package/dist/src/server/tools/diff-heuristics.test.js +151 -0
- package/dist/src/server/tools/diff-heuristics.test.js.map +1 -0
- package/dist/src/server/tools/find-references.d.ts +4 -0
- package/dist/src/server/tools/find-references.js +46 -6
- package/dist/src/server/tools/find-references.js.map +1 -1
- package/dist/src/server/tools/find-references.test.d.ts +1 -0
- package/dist/src/server/tools/find-references.test.js +98 -0
- package/dist/src/server/tools/find-references.test.js.map +1 -0
- package/dist/src/server/tools/forget.js +3 -0
- package/dist/src/server/tools/forget.js.map +1 -1
- package/dist/src/server/tools/index-status.js +16 -0
- package/dist/src/server/tools/index-status.js.map +1 -1
- package/dist/src/server/tools/lookup.js +21 -13
- package/dist/src/server/tools/lookup.js.map +1 -1
- package/dist/src/server/tools/lookup.test.d.ts +1 -0
- package/dist/src/server/tools/lookup.test.js +110 -0
- package/dist/src/server/tools/lookup.test.js.map +1 -0
- package/dist/src/server/tools/recall.d.ts +5 -1
- package/dist/src/server/tools/recall.js +68 -6
- package/dist/src/server/tools/recall.js.map +1 -1
- package/dist/src/server/tools/recall.test.d.ts +1 -0
- package/dist/src/server/tools/recall.test.js +116 -0
- package/dist/src/server/tools/recall.test.js.map +1 -0
- package/dist/src/server/tools/remember.js +16 -2
- package/dist/src/server/tools/remember.js.map +1 -1
- package/dist/src/server/tools/review-diff.js +31 -0
- package/dist/src/server/tools/review-diff.js.map +1 -1
- package/dist/src/server/tools/search.js +29 -4
- package/dist/src/server/tools/search.js.map +1 -1
- package/dist/src/server/tools/search.test.d.ts +1 -0
- package/dist/src/server/tools/search.test.js +118 -0
- package/dist/src/server/tools/search.test.js.map +1 -0
- package/dist/src/server/tools/tier.js +25 -1
- package/dist/src/server/tools/tier.js.map +1 -1
- package/dist/src/storage/chunk-store.d.ts +12 -0
- package/dist/src/storage/chunk-store.js +16 -0
- package/dist/src/storage/chunk-store.js.map +1 -1
- package/dist/src/storage/chunk-store.test.d.ts +1 -0
- package/dist/src/storage/chunk-store.test.js +69 -0
- package/dist/src/storage/chunk-store.test.js.map +1 -0
- package/dist/src/storage/database.d.ts +13 -0
- package/dist/src/storage/database.js +47 -0
- package/dist/src/storage/database.js.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { findUnguardedStreamCalls, parseUnifiedDiff, runAllHeuristics, } from "./diff-heuristics.js";
|
|
3
|
+
// Regression tests for github.com/sverklo/sverklo/issues/5 — the diff
|
|
4
|
+
// review case that previously missed a class of production risk: a new
|
|
5
|
+
// call site introduced inside a stream pipeline with no enclosing
|
|
6
|
+
// try-catch. The heuristic is a proxy for the formal AST check we
|
|
7
|
+
// eventually want, and these tests lock in its behavior on the shapes
|
|
8
|
+
// we care about most.
|
|
9
|
+
describe("findUnguardedStreamCalls", () => {
|
|
10
|
+
it("flags a new call added inside a .map() without try-catch", () => {
|
|
11
|
+
// Realistic-shaped diff hunk: a production read path gains a new
|
|
12
|
+
// helper call inside its stream pipeline. No try-catch anywhere
|
|
13
|
+
// in the surrounding context — this is the bug we previously missed.
|
|
14
|
+
const diffText = [
|
|
15
|
+
"diff --git a/Service.java b/Service.java",
|
|
16
|
+
"--- a/Service.java",
|
|
17
|
+
"+++ b/Service.java",
|
|
18
|
+
"@@ -10,5 +10,6 @@",
|
|
19
|
+
" public List<Package> getAll() {",
|
|
20
|
+
" return repo.findAll().stream()",
|
|
21
|
+
" .map(p -> {",
|
|
22
|
+
"+ var feat = computeFeatures(p);",
|
|
23
|
+
" return toDto(p);",
|
|
24
|
+
" })",
|
|
25
|
+
" .collect(Collectors.toList());",
|
|
26
|
+
" }",
|
|
27
|
+
].join("\n");
|
|
28
|
+
const hunks = parseUnifiedDiff(diffText);
|
|
29
|
+
expect(hunks.length).toBeGreaterThan(0);
|
|
30
|
+
const findings = findUnguardedStreamCalls(hunks);
|
|
31
|
+
expect(findings.length).toBe(1);
|
|
32
|
+
expect(findings[0].heuristic).toBe("unguarded-stream-call");
|
|
33
|
+
expect(findings[0].file).toBe("Service.java");
|
|
34
|
+
expect(findings[0].snippet).toContain("computeFeatures");
|
|
35
|
+
});
|
|
36
|
+
it("does NOT flag when a try-catch is visible in the hunk context", () => {
|
|
37
|
+
// Same pattern, but the enclosing method catches its own exceptions.
|
|
38
|
+
// The heuristic should back off — not perfect, but no false positive.
|
|
39
|
+
const diffText = [
|
|
40
|
+
"diff --git a/Service.java b/Service.java",
|
|
41
|
+
"--- a/Service.java",
|
|
42
|
+
"+++ b/Service.java",
|
|
43
|
+
"@@ -10,7 +10,8 @@",
|
|
44
|
+
" public List<Package> getAll() {",
|
|
45
|
+
" try {",
|
|
46
|
+
" return repo.findAll().stream()",
|
|
47
|
+
" .map(p -> {",
|
|
48
|
+
"+ var feat = computeFeatures(p);",
|
|
49
|
+
" return toDto(p);",
|
|
50
|
+
" })",
|
|
51
|
+
" .collect(Collectors.toList());",
|
|
52
|
+
" } catch (Exception e) { return List.of(); }",
|
|
53
|
+
" }",
|
|
54
|
+
].join("\n");
|
|
55
|
+
const hunks = parseUnifiedDiff(diffText);
|
|
56
|
+
const findings = findUnguardedStreamCalls(hunks);
|
|
57
|
+
expect(findings.length).toBe(0);
|
|
58
|
+
});
|
|
59
|
+
it("does not flag added lines outside a stream pipeline", () => {
|
|
60
|
+
const diffText = [
|
|
61
|
+
"diff --git a/Util.ts b/Util.ts",
|
|
62
|
+
"--- a/Util.ts",
|
|
63
|
+
"+++ b/Util.ts",
|
|
64
|
+
"@@ -1,5 +1,6 @@",
|
|
65
|
+
" export function loadConfig() {",
|
|
66
|
+
" const raw = readFileSync('config.json');",
|
|
67
|
+
"+ const parsed = JSON.parse(raw);",
|
|
68
|
+
" return raw;",
|
|
69
|
+
" }",
|
|
70
|
+
].join("\n");
|
|
71
|
+
const hunks = parseUnifiedDiff(diffText);
|
|
72
|
+
const findings = findUnguardedStreamCalls(hunks);
|
|
73
|
+
expect(findings.length).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
it("flags at most once per stream block", () => {
|
|
76
|
+
// Ten new calls inside one .map() should produce exactly one
|
|
77
|
+
// finding, not ten. Noise control.
|
|
78
|
+
const diffText = [
|
|
79
|
+
"diff --git a/Batch.ts b/Batch.ts",
|
|
80
|
+
"--- a/Batch.ts",
|
|
81
|
+
"+++ b/Batch.ts",
|
|
82
|
+
"@@ -1,5 +1,14 @@",
|
|
83
|
+
" function processAll(items: Item[]) {",
|
|
84
|
+
" return items.map(it => {",
|
|
85
|
+
"+ const a = transformA(it);",
|
|
86
|
+
"+ const b = transformB(it);",
|
|
87
|
+
"+ const c = transformC(it);",
|
|
88
|
+
"+ const d = transformD(it);",
|
|
89
|
+
" return it;",
|
|
90
|
+
" });",
|
|
91
|
+
" }",
|
|
92
|
+
].join("\n");
|
|
93
|
+
const hunks = parseUnifiedDiff(diffText);
|
|
94
|
+
const findings = findUnguardedStreamCalls(hunks);
|
|
95
|
+
expect(findings.length).toBe(1);
|
|
96
|
+
});
|
|
97
|
+
it("handles .forEach and .flatMap the same way as .map", () => {
|
|
98
|
+
const diffText = [
|
|
99
|
+
"diff --git a/Listener.ts b/Listener.ts",
|
|
100
|
+
"--- a/Listener.ts",
|
|
101
|
+
"+++ b/Listener.ts",
|
|
102
|
+
"@@ -1,4 +1,5 @@",
|
|
103
|
+
" function broadcast(events: Event[]) {",
|
|
104
|
+
" events.forEach(e => {",
|
|
105
|
+
"+ publishToQueue(e);",
|
|
106
|
+
" });",
|
|
107
|
+
" }",
|
|
108
|
+
].join("\n");
|
|
109
|
+
const hunks = parseUnifiedDiff(diffText);
|
|
110
|
+
const findings = findUnguardedStreamCalls(hunks);
|
|
111
|
+
expect(findings.length).toBe(1);
|
|
112
|
+
expect(findings[0].snippet).toContain("publishToQueue");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe("runAllHeuristics", () => {
|
|
116
|
+
it("returns empty array for empty hunks", () => {
|
|
117
|
+
expect(runAllHeuristics([])).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
it("does not throw if a hunk has malformed lines", () => {
|
|
120
|
+
// Sanity: a heuristic must never take down review even on weird input.
|
|
121
|
+
expect(() => runAllHeuristics([
|
|
122
|
+
{
|
|
123
|
+
filePath: "foo.ts",
|
|
124
|
+
oldStart: 1,
|
|
125
|
+
newStart: 1,
|
|
126
|
+
lines: ["not-a-valid-diff-line", "+ok"],
|
|
127
|
+
},
|
|
128
|
+
])).not.toThrow();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("parseUnifiedDiff", () => {
|
|
132
|
+
it("extracts file path and hunk ranges from a standard git diff", () => {
|
|
133
|
+
const text = [
|
|
134
|
+
"diff --git a/src/foo.ts b/src/foo.ts",
|
|
135
|
+
"index abc..def 100644",
|
|
136
|
+
"--- a/src/foo.ts",
|
|
137
|
+
"+++ b/src/foo.ts",
|
|
138
|
+
"@@ -5,3 +5,4 @@",
|
|
139
|
+
" context",
|
|
140
|
+
"+added",
|
|
141
|
+
" context",
|
|
142
|
+
" context",
|
|
143
|
+
].join("\n");
|
|
144
|
+
const hunks = parseUnifiedDiff(text);
|
|
145
|
+
expect(hunks.length).toBe(1);
|
|
146
|
+
expect(hunks[0].filePath).toBe("src/foo.ts");
|
|
147
|
+
expect(hunks[0].oldStart).toBe(5);
|
|
148
|
+
expect(hunks[0].newStart).toBe(5);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
//# sourceMappingURL=diff-heuristics.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-heuristics.test.js","sourceRoot":"","sources":["../../../../src/server/tools/diff-heuristics.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const findReferencesTool = {
|
|
2
2
|
name: "sverklo_refs",
|
|
3
|
-
description: "Find all references to a symbol across the codebase. Shows where a function, class, or type is imported, called, or used.",
|
|
3
|
+
description: "Find all references to a symbol across the codebase. Shows where a function, class, or type is imported, called, or used. Matches on identifier word boundaries by default — `embed` does NOT match `embeddingStore`. Pass `exact: false` to opt into substring matching.",
|
|
4
4
|
inputSchema: {
|
|
5
5
|
type: "object",
|
|
6
6
|
properties: {
|
|
@@ -8,6 +8,10 @@ export const findReferencesTool = {
|
|
|
8
8
|
type: "string",
|
|
9
9
|
description: "Symbol name to find references for",
|
|
10
10
|
},
|
|
11
|
+
exact: {
|
|
12
|
+
type: "boolean",
|
|
13
|
+
description: "When true (default), match on whole-identifier boundaries — `embed` won't match `embeddingStore`. When false, substring-match like the old behavior.",
|
|
14
|
+
},
|
|
11
15
|
token_budget: {
|
|
12
16
|
type: "number",
|
|
13
17
|
description: "Max tokens to return (default: 1500)",
|
|
@@ -16,10 +20,44 @@ export const findReferencesTool = {
|
|
|
16
20
|
required: ["symbol"],
|
|
17
21
|
},
|
|
18
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Build a matcher function that decides whether a line references
|
|
25
|
+
* the symbol. Issue #14: short symbol names like `embed` were matching
|
|
26
|
+
* `embeddingStore` and dozens of other unrelated identifiers via
|
|
27
|
+
* substring, polluting the output. Word-boundary matching is the
|
|
28
|
+
* right default — users can opt out via `exact: false` for the rare
|
|
29
|
+
* case where they genuinely want substring.
|
|
30
|
+
*
|
|
31
|
+
* We escape the symbol before using it in a regex so that users can
|
|
32
|
+
* look up symbols that contain regex metacharacters (e.g. `$scope`,
|
|
33
|
+
* `Foo.bar`) without the query being reinterpreted.
|
|
34
|
+
*/
|
|
35
|
+
function buildSymbolMatcher(symbol, exact) {
|
|
36
|
+
if (!exact) {
|
|
37
|
+
return (line) => line.includes(symbol);
|
|
38
|
+
}
|
|
39
|
+
// \b uses \w = [A-Za-z0-9_], which is what we want for
|
|
40
|
+
// JS/TS/Python/Go/Rust/Java/C/C++/Ruby/PHP identifiers. It's not
|
|
41
|
+
// perfect for Unicode identifiers (Kotlin, Swift can use non-ASCII)
|
|
42
|
+
// but for the supported languages it gives the right answer in
|
|
43
|
+
// every case we tested in the dogfood session.
|
|
44
|
+
const escaped = symbol.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
45
|
+
const re = new RegExp(`\\b${escaped}\\b`);
|
|
46
|
+
return (line) => re.test(line);
|
|
47
|
+
}
|
|
19
48
|
export function handleFindReferences(indexer, args) {
|
|
20
49
|
const symbol = args.symbol;
|
|
50
|
+
if (typeof symbol !== "string" || symbol.trim() === "") {
|
|
51
|
+
return 'Error: `symbol` is required. Usage: sverklo_refs symbol:"MyClass".';
|
|
52
|
+
}
|
|
53
|
+
const exact = args.exact !== false; // default true
|
|
21
54
|
const tokenBudget = args.token_budget || 1500;
|
|
22
|
-
|
|
55
|
+
const matches = buildSymbolMatcher(symbol, exact);
|
|
56
|
+
// Use FTS to find candidate chunks mentioning the symbol at all.
|
|
57
|
+
// FTS still does substring-ish matching on the chunk body, but we
|
|
58
|
+
// re-filter every candidate line against the matcher above before
|
|
59
|
+
// we keep it — so the output respects the exact/word-boundary
|
|
60
|
+
// contract even when FTS hands us noise.
|
|
23
61
|
const ftsResults = indexer.chunkStore.searchFts(symbol, 50);
|
|
24
62
|
const fileCache = new Map();
|
|
25
63
|
for (const f of indexer.fileStore.getAll()) {
|
|
@@ -31,13 +69,15 @@ export function handleFindReferences(indexer, args) {
|
|
|
31
69
|
const file = fileCache.get(chunk.file_id);
|
|
32
70
|
if (!file)
|
|
33
71
|
continue;
|
|
34
|
-
//
|
|
35
|
-
|
|
72
|
+
// Early reject: if the symbol doesn't appear in the chunk at all
|
|
73
|
+
// (under the current matching mode), skip the per-line scan.
|
|
74
|
+
if (!matches(chunk.content))
|
|
36
75
|
continue;
|
|
37
|
-
// Find specific lines
|
|
76
|
+
// Find specific lines that actually contain the symbol as a
|
|
77
|
+
// whole word (or substring, if exact=false).
|
|
38
78
|
const lines = chunk.content.split("\n");
|
|
39
79
|
for (let i = 0; i < lines.length; i++) {
|
|
40
|
-
if (lines[i]
|
|
80
|
+
if (matches(lines[i])) {
|
|
41
81
|
const refs = byFile.get(file.path) || [];
|
|
42
82
|
refs.push({
|
|
43
83
|
line: chunk.start_line + i,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-references.js","sourceRoot":"","sources":["../../../../src/server/tools/find-references.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,IAAI,EAAE,cAAc;IACpB,WAAW,EACT,
|
|
1
|
+
{"version":3,"file":"find-references.js","sourceRoot":"","sources":["../../../../src/server/tools/find-references.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Indexer } from "../../indexer/indexer.js";
|
|
6
|
+
import { getProjectConfig } from "../../utils/config.js";
|
|
7
|
+
import { handleFindReferences } from "./find-references.js";
|
|
8
|
+
// Regression tests for github.com/sverklo/sverklo/issues/14.
|
|
9
|
+
//
|
|
10
|
+
// sverklo_refs used to substring-match the symbol name, so a query
|
|
11
|
+
// for `embed` returned 48 hits that were mostly `embeddingStore`,
|
|
12
|
+
// `embeddingBatch`, `EmbeddingStore` class, etc. — dozens of false
|
|
13
|
+
// positives drowning the 5 real call sites.
|
|
14
|
+
//
|
|
15
|
+
// The fix: word-boundary matching by default, substring opt-in via
|
|
16
|
+
// `exact: false`.
|
|
17
|
+
describe("handleFindReferences — issue #14 regression", () => {
|
|
18
|
+
let tmpRoot;
|
|
19
|
+
let indexer;
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tmpRoot = mkdtempSync(join(tmpdir(), "sverklo-refs-"));
|
|
22
|
+
mkdirSync(join(tmpRoot, "src"), { recursive: true });
|
|
23
|
+
// Seed a codebase that reproduces the original noise pattern:
|
|
24
|
+
// one real identifier `embed` plus several longer identifiers
|
|
25
|
+
// that contain `embed` as a substring. The exact-match mode
|
|
26
|
+
// should return only the real ones.
|
|
27
|
+
writeFileSync(join(tmpRoot, "src", "indexer.ts"), [
|
|
28
|
+
"export class Indexer {",
|
|
29
|
+
" public embeddingStore: unknown;",
|
|
30
|
+
" public embeddingBatch: unknown[] = [];",
|
|
31
|
+
" async run() {",
|
|
32
|
+
" const vectors = await embed(['text']);",
|
|
33
|
+
" this.embeddingStore = vectors;",
|
|
34
|
+
" this.embeddingBatch.push(...vectors);",
|
|
35
|
+
" return embed(['another']);",
|
|
36
|
+
" }",
|
|
37
|
+
"}",
|
|
38
|
+
"declare function embed(texts: string[]): Promise<unknown>;",
|
|
39
|
+
].join("\n"), "utf-8");
|
|
40
|
+
writeFileSync(join(tmpRoot, "src", "storage.ts"), [
|
|
41
|
+
"export class EmbeddingStore {",
|
|
42
|
+
" // stores vectors produced by the indexer",
|
|
43
|
+
" save() {}",
|
|
44
|
+
"}",
|
|
45
|
+
].join("\n"), "utf-8");
|
|
46
|
+
const cfg = getProjectConfig(tmpRoot);
|
|
47
|
+
indexer = new Indexer(cfg);
|
|
48
|
+
await indexer.index();
|
|
49
|
+
});
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
try {
|
|
52
|
+
indexer.close();
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
try {
|
|
56
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
});
|
|
60
|
+
it("default exact mode matches whole identifiers only", () => {
|
|
61
|
+
const out = handleFindReferences(indexer, { symbol: "embed" });
|
|
62
|
+
// The two real calls to embed() must appear
|
|
63
|
+
expect(out).toContain("await embed(['text'])");
|
|
64
|
+
expect(out).toContain("embed(['another'])");
|
|
65
|
+
// But `embeddingStore`, `embeddingBatch`, `EmbeddingStore` must NOT
|
|
66
|
+
// be reported as references to `embed`
|
|
67
|
+
expect(out).not.toContain("embeddingStore: unknown");
|
|
68
|
+
expect(out).not.toContain("embeddingBatch.push");
|
|
69
|
+
expect(out).not.toContain("class EmbeddingStore");
|
|
70
|
+
});
|
|
71
|
+
it("exact: false opts into substring behavior for edge cases", () => {
|
|
72
|
+
const out = handleFindReferences(indexer, { symbol: "embed", exact: false });
|
|
73
|
+
// In substring mode, the longer names do match
|
|
74
|
+
expect(out).toContain("embeddingStore");
|
|
75
|
+
// And the real calls are still there
|
|
76
|
+
expect(out).toContain("embed(['text'])");
|
|
77
|
+
});
|
|
78
|
+
it("rejects missing symbol with a clear error", () => {
|
|
79
|
+
const out = handleFindReferences(indexer, {});
|
|
80
|
+
expect(out).toContain("Error");
|
|
81
|
+
expect(out).toContain("symbol");
|
|
82
|
+
});
|
|
83
|
+
it("does not match inside longer identifiers that share a prefix", () => {
|
|
84
|
+
// `Embedding` has `embed` as a prefix but should not match
|
|
85
|
+
// in exact mode.
|
|
86
|
+
const out = handleFindReferences(indexer, { symbol: "embed" });
|
|
87
|
+
expect(out).not.toContain("EmbeddingStore");
|
|
88
|
+
});
|
|
89
|
+
it("matches exact identifier even when it contains regex metachars", () => {
|
|
90
|
+
// Names with dots / dollar signs / brackets must not break the
|
|
91
|
+
// regex builder.
|
|
92
|
+
const out = handleFindReferences(indexer, { symbol: "$invalid" });
|
|
93
|
+
// Should return "No references" for a non-existent symbol, not
|
|
94
|
+
// throw on regex construction.
|
|
95
|
+
expect(out).toContain("No references found");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
//# sourceMappingURL=find-references.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-references.test.js","sourceRoot":"","sources":["../../../../src/server/tools/find-references.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,6 +20,9 @@ export function handleForget(indexer, args) {
|
|
|
20
20
|
}
|
|
21
21
|
indexer.memoryStore.delete(id);
|
|
22
22
|
indexer.memoryEmbeddingStore.delete(id);
|
|
23
|
+
// Mirror the delete as a tombstone in the JSONL journal so the
|
|
24
|
+
// journal stays replayable. Issue #7.
|
|
25
|
+
indexer.memoryJournal.forget(id);
|
|
23
26
|
return `Deleted memory #${id} (${memory.category}): "${memory.content.slice(0, 80)}${memory.content.length > 80 ? "..." : ""}"`;
|
|
24
27
|
}
|
|
25
28
|
//# sourceMappingURL=forget.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forget.js","sourceRoot":"","sources":["../../../../src/server/tools/forget.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,wBAAwB;IACrC,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,EAAE,EAAE;gBACF,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,2CAA2C;aACzD;SACF;QACD,QAAQ,EAAE,CAAC,IAAI,CAAC;KACjB;CACF,CAAC;AAEF,MAAM,UAAU,YAAY,CAC1B,OAAgB,EAChB,IAA6B;IAE7B,MAAM,EAAE,GAAG,IAAI,CAAC,EAAY,CAAC;IAE7B,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,WAAW,EAAE,aAAa,CAAC;IACpC,CAAC;IAED,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC/B,OAAO,CAAC,oBAAoB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACxC,OAAO,mBAAmB,EAAE,KAAK,MAAM,CAAC,QAAQ,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC;AAClI,CAAC"}
|
|
1
|
+
{"version":3,"file":"forget.js","sourceRoot":"","sources":["../../../../src/server/tools/forget.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,wBAAwB;IACrC,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,EAAE,EAAE;gBACF,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,2CAA2C;aACzD;SACF;QACD,QAAQ,EAAE,CAAC,IAAI,CAAC;KACjB;CACF,CAAC;AAEF,MAAM,UAAU,YAAY,CAC1B,OAAgB,EAChB,IAA6B;IAE7B,MAAM,EAAE,GAAG,IAAI,CAAC,EAAY,CAAC;IAE7B,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,WAAW,EAAE,aAAa,CAAC;IACpC,CAAC;IAED,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC/B,OAAO,CAAC,oBAAoB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACxC,+DAA+D;IAC/D,sCAAsC;IACtC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACjC,OAAO,mBAAmB,EAAE,KAAK,MAAM,CAAC,QAAQ,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC;AAClI,CAAC"}
|
|
@@ -24,6 +24,22 @@ export function handleIndexStatus(indexer) {
|
|
|
24
24
|
parts.push(`- ${status.fileCount} files · ${status.chunkCount} symbols · ${symbolRefCount} references`);
|
|
25
25
|
parts.push(`- Languages: ${status.languages.join(", ") || "none"}`);
|
|
26
26
|
parts.push(`- Status: ${status.indexing ? `indexing (${status.progress?.done}/${status.progress?.total})` : "ready"}`);
|
|
27
|
+
// Embedding provider: read from the live indexer so the displayed
|
|
28
|
+
// provider reflects what was *actually* selected, not what env vars
|
|
29
|
+
// *asked for*. If the user requested openai but the init failed and
|
|
30
|
+
// we fell back to default, this will correctly show default. Issue #9.
|
|
31
|
+
const requestedProvider = (process.env.SVERKLO_EMBEDDING_PROVIDER || "default").toLowerCase();
|
|
32
|
+
const activeProvider = indexer.embeddingProviderName;
|
|
33
|
+
const activeDims = indexer.embeddingDimensions;
|
|
34
|
+
if (requestedProvider !== "default" &&
|
|
35
|
+
requestedProvider !== "bundled" &&
|
|
36
|
+
requestedProvider !== "onnx" &&
|
|
37
|
+
activeProvider === "default") {
|
|
38
|
+
parts.push(`- Embedding provider: ${activeProvider} (${activeDims}d) ⚠️ requested '${requestedProvider}' but fell back — check SVERKLO_DEBUG for details`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
parts.push(`- Embedding provider: ${activeProvider} (${activeDims}d)`);
|
|
42
|
+
}
|
|
27
43
|
// Freshness signal — only meaningful once the index has something to compare
|
|
28
44
|
// against. Skip the disk walk entirely on an empty index to avoid scaring
|
|
29
45
|
// the agent with "everything is dirty" noise during initial bootstrap.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index-status.js","sourceRoot":"","sources":["../../../../src/server/tools/index-status.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,gBAAgB;IACtB,WAAW,EACT,qFAAqF;QACrF,yFAAyF;QACzF,uFAAuF;IACzF,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE,EAAE;KACf;CACF,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,OAAgB;IAChD,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IACnC,MAAM,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;IAC7C,MAAM,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACrD,MAAM,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC;IACrD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;IAEtD,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,yBAAyB;IACzB,KAAK,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IACtC,KAAK,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;IACrC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,sBAAsB;IACtB,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvB,KAAK,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,SAAS,YAAY,MAAM,CAAC,UAAU,cAAc,cAAc,aAAa,CAAC,CAAC;IACxG,KAAK,CAAC,IAAI,CAAC,gBAAgB,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC;IACpE,KAAK,CAAC,IAAI,CAAC,aAAa,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,MAAM,CAAC,QAAQ,EAAE,IAAI,IAAI,MAAM,CAAC,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index-status.js","sourceRoot":"","sources":["../../../../src/server/tools/index-status.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { formatLookup } from "../../search/token-budget.js";
|
|
2
|
+
// Issue #6: on the first call, sverklo_lookup paid a ~1.6s penalty while
|
|
3
|
+
// warming up prepared statements via fileStore.getAll() to build a
|
|
4
|
+
// pagerank-by-file map. The getByNameWithFile JOIN below returns the
|
|
5
|
+
// same shape in a single indexed query, eliminating the full scan.
|
|
2
6
|
export const lookupTool = {
|
|
3
7
|
name: "sverklo_lookup",
|
|
4
8
|
description: "Look up a specific symbol (function, class, type, variable) by name. Returns its full definition, signature, and location.",
|
|
@@ -31,24 +35,28 @@ export const lookupTool = {
|
|
|
31
35
|
},
|
|
32
36
|
};
|
|
33
37
|
export function handleLookup(indexer, args) {
|
|
38
|
+
// Bug A (issue #15 investigation): missing / wrong-named required
|
|
39
|
+
// params previously fell through to a SQL LIKE '%undefined%' and
|
|
40
|
+
// returned "No results found" — indistinguishable from "the symbol
|
|
41
|
+
// doesn't exist" and actively misleading. Fail loud instead so the
|
|
42
|
+
// caller knows it was their mistake, not the index's.
|
|
34
43
|
const symbol = args.symbol;
|
|
44
|
+
if (typeof symbol !== "string" || symbol.trim() === "") {
|
|
45
|
+
return ('Error: `symbol` is required. Usage: sverklo_lookup symbol:"MyClass".\n' +
|
|
46
|
+
"The tool schema names this parameter `symbol`, not `name` — common typo.");
|
|
47
|
+
}
|
|
35
48
|
const type = args.type || "any";
|
|
36
49
|
const tokenBudget = args.token_budget || 1200;
|
|
37
|
-
|
|
50
|
+
// Single JOIN'd query — chunks come back pre-sorted by pagerank DESC
|
|
51
|
+
// and carry the containing file's path, so no full fileStore scan.
|
|
52
|
+
let chunks = indexer.chunkStore.getByNameWithFile(symbol, 20);
|
|
38
53
|
if (type !== "any") {
|
|
39
54
|
chunks = chunks.filter((c) => c.type === type);
|
|
40
55
|
}
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Sort by PageRank of containing file
|
|
47
|
-
chunks.sort((a, b) => {
|
|
48
|
-
const fa = fileCache.get(a.file_id);
|
|
49
|
-
const fb = fileCache.get(b.file_id);
|
|
50
|
-
return (fb?.pagerank || 0) - (fa?.pagerank || 0);
|
|
51
|
-
});
|
|
52
|
-
return formatLookup(chunks, fileCache, tokenBudget);
|
|
56
|
+
// formatLookup only reads filePath / lang off the file map when the
|
|
57
|
+
// chunk itself doesn't carry filePath. Since our JOIN provides it,
|
|
58
|
+
// we can pass an empty map and avoid the scan.
|
|
59
|
+
const emptyFileMap = new Map();
|
|
60
|
+
return formatLookup(chunks, emptyFileMap, tokenBudget);
|
|
53
61
|
}
|
|
54
62
|
//# sourceMappingURL=lookup.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lookup.js","sourceRoot":"","sources":["../../../../src/server/tools/lookup.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAG5D,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,IAAI,EAAE,gBAAgB;IACtB,WAAW,EACT,4HAA4H;IAC9H,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,MAAM,EAAE;gBACN,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,gDAAgD;aAC9D;YACD,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE;oBACJ,UAAU;oBACV,OAAO;oBACP,MAAM;oBACN,WAAW;oBACX,QAAQ;oBACR,UAAU;oBACV,KAAK;iBACN;gBACD,WAAW,EAAE,uBAAuB;aACrC;YACD,YAAY,EAAE;gBACZ,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,sCAAsC;aACpD;SACF;QACD,QAAQ,EAAE,CAAC,QAAQ,CAAC;KACrB;CACF,CAAC;AAEF,MAAM,UAAU,YAAY,CAC1B,OAAgB,EAChB,IAA6B;IAE7B,
|
|
1
|
+
{"version":3,"file":"lookup.js","sourceRoot":"","sources":["../../../../src/server/tools/lookup.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAG5D,yEAAyE;AACzE,mEAAmE;AACnE,qEAAqE;AACrE,mEAAmE;AAEnE,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,IAAI,EAAE,gBAAgB;IACtB,WAAW,EACT,4HAA4H;IAC9H,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,MAAM,EAAE;gBACN,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,gDAAgD;aAC9D;YACD,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE;oBACJ,UAAU;oBACV,OAAO;oBACP,MAAM;oBACN,WAAW;oBACX,QAAQ;oBACR,UAAU;oBACV,KAAK;iBACN;gBACD,WAAW,EAAE,uBAAuB;aACrC;YACD,YAAY,EAAE;gBACZ,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,sCAAsC;aACpD;SACF;QACD,QAAQ,EAAE,CAAC,QAAQ,CAAC;KACrB;CACF,CAAC;AAEF,MAAM,UAAU,YAAY,CAC1B,OAAgB,EAChB,IAA6B;IAE7B,kEAAkE;IAClE,iEAAiE;IACjE,mEAAmE;IACnE,mEAAmE;IACnE,sDAAsD;IACtD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACvD,OAAO,CACL,wEAAwE;YACxE,0EAA0E,CAC3E,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAI,IAAI,CAAC,IAA0B,IAAI,KAAK,CAAC;IACvD,MAAM,WAAW,GAAI,IAAI,CAAC,YAAuB,IAAI,IAAI,CAAC;IAE1D,qEAAqE;IACrE,mEAAmE;IACnE,IAAI,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,iBAAiB,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE9D,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IACjD,CAAC;IAED,oEAAoE;IACpE,mEAAmE;IACnE,+CAA+C;IAC/C,MAAM,YAAY,GAAG,IAAI,GAAG,EAAsB,CAAC;IACnD,OAAO,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Indexer } from "../../indexer/indexer.js";
|
|
6
|
+
import { getProjectConfig } from "../../utils/config.js";
|
|
7
|
+
import { handleLookup } from "./lookup.js";
|
|
8
|
+
// Regression tests for github.com/sverklo/sverklo/issues/15.
|
|
9
|
+
//
|
|
10
|
+
// Two bugs combined to silently lie about whether a symbol existed:
|
|
11
|
+
//
|
|
12
|
+
// A. Missing-required-parameter fallthrough. Calling the tool with
|
|
13
|
+
// the wrong param name (or no param) went straight to a SQL
|
|
14
|
+
// LIKE '%undefined%' which matches nothing, returning "No results
|
|
15
|
+
// found" — indistinguishable from "the symbol does not exist."
|
|
16
|
+
//
|
|
17
|
+
// B. Oversize-chunk silent drop. When a query matched both a small
|
|
18
|
+
// chunk (fits the budget) and a big one (doesn't), the big one
|
|
19
|
+
// was silently dropped. The user saw the small match and
|
|
20
|
+
// concluded the big one didn't exist. On a realistic codebase
|
|
21
|
+
// looking up a major class, this made the tool useless — the
|
|
22
|
+
// real class body is always the biggest match.
|
|
23
|
+
describe("handleLookup — issue #15 regression", () => {
|
|
24
|
+
let tmpRoot;
|
|
25
|
+
let indexer;
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
tmpRoot = mkdtempSync(join(tmpdir(), "sverklo-lookup-"));
|
|
28
|
+
mkdirSync(join(tmpRoot, "src"), { recursive: true });
|
|
29
|
+
mkdirSync(join(tmpRoot, "test"), { recursive: true });
|
|
30
|
+
// A "big" class that exceeds the default token budget (1200).
|
|
31
|
+
// We pad the body with enough meaningful-looking code to push the
|
|
32
|
+
// chunk's estimated token count well above 1200.
|
|
33
|
+
const padLines = Array.from({ length: 200 }, (_, i) => ` method${i}(): void { return; }`).join("\n");
|
|
34
|
+
writeFileSync(join(tmpRoot, "src", "indexer.ts"), ["export class Indexer {", padLines, "}"].join("\n"), "utf-8");
|
|
35
|
+
// A "small" function that matches the same substring but fits
|
|
36
|
+
// the budget. Without the fix, this would hide the Indexer class.
|
|
37
|
+
writeFileSync(join(tmpRoot, "test", "fakes.ts"), "export function fakeIndexerWithCore() { return {}; }\n", "utf-8");
|
|
38
|
+
const cfg = getProjectConfig(tmpRoot);
|
|
39
|
+
indexer = new Indexer(cfg);
|
|
40
|
+
await indexer.index();
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
try {
|
|
44
|
+
indexer.close();
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
try {
|
|
48
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
});
|
|
52
|
+
// ── Bug A ─────────────────────────────────────────────────────
|
|
53
|
+
it("returns a helpful error when `symbol` is missing (bug A)", () => {
|
|
54
|
+
const out = handleLookup(indexer, {});
|
|
55
|
+
expect(out).toContain("Error");
|
|
56
|
+
expect(out).toContain("symbol");
|
|
57
|
+
// The error should specifically mention the common typo so
|
|
58
|
+
// users don't have to read the schema.
|
|
59
|
+
expect(out).toMatch(/name.*typo|symbol.*not.*name/i);
|
|
60
|
+
});
|
|
61
|
+
it("returns a helpful error when `symbol` is an empty string (bug A)", () => {
|
|
62
|
+
const out = handleLookup(indexer, { symbol: "" });
|
|
63
|
+
expect(out).toContain("Error");
|
|
64
|
+
});
|
|
65
|
+
it("returns a helpful error when the wrong parameter name is used (bug A)", () => {
|
|
66
|
+
// Users commonly call this with `name: "Foo"` instead of
|
|
67
|
+
// `symbol: "Foo"`. Before the fix, that silently returned
|
|
68
|
+
// "No results found" — indistinguishable from a real miss.
|
|
69
|
+
const out = handleLookup(indexer, { name: "Indexer" });
|
|
70
|
+
expect(out).toContain("Error");
|
|
71
|
+
expect(out).toMatch(/name.*typo|symbol.*not.*name/i);
|
|
72
|
+
});
|
|
73
|
+
// ── Bug B ─────────────────────────────────────────────────────
|
|
74
|
+
it("surfaces oversize matches as locations, even when smaller matches fit (bug B)", () => {
|
|
75
|
+
// `Indexer` matches both: the big class (oversized) and the small
|
|
76
|
+
// fakeIndexerWithCore (fits). Before the fix, only the small one
|
|
77
|
+
// was shown and the big one was silently dropped.
|
|
78
|
+
const out = handleLookup(indexer, { symbol: "Indexer", token_budget: 1200 });
|
|
79
|
+
// The small match should be visible
|
|
80
|
+
expect(out).toContain("fakeIndexerWithCore");
|
|
81
|
+
// And the big match should ALSO be visible, at minimum as a
|
|
82
|
+
// locations-only entry pointing at indexer.ts.
|
|
83
|
+
expect(out).toContain("indexer.ts");
|
|
84
|
+
expect(out).toMatch(/class: Indexer|Indexer/);
|
|
85
|
+
// The "too large" explanation should be present so the caller
|
|
86
|
+
// knows the body was skipped on purpose, not missing.
|
|
87
|
+
expect(out).toMatch(/too large|exceed|token_budget/i);
|
|
88
|
+
});
|
|
89
|
+
it("surfaces all matches as locations when none fit", () => {
|
|
90
|
+
// With a tiny budget, nothing fits — but we still need the
|
|
91
|
+
// locations list, not "No results found."
|
|
92
|
+
const out = handleLookup(indexer, { symbol: "Indexer", token_budget: 80 });
|
|
93
|
+
expect(out).not.toBe("No results found.");
|
|
94
|
+
expect(out).toMatch(/exceed|token_budget|locations only/i);
|
|
95
|
+
expect(out).toContain("indexer.ts");
|
|
96
|
+
});
|
|
97
|
+
it("with a large budget, returns full bodies for both matches", () => {
|
|
98
|
+
const out = handleLookup(indexer, { symbol: "Indexer", token_budget: 20000 });
|
|
99
|
+
expect(out).toContain("fakeIndexerWithCore");
|
|
100
|
+
expect(out).toContain("class Indexer");
|
|
101
|
+
// With full bodies rendered, the "too large" warning should not
|
|
102
|
+
// appear.
|
|
103
|
+
expect(out).not.toMatch(/too large/i);
|
|
104
|
+
});
|
|
105
|
+
it("still returns 'No results found' when the symbol genuinely does not exist", () => {
|
|
106
|
+
const out = handleLookup(indexer, { symbol: "NonexistentSymbolXYZ" });
|
|
107
|
+
expect(out).toBe("No results found.");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
//# sourceMappingURL=lookup.test.js.map
|