sverklo 0.2.11 → 0.2.13
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 +115 -19
- package/dist/bin/sverklo.js +96 -6
- 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.d.ts +10 -0
- package/dist/src/indexer/indexer.js +46 -4
- package/dist/src/indexer/indexer.js.map +1 -1
- 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 +167 -5
- 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 +109 -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 +1 -1
- 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/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 +12 -13
- package/dist/src/server/tools/lookup.js.map +1 -1
- package/dist/src/server/tools/recall.d.ts +5 -1
- package/dist/src/server/tools/recall.js +66 -4
- 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 +14 -0
- 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/package.json +5 -2
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Structural heuristics that scan the actual diff text for specific classes
|
|
2
|
+
// of risk that symbol-level analysis alone will not catch. Kept as a
|
|
3
|
+
// separate module so new heuristics can be added without touching the
|
|
4
|
+
// main review-diff handler, and so each heuristic is unit-testable in
|
|
5
|
+
// isolation.
|
|
6
|
+
//
|
|
7
|
+
// Heuristics are *heuristics* — they trade some false positives for
|
|
8
|
+
// recall on real-world bugs. Each one attaches a short "why" so a human
|
|
9
|
+
// reviewer can quickly dismiss a false flag.
|
|
10
|
+
//
|
|
11
|
+
// Current heuristics:
|
|
12
|
+
// - unguarded-stream-call: a new call site introduced inside a stream
|
|
13
|
+
// pipeline (.map / .forEach / .flatMap / .filter / .reduce / etc.)
|
|
14
|
+
// where the enclosing function has no visible try-catch. One
|
|
15
|
+
// uncaught RuntimeException on a single element will abort the
|
|
16
|
+
// entire stream — a real outage risk on production read paths.
|
|
17
|
+
// Tracked in github.com/sverklo/sverklo/issues/5.
|
|
18
|
+
//
|
|
19
|
+
// Adding a heuristic:
|
|
20
|
+
// 1. Write a pure function that takes a DiffHunk[] and returns
|
|
21
|
+
// HeuristicFinding[]. No I/O, no git, no filesystem — the caller
|
|
22
|
+
// is responsible for producing hunks.
|
|
23
|
+
// 2. Register it in ALL_HEURISTICS below.
|
|
24
|
+
// 3. Unit-test it with representative fixtures.
|
|
25
|
+
import { execSync } from "node:child_process";
|
|
26
|
+
// ────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Heuristic 1 — unguarded stream-pipeline call
|
|
28
|
+
// ────────────────────────────────────────────────────────────────────
|
|
29
|
+
// Stream pipeline markers we care about. The list is deliberately
|
|
30
|
+
// multi-language: Java / TS / Kotlin / Scala all share the same shape.
|
|
31
|
+
const STREAM_METHODS = [
|
|
32
|
+
"map",
|
|
33
|
+
"forEach",
|
|
34
|
+
"flatMap",
|
|
35
|
+
"filter",
|
|
36
|
+
"reduce",
|
|
37
|
+
"mapToInt",
|
|
38
|
+
"mapToLong",
|
|
39
|
+
"mapToDouble",
|
|
40
|
+
"peek",
|
|
41
|
+
"collect",
|
|
42
|
+
];
|
|
43
|
+
// A line is "entering a stream pipeline" if it matches `.<method>(`
|
|
44
|
+
// where <method> is one of the above. We accept optional whitespace.
|
|
45
|
+
const STREAM_ENTRY_RE = new RegExp(`\\.(${STREAM_METHODS.join("|")})\\s*\\(`);
|
|
46
|
+
// A line looks like a call that may throw if it contains `.<name>(`
|
|
47
|
+
// for an identifier-shaped name. This is broad on purpose — most method
|
|
48
|
+
// calls *can* throw, so any introduced call inside a stream pipeline
|
|
49
|
+
// without a try-catch is the pattern we want to flag.
|
|
50
|
+
const ANY_CALL_RE = /\b\w+\s*\(/;
|
|
51
|
+
// A try-catch is "visible" in the hunk if we see a `try` or `catch`
|
|
52
|
+
// token on any context line. This is a conservative proxy for "the
|
|
53
|
+
// enclosing method catches its exceptions" — an AST-based check would
|
|
54
|
+
// be more accurate but would require parsing the entire file per diff.
|
|
55
|
+
const TRY_OR_CATCH_RE = /\b(try|catch)\b/;
|
|
56
|
+
export function findUnguardedStreamCalls(hunks) {
|
|
57
|
+
const findings = [];
|
|
58
|
+
for (const hunk of hunks) {
|
|
59
|
+
// Is there a visible try / catch anywhere in the hunk's context?
|
|
60
|
+
// If so, skip the whole hunk — the enclosing method probably
|
|
61
|
+
// catches. False negatives here are fine; we care about loud wins.
|
|
62
|
+
const hasTryCatch = hunk.lines.some((l) => TRY_OR_CATCH_RE.test(l));
|
|
63
|
+
if (hasTryCatch)
|
|
64
|
+
continue;
|
|
65
|
+
// Walk the hunk line by line. Track whether we've seen a stream
|
|
66
|
+
// entry on a preceding context/added line, and whether we're still
|
|
67
|
+
// "inside" it (approximated by nesting depth on the same or next
|
|
68
|
+
// lines in the hunk).
|
|
69
|
+
let insideStreamDepth = 0;
|
|
70
|
+
let streamFileLine = hunk.newStart;
|
|
71
|
+
let currentNewLine = hunk.newStart;
|
|
72
|
+
for (const rawLine of hunk.lines) {
|
|
73
|
+
const prefix = rawLine.charAt(0);
|
|
74
|
+
const content = rawLine.slice(1);
|
|
75
|
+
// Track whether we're in a stream block, using a crude brace count
|
|
76
|
+
// scoped to the hunk. This misses pipelines that span more than the
|
|
77
|
+
// hunk window — we accept that as a recall tradeoff.
|
|
78
|
+
if (STREAM_ENTRY_RE.test(content)) {
|
|
79
|
+
insideStreamDepth = 1;
|
|
80
|
+
streamFileLine = currentNewLine;
|
|
81
|
+
}
|
|
82
|
+
else if (insideStreamDepth > 0) {
|
|
83
|
+
const opens = (content.match(/\(/g) || []).length;
|
|
84
|
+
const closes = (content.match(/\)/g) || []).length;
|
|
85
|
+
insideStreamDepth += opens - closes;
|
|
86
|
+
if (insideStreamDepth < 0)
|
|
87
|
+
insideStreamDepth = 0;
|
|
88
|
+
}
|
|
89
|
+
// Only flag on added lines (new risks), not context. Context
|
|
90
|
+
// lines merely help us track state.
|
|
91
|
+
if (prefix === "+" && insideStreamDepth > 0 && ANY_CALL_RE.test(content)) {
|
|
92
|
+
// Skip the stream-entry line itself — we only flag the *body*
|
|
93
|
+
// calls inside the pipeline, not the pipeline declaration.
|
|
94
|
+
if (!STREAM_ENTRY_RE.test(content)) {
|
|
95
|
+
findings.push({
|
|
96
|
+
heuristic: "unguarded-stream-call",
|
|
97
|
+
severity: "medium",
|
|
98
|
+
file: hunk.filePath,
|
|
99
|
+
line: currentNewLine,
|
|
100
|
+
snippet: content.trim().slice(0, 120),
|
|
101
|
+
message: "New call inside a stream pipeline with no visible try-catch in the hunk. " +
|
|
102
|
+
"A single RuntimeException on one element will abort the entire pipeline — " +
|
|
103
|
+
"on a production read path this is an outage. Wrap the lambda body in " +
|
|
104
|
+
"try-catch or pre-filter elements that could throw.",
|
|
105
|
+
});
|
|
106
|
+
// Flag once per stream block to avoid spamming — a block with
|
|
107
|
+
// ten calls is one finding, not ten.
|
|
108
|
+
insideStreamDepth = 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Advance the running new-file line counter. Context and added
|
|
112
|
+
// lines both consume new-file line numbers; removed lines do not.
|
|
113
|
+
if (prefix === "+" || prefix === " ")
|
|
114
|
+
currentNewLine++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return findings;
|
|
118
|
+
}
|
|
119
|
+
// ────────────────────────────────────────────────────────────────────
|
|
120
|
+
// Registry + driver
|
|
121
|
+
// ────────────────────────────────────────────────────────────────────
|
|
122
|
+
export const ALL_HEURISTICS = [
|
|
123
|
+
findUnguardedStreamCalls,
|
|
124
|
+
];
|
|
125
|
+
export function runAllHeuristics(hunks) {
|
|
126
|
+
const all = [];
|
|
127
|
+
for (const fn of ALL_HEURISTICS) {
|
|
128
|
+
try {
|
|
129
|
+
all.push(...fn(hunks));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// One broken heuristic must not take down review. Swallow and move on.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return all;
|
|
136
|
+
}
|
|
137
|
+
// ────────────────────────────────────────────────────────────────────
|
|
138
|
+
// Parser — turn `git diff --unified=N` text into DiffHunk[]
|
|
139
|
+
// ────────────────────────────────────────────────────────────────────
|
|
140
|
+
/**
|
|
141
|
+
* Parse a unified-diff string (output of `git diff -U5 ref`) into hunks.
|
|
142
|
+
* Very permissive — anything that doesn't match the expected header is
|
|
143
|
+
* skipped rather than raising.
|
|
144
|
+
*/
|
|
145
|
+
export function parseUnifiedDiff(diffText) {
|
|
146
|
+
const hunks = [];
|
|
147
|
+
const lines = diffText.split("\n");
|
|
148
|
+
let currentFile = null;
|
|
149
|
+
let currentHunk = null;
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
// New file header: "diff --git a/foo b/foo"
|
|
152
|
+
if (line.startsWith("diff --git ")) {
|
|
153
|
+
if (currentHunk) {
|
|
154
|
+
hunks.push(currentHunk);
|
|
155
|
+
currentHunk = null;
|
|
156
|
+
}
|
|
157
|
+
currentFile = null;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// "+++ b/path" gives us the new file path
|
|
161
|
+
if (line.startsWith("+++ ")) {
|
|
162
|
+
const path = line.slice(4).replace(/^b\//, "").trim();
|
|
163
|
+
currentFile = path === "/dev/null" ? null : path;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// Hunk header: "@@ -12,5 +34,7 @@ optional-context"
|
|
167
|
+
if (line.startsWith("@@ ")) {
|
|
168
|
+
if (currentHunk)
|
|
169
|
+
hunks.push(currentHunk);
|
|
170
|
+
const match = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
171
|
+
if (match && currentFile) {
|
|
172
|
+
currentHunk = {
|
|
173
|
+
filePath: currentFile,
|
|
174
|
+
oldStart: parseInt(match[1], 10),
|
|
175
|
+
newStart: parseInt(match[2], 10),
|
|
176
|
+
lines: [],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
currentHunk = null;
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Body lines: "+", "-", or " " prefixed
|
|
185
|
+
if (currentHunk && (line.startsWith("+") || line.startsWith("-") || line.startsWith(" "))) {
|
|
186
|
+
// Skip the file-metadata lines "+++" / "---" which we already handled above
|
|
187
|
+
if (line.startsWith("+++") || line.startsWith("---"))
|
|
188
|
+
continue;
|
|
189
|
+
currentHunk.lines.push(line);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (currentHunk)
|
|
193
|
+
hunks.push(currentHunk);
|
|
194
|
+
return hunks;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Convenience: pull a unified diff from git and parse it.
|
|
198
|
+
*/
|
|
199
|
+
export function getDiffHunks(rootPath, ref) {
|
|
200
|
+
try {
|
|
201
|
+
const out = execSync(`git diff --unified=10 ${ref}`, {
|
|
202
|
+
cwd: rootPath,
|
|
203
|
+
encoding: "utf-8",
|
|
204
|
+
timeout: 8000,
|
|
205
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
206
|
+
});
|
|
207
|
+
return parseUnifiedDiff(out);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=diff-heuristics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-heuristics.js","sourceRoot":"","sources":["../../../../src/server/tools/diff-heuristics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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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"}
|
|
@@ -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 selection — read from env so users can verify
|
|
28
|
+
// their SVERKLO_EMBEDDING_PROVIDER setting took effect. Issue #9.
|
|
29
|
+
const providerEnv = (process.env.SVERKLO_EMBEDDING_PROVIDER || "default").toLowerCase();
|
|
30
|
+
if (providerEnv !== "default" && providerEnv !== "bundled" && providerEnv !== "onnx") {
|
|
31
|
+
const extra = [];
|
|
32
|
+
if (providerEnv === "openai" && process.env.SVERKLO_OPENAI_MODEL) {
|
|
33
|
+
extra.push(process.env.SVERKLO_OPENAI_MODEL);
|
|
34
|
+
}
|
|
35
|
+
if (providerEnv === "ollama" && process.env.SVERKLO_OLLAMA_MODEL) {
|
|
36
|
+
extra.push(process.env.SVERKLO_OLLAMA_MODEL);
|
|
37
|
+
}
|
|
38
|
+
parts.push(`- Embedding provider: ${providerEnv}${extra.length > 0 ? ` (${extra.join(", ")})` : ""}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
parts.push(`- Embedding provider: default (bundled ONNX, 384d)`);
|
|
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.",
|
|
@@ -34,21 +38,16 @@ export function handleLookup(indexer, args) {
|
|
|
34
38
|
const symbol = args.symbol;
|
|
35
39
|
const type = args.type || "any";
|
|
36
40
|
const tokenBudget = args.token_budget || 1200;
|
|
37
|
-
|
|
41
|
+
// Single JOIN'd query — chunks come back pre-sorted by pagerank DESC
|
|
42
|
+
// and carry the containing file's path, so no full fileStore scan.
|
|
43
|
+
let chunks = indexer.chunkStore.getByNameWithFile(symbol, 20);
|
|
38
44
|
if (type !== "any") {
|
|
39
45
|
chunks = chunks.filter((c) => c.type === type);
|
|
40
46
|
}
|
|
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);
|
|
47
|
+
// formatLookup only reads filePath / lang off the file map when the
|
|
48
|
+
// chunk itself doesn't carry filePath. Since our JOIN provides it,
|
|
49
|
+
// we can pass an empty map and avoid the scan.
|
|
50
|
+
const emptyFileMap = new Map();
|
|
51
|
+
return formatLookup(chunks, emptyFileMap, tokenBudget);
|
|
53
52
|
}
|
|
54
53
|
//# 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,MAAM,MAAM,GAAG,IAAI,CAAC,MAAgB,CAAC;IACrC,MAAM,IAAI,GAAI,IAAI,CAAC,IAA0B,IAAI,KAAK,CAAC;IACvD,MAAM,WAAW,GAAI,IAAI,CAAC,YAAuB,IAAI,IAAI,CAAC;IAE1D,IAAI,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,
|
|
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,MAAM,MAAM,GAAG,IAAI,CAAC,MAAgB,CAAC;IACrC,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"}
|
|
@@ -9,6 +9,11 @@ export declare const recallTool: {
|
|
|
9
9
|
type: string;
|
|
10
10
|
description: string;
|
|
11
11
|
};
|
|
12
|
+
mode: {
|
|
13
|
+
type: string;
|
|
14
|
+
enum: string[];
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
12
17
|
category: {
|
|
13
18
|
type: string;
|
|
14
19
|
enum: string[];
|
|
@@ -23,7 +28,6 @@ export declare const recallTool: {
|
|
|
23
28
|
description: string;
|
|
24
29
|
};
|
|
25
30
|
};
|
|
26
|
-
required: string[];
|
|
27
31
|
};
|
|
28
32
|
};
|
|
29
33
|
export declare function handleRecall(indexer: Indexer, args: Record<string, unknown>): Promise<string>;
|
|
@@ -2,15 +2,37 @@ import { embed, cosineSimilarity } from "../../indexer/embedder.js";
|
|
|
2
2
|
import { checkStaleness } from "../../memory/staleness.js";
|
|
3
3
|
import { track } from "../../telemetry/index.js";
|
|
4
4
|
const RRF_K = 60;
|
|
5
|
+
// Issue #11: core vs archival memory. Core memories are always-on
|
|
6
|
+
// project invariants auto-injected at session start. Archival memories
|
|
7
|
+
// are the searchable long tail. The mode parameter lets callers pick:
|
|
8
|
+
//
|
|
9
|
+
// - mode=core → core tier only, returned in priority order
|
|
10
|
+
// - mode=archival → project + archive tiers, full semantic search
|
|
11
|
+
// - mode=all → default, searches everything
|
|
12
|
+
//
|
|
13
|
+
// Core tier is soft-capped at 25 memories — LLM context windows don't
|
|
14
|
+
// appreciate a 500-line system prompt. Exceeding the cap emits a
|
|
15
|
+
// warning in the recall output but does not block writes.
|
|
16
|
+
const CORE_TIER_SOFT_LIMIT = 25;
|
|
5
17
|
export const recallTool = {
|
|
6
18
|
name: "sverklo_recall",
|
|
7
|
-
description: "Search memories semantically. Finds past decisions, preferences, and patterns relevant to a query."
|
|
19
|
+
description: "Search memories semantically. Finds past decisions, preferences, and patterns relevant to a query. " +
|
|
20
|
+
"Supports two specialized modes: `mode=core` returns only the always-on project invariants (fast, " +
|
|
21
|
+
"no query needed — use at session start); `mode=archival` searches the full archive with semantic " +
|
|
22
|
+
"ranking; `mode=all` (default) searches both. Use core for 'what are the project-wide rules I must " +
|
|
23
|
+
"not violate' and archival for 'what did we decide about X on this codebase'.",
|
|
8
24
|
inputSchema: {
|
|
9
25
|
type: "object",
|
|
10
26
|
properties: {
|
|
11
27
|
query: {
|
|
12
28
|
type: "string",
|
|
13
|
-
description: "What to search for in memories",
|
|
29
|
+
description: "What to search for in memories (optional when mode=core)",
|
|
30
|
+
},
|
|
31
|
+
mode: {
|
|
32
|
+
type: "string",
|
|
33
|
+
enum: ["core", "archival", "all"],
|
|
34
|
+
description: "Which memory tier to search. 'core' = always-on invariants only, 'archival' = " +
|
|
35
|
+
"searchable long tail, 'all' = both (default).",
|
|
14
36
|
},
|
|
15
37
|
category: {
|
|
16
38
|
type: "string",
|
|
@@ -26,14 +48,52 @@ export const recallTool = {
|
|
|
26
48
|
description: "Include stale memories (default: false)",
|
|
27
49
|
},
|
|
28
50
|
},
|
|
29
|
-
required: ["query"],
|
|
30
51
|
},
|
|
31
52
|
};
|
|
32
53
|
export async function handleRecall(indexer, args) {
|
|
33
|
-
const query = args.query;
|
|
54
|
+
const query = args.query || "";
|
|
55
|
+
const mode = args.mode || "all";
|
|
34
56
|
const category = args.category || "any";
|
|
35
57
|
const limit = args.limit || 10;
|
|
36
58
|
const includeStale = args.include_stale || false;
|
|
59
|
+
// Mode: core — return the always-on invariants without semantic
|
|
60
|
+
// ranking. This is the session-start fast path; agents should call
|
|
61
|
+
// this (or read sverklo://context) at the top of every session.
|
|
62
|
+
if (mode === "core") {
|
|
63
|
+
const coreMemories = indexer.memoryStore.getCore(limit);
|
|
64
|
+
const filtered = category === "any"
|
|
65
|
+
? coreMemories
|
|
66
|
+
: coreMemories.filter((m) => m.category === category);
|
|
67
|
+
void track("memory.read");
|
|
68
|
+
if (filtered.length === 0) {
|
|
69
|
+
return ("No core memories yet. Core memories are always-on project invariants " +
|
|
70
|
+
"that auto-load each session. Promote an existing memory with " +
|
|
71
|
+
"`sverklo_promote id:<n> tier:core`, or save a new one with " +
|
|
72
|
+
"`sverklo_remember ... tier:core`.");
|
|
73
|
+
}
|
|
74
|
+
const parts = ["# Core memories (always-on project invariants)", ""];
|
|
75
|
+
for (const m of filtered) {
|
|
76
|
+
const staleFlag = m.is_stale ? " [STALE]" : "";
|
|
77
|
+
parts.push(`- **[${m.category}]**${staleFlag} ${m.content}`);
|
|
78
|
+
}
|
|
79
|
+
parts.push("");
|
|
80
|
+
// Soft-limit warning: too many core memories is an anti-pattern.
|
|
81
|
+
const totalCore = indexer.memoryStore.getCore(1000).length;
|
|
82
|
+
if (totalCore > CORE_TIER_SOFT_LIMIT) {
|
|
83
|
+
parts.push(`⚠️ ${totalCore} core memories — exceeds the soft limit of ${CORE_TIER_SOFT_LIMIT}. ` +
|
|
84
|
+
"Core memories are injected into every session prompt; too many crowds the context " +
|
|
85
|
+
"window. Demote the least-critical ones with `sverklo_demote id:<n>`.");
|
|
86
|
+
}
|
|
87
|
+
return parts.join("\n");
|
|
88
|
+
}
|
|
89
|
+
// Mode: archival | all — full semantic search over the selected
|
|
90
|
+
// tier(s). If the caller passed mode=archival we exclude core; if
|
|
91
|
+
// mode=all we include everything.
|
|
92
|
+
if (!query) {
|
|
93
|
+
return ("A `query` is required for archival/all recall. Pass `mode:core` if you want " +
|
|
94
|
+
"to list always-on invariants without a query.");
|
|
95
|
+
}
|
|
96
|
+
const excludeTiers = mode === "archival" ? ["core"] : [];
|
|
37
97
|
// Signal A: FTS text search
|
|
38
98
|
const ftsResults = indexer.memoryStore.searchFts(query, 30);
|
|
39
99
|
// Signal B: Vector similarity
|
|
@@ -64,6 +124,8 @@ export async function handleRecall(indexer, args) {
|
|
|
64
124
|
continue;
|
|
65
125
|
if (category !== "any" && memory.category !== category)
|
|
66
126
|
continue;
|
|
127
|
+
if (excludeTiers.includes(memory.tier))
|
|
128
|
+
continue;
|
|
67
129
|
// Staleness check (lazy)
|
|
68
130
|
const stale = checkStaleness(memory, indexer.fileStore, indexer.memoryStore);
|
|
69
131
|
if (stale)
|