@nxuss/lemma 1.9.1 → 1.10.0
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 +24 -4
- package/dist/cjs/mcp/tool-profiles.d.ts.map +1 -1
- package/dist/cjs/mcp/tool-profiles.js +3 -0
- package/dist/cjs/mcp/tool-profiles.js.map +1 -1
- package/dist/cjs/mcp/tools.d.ts.map +1 -1
- package/dist/cjs/mcp/tools.js +290 -138
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts +27 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.js +53 -1
- package/dist/cjs/subconscious/TheBrainV2.js.map +1 -1
- package/dist/cjs/utils/AffectedTests.d.ts +54 -0
- package/dist/cjs/utils/AffectedTests.d.ts.map +1 -0
- package/dist/cjs/utils/AffectedTests.js +195 -0
- package/dist/cjs/utils/AffectedTests.js.map +1 -0
- package/dist/cjs/utils/DepGraph.d.ts +28 -0
- package/dist/cjs/utils/DepGraph.d.ts.map +1 -0
- package/dist/cjs/utils/DepGraph.js +210 -0
- package/dist/cjs/utils/DepGraph.js.map +1 -0
- package/dist/cjs/utils/LocalPrefilter.d.ts +61 -0
- package/dist/cjs/utils/LocalPrefilter.d.ts.map +1 -0
- package/dist/cjs/utils/LocalPrefilter.js +169 -0
- package/dist/cjs/utils/LocalPrefilter.js.map +1 -0
- package/dist/cjs/utils/OutputDelta.d.ts +60 -0
- package/dist/cjs/utils/OutputDelta.d.ts.map +1 -0
- package/dist/cjs/utils/OutputDelta.js +226 -0
- package/dist/cjs/utils/OutputDelta.js.map +1 -0
- package/dist/cjs/utils/OutputLedger.d.ts +60 -0
- package/dist/cjs/utils/OutputLedger.d.ts.map +1 -0
- package/dist/cjs/utils/OutputLedger.js +116 -0
- package/dist/cjs/utils/OutputLedger.js.map +1 -0
- package/dist/cjs/utils/ReadWorkspaceCache.d.ts +8 -2
- package/dist/cjs/utils/ReadWorkspaceCache.d.ts.map +1 -1
- package/dist/cjs/utils/ReadWorkspaceCache.js +58 -3
- package/dist/cjs/utils/ReadWorkspaceCache.js.map +1 -1
- package/dist/esm/mcp/tool-profiles.d.ts.map +1 -1
- package/dist/esm/mcp/tool-profiles.js +3 -0
- package/dist/esm/mcp/tool-profiles.js.map +1 -1
- package/dist/esm/mcp/tools.d.ts.map +1 -1
- package/dist/esm/mcp/tools.js +288 -136
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts +27 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.js +53 -1
- package/dist/esm/subconscious/TheBrainV2.js.map +1 -1
- package/dist/esm/utils/AffectedTests.d.ts +54 -0
- package/dist/esm/utils/AffectedTests.d.ts.map +1 -0
- package/dist/esm/utils/AffectedTests.js +186 -0
- package/dist/esm/utils/AffectedTests.js.map +1 -0
- package/dist/esm/utils/DepGraph.d.ts +28 -0
- package/dist/esm/utils/DepGraph.d.ts.map +1 -0
- package/dist/esm/utils/DepGraph.js +169 -0
- package/dist/esm/utils/DepGraph.js.map +1 -0
- package/dist/esm/utils/LocalPrefilter.d.ts +61 -0
- package/dist/esm/utils/LocalPrefilter.d.ts.map +1 -0
- package/dist/esm/utils/LocalPrefilter.js +159 -0
- package/dist/esm/utils/LocalPrefilter.js.map +1 -0
- package/dist/esm/utils/OutputDelta.d.ts +60 -0
- package/dist/esm/utils/OutputDelta.d.ts.map +1 -0
- package/dist/esm/utils/OutputDelta.js +221 -0
- package/dist/esm/utils/OutputDelta.js.map +1 -0
- package/dist/esm/utils/OutputLedger.d.ts +60 -0
- package/dist/esm/utils/OutputLedger.d.ts.map +1 -0
- package/dist/esm/utils/OutputLedger.js +107 -0
- package/dist/esm/utils/OutputLedger.js.map +1 -0
- package/dist/esm/utils/ReadWorkspaceCache.d.ts +8 -2
- package/dist/esm/utils/ReadWorkspaceCache.d.ts.map +1 -1
- package/dist/esm/utils/ReadWorkspaceCache.js +57 -3
- package/dist/esm/utils/ReadWorkspaceCache.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DepGraph — the workspace import graph, built from the TypeScript AST.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `mcp/tools.ts` so more than one tool can use it: `depgraph` reports it to
|
|
6
|
+
* the model, and `affected_tests` walks its reverse edges to decide which tests a change
|
|
7
|
+
* can possibly break. Zero LLM calls — this is parsing, not inference.
|
|
8
|
+
*
|
|
9
|
+
* Building the graph parses every source file in the repo, which is far too expensive to
|
|
10
|
+
* redo on every call. `buildDepGraphMap` therefore memoizes on a stat-only fingerprint:
|
|
11
|
+
* cheap to compute, and it changes the moment any source file is written.
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
47
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
48
|
+
};
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.buildDepGraphMap = buildDepGraphMap;
|
|
51
|
+
exports.bfsDepGraph = bfsDepGraph;
|
|
52
|
+
exports.resetDepGraphCache = resetDepGraphCache;
|
|
53
|
+
const fs_1 = __importDefault(require("fs"));
|
|
54
|
+
const path_1 = __importDefault(require("path"));
|
|
55
|
+
const ts = __importStar(require("typescript"));
|
|
56
|
+
const IGNORE_DG = new Set(["node_modules", ".git", "dist", "chroma_data", ".lemma", "build", "coverage", ".next"]);
|
|
57
|
+
const SOURCE_RE = /\.(ts|tsx|js|jsx)$/;
|
|
58
|
+
/** Extensions tried, in order, when resolving a relative import that omits its suffix. */
|
|
59
|
+
const RESOLUTION_SUFFIXES = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
60
|
+
const graphCache = new Map();
|
|
61
|
+
function collectSourceFiles(workspaceRoot) {
|
|
62
|
+
const found = [];
|
|
63
|
+
const walk = (dir) => {
|
|
64
|
+
let entries = [];
|
|
65
|
+
try {
|
|
66
|
+
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const e of entries) {
|
|
72
|
+
if (IGNORE_DG.has(e.name))
|
|
73
|
+
continue;
|
|
74
|
+
const abs = path_1.default.join(dir, e.name);
|
|
75
|
+
if (e.isDirectory()) {
|
|
76
|
+
walk(abs);
|
|
77
|
+
}
|
|
78
|
+
else if (SOURCE_RE.test(e.name) && !e.name.endsWith(".d.ts")) {
|
|
79
|
+
let stat;
|
|
80
|
+
try {
|
|
81
|
+
stat = fs_1.default.statSync(abs);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
found.push({
|
|
87
|
+
abs,
|
|
88
|
+
rel: path_1.default.relative(workspaceRoot, abs).replace(/\\/g, "/"),
|
|
89
|
+
mtimeMs: stat.mtimeMs,
|
|
90
|
+
size: stat.size,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
walk(workspaceRoot);
|
|
96
|
+
return found;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Fingerprint over paths, sizes and mtimes. Stat-only, so it costs a directory walk rather
|
|
100
|
+
* than a full parse — which is the whole point: it lets us skip the parse when nothing moved.
|
|
101
|
+
*/
|
|
102
|
+
function fingerprintOf(files) {
|
|
103
|
+
return files
|
|
104
|
+
.map((f) => `${f.rel}:${f.mtimeMs}:${f.size}`)
|
|
105
|
+
.sort()
|
|
106
|
+
.join("|");
|
|
107
|
+
}
|
|
108
|
+
function parseInto(map, workspaceRoot) {
|
|
109
|
+
for (const [, node] of map) {
|
|
110
|
+
try {
|
|
111
|
+
const src = fs_1.default.readFileSync(node.abs, "utf8");
|
|
112
|
+
const sf = ts.createSourceFile(node.abs, src, ts.ScriptTarget.Latest, true);
|
|
113
|
+
ts.forEachChild(sf, (n) => {
|
|
114
|
+
if (ts.isImportDeclaration(n) && ts.isStringLiteral(n.moduleSpecifier)) {
|
|
115
|
+
const spec = n.moduleSpecifier.text;
|
|
116
|
+
if (spec.startsWith(".")) {
|
|
117
|
+
const dir = path_1.default.dirname(node.abs);
|
|
118
|
+
let resolved = path_1.default.resolve(dir, spec);
|
|
119
|
+
for (const ext of RESOLUTION_SUFFIXES) {
|
|
120
|
+
if (fs_1.default.existsSync(resolved + ext)) {
|
|
121
|
+
resolved += ext;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const rel = path_1.default.relative(workspaceRoot, resolved).replace(/\\/g, "/");
|
|
126
|
+
if (!node.imports.includes(rel))
|
|
127
|
+
node.imports.push(rel);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const pkg = spec.split("/")[0];
|
|
131
|
+
if (!node.externalImports.includes(pkg))
|
|
132
|
+
node.externalImports.push(pkg);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const hasExportMod = (nd) => ts.canHaveModifiers(nd) &&
|
|
136
|
+
(ts.getModifiers(nd) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
137
|
+
if (hasExportMod(n)) {
|
|
138
|
+
if (ts.isFunctionDeclaration(n) && n.name)
|
|
139
|
+
node.exports.push(n.name.text);
|
|
140
|
+
else if (ts.isClassDeclaration(n) && n.name)
|
|
141
|
+
node.exports.push(n.name.text);
|
|
142
|
+
else if (ts.isVariableStatement(n)) {
|
|
143
|
+
for (const decl of n.declarationList.declarations) {
|
|
144
|
+
if (ts.isIdentifier(decl.name))
|
|
145
|
+
node.exports.push(decl.name.text);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (ts.isInterfaceDeclaration(n))
|
|
149
|
+
node.exports.push(n.name.text);
|
|
150
|
+
else if (ts.isTypeAliasDeclaration(n))
|
|
151
|
+
node.exports.push(n.name.text);
|
|
152
|
+
else if (ts.isEnumDeclaration(n))
|
|
153
|
+
node.exports.push(n.name.text);
|
|
154
|
+
}
|
|
155
|
+
if (ts.isExportDeclaration(n) && n.exportClause && ts.isNamedExports(n.exportClause)) {
|
|
156
|
+
for (const el of n.exportClause.elements)
|
|
157
|
+
node.exports.push(el.name.text);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
/* skip unparseable */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const [, node] of map) {
|
|
166
|
+
for (const imp of node.imports) {
|
|
167
|
+
const target = map.get(imp);
|
|
168
|
+
if (target && !target.importedBy.includes(node.rel))
|
|
169
|
+
target.importedBy.push(node.rel);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function buildDepGraphMap(workspaceRoot) {
|
|
174
|
+
const files = collectSourceFiles(workspaceRoot);
|
|
175
|
+
const fingerprint = fingerprintOf(files);
|
|
176
|
+
const cached = graphCache.get(workspaceRoot);
|
|
177
|
+
if (cached && cached.fingerprint === fingerprint)
|
|
178
|
+
return cached.map;
|
|
179
|
+
const map = new Map();
|
|
180
|
+
for (const f of files) {
|
|
181
|
+
map.set(f.rel, { rel: f.rel, abs: f.abs, imports: [], importedBy: [], externalImports: [], exports: [] });
|
|
182
|
+
}
|
|
183
|
+
parseInto(map, workspaceRoot);
|
|
184
|
+
graphCache.set(workspaceRoot, { fingerprint, map });
|
|
185
|
+
return map;
|
|
186
|
+
}
|
|
187
|
+
function bfsDepGraph(map, startRel, direction, maxDepth) {
|
|
188
|
+
const visited = new Map();
|
|
189
|
+
const queue = [{ rel: startRel, depth: 0 }];
|
|
190
|
+
while (queue.length > 0) {
|
|
191
|
+
const item = queue.shift();
|
|
192
|
+
if (visited.has(item.rel) || item.depth > maxDepth)
|
|
193
|
+
continue;
|
|
194
|
+
const node = map.get(item.rel);
|
|
195
|
+
if (!node)
|
|
196
|
+
continue;
|
|
197
|
+
visited.set(item.rel, { node, depth: item.depth });
|
|
198
|
+
if (item.depth < maxDepth) {
|
|
199
|
+
const neighbors = direction === "forward" ? node.imports : node.importedBy;
|
|
200
|
+
for (const n of neighbors)
|
|
201
|
+
queue.push({ rel: n, depth: item.depth + 1 });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return visited;
|
|
205
|
+
}
|
|
206
|
+
/** Test seam — the cache is process-global and would otherwise leak between test cases. */
|
|
207
|
+
function resetDepGraphCache() {
|
|
208
|
+
graphCache.clear();
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=DepGraph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DepGraph.js","sourceRoot":"","sources":["../../../src/utils/DepGraph.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoIH,4CAeC;AAED,kCAoBC;AAGD,gDAEC;AA5KD,4CAAoB;AACpB,gDAAwB;AACxB,+CAAiC;AAajC,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;AACnH,MAAM,SAAS,GAAG,oBAAoB,CAAC;AAEvC,0FAA0F;AAC1F,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;AAOnG,MAAM,UAAU,GAAG,IAAI,GAAG,EAAuB,CAAC;AAElD,SAAS,kBAAkB,CAAC,aAAqB;IAC/C,MAAM,KAAK,GAAuE,EAAE,CAAC;IAErF,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAE;QAC3B,IAAI,OAAO,GAAgB,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,OAAO,GAAG,YAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;gBAAE,SAAS;YACpC,MAAM,GAAG,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;gBACpB,IAAI,CAAC,GAAG,CAAC,CAAC;YACZ,CAAC;iBAAM,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/D,IAAI,IAAc,CAAC;gBACnB,IAAI,CAAC;oBACH,IAAI,GAAG,YAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;gBAC1B,CAAC;gBAAC,MAAM,CAAC;oBACP,SAAS;gBACX,CAAC;gBACD,KAAK,CAAC,IAAI,CAAC;oBACT,GAAG;oBACH,GAAG,EAAE,cAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;oBAC1D,OAAO,EAAE,IAAI,CAAC,OAAO;oBACrB,IAAI,EAAE,IAAI,CAAC,IAAI;iBAChB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,CAAC,aAAa,CAAC,CAAC;IACpB,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa,CAAC,KAA4D;IACjF,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;SAC7C,IAAI,EAAE;SACN,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,SAAS,SAAS,CAAC,GAAgB,EAAE,aAAqB;IACxD,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,YAAE,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAC9C,MAAM,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAC5E,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE;gBACxB,IAAI,EAAE,CAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE,CAAC;oBACvE,MAAM,IAAI,GAAG,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC;oBACpC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;wBACzB,MAAM,GAAG,GAAG,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;wBACnC,IAAI,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;wBACvC,KAAK,MAAM,GAAG,IAAI,mBAAmB,EAAE,CAAC;4BACtC,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,GAAG,GAAG,CAAC,EAAE,CAAC;gCAClC,QAAQ,IAAI,GAAG,CAAC;gCAChB,MAAM;4BACR,CAAC;wBACH,CAAC;wBACD,MAAM,GAAG,GAAG,cAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;wBACvE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;4BAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;oBAC1D,CAAC;yBAAM,CAAC;wBACN,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;wBAC/B,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC;4BAAE,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;oBAC1E,CAAC;gBACH,CAAC;gBACD,MAAM,YAAY,GAAG,CAAC,EAAW,EAAE,EAAE,CACnC,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC;oBACvB,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;gBAClF,IAAI,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;oBACpB,IAAI,EAAE,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI;wBAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;yBACrE,IAAI,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI;wBAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;yBACvE,IAAI,EAAE,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC;wBACnC,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,eAAe,CAAC,YAAY,EAAE,CAAC;4BAClD,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;gCAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;wBACpE,CAAC;oBACH,CAAC;yBAAM,IAAI,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAC;wBAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAE,CAA6B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;yBAChG,IAAI,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAC;wBAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;yBACjE,IAAI,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC;wBAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACnE,CAAC;gBACD,IAAI,EAAE,CAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,IAAI,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC;oBACrF,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,YAAY,CAAC,QAAQ;wBAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC5E,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,sBAAsB;QACxB,CAAC;IACH,CAAC;IAED,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;QAC3B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC5B,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC;gBAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAgB,gBAAgB,CAAC,aAAqB;IACpD,MAAM,KAAK,GAAG,kBAAkB,CAAC,aAAa,CAAC,CAAC;IAChD,MAAM,WAAW,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAEzC,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC7C,IAAI,MAAM,IAAI,MAAM,CAAC,WAAW,KAAK,WAAW;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC;IAEpE,MAAM,GAAG,GAAgB,IAAI,GAAG,EAAE,CAAC;IACnC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,eAAe,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;IAC5G,CAAC;IACD,SAAS,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IAE9B,UAAU,CAAC,GAAG,CAAC,aAAa,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC;IACpD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAgB,WAAW,CACzB,GAAgB,EAChB,QAAgB,EAChB,SAAiC,EACjC,QAAgB;IAEhB,MAAM,OAAO,GAAG,IAAI,GAAG,EAAiD,CAAC;IACzE,MAAM,KAAK,GAA0C,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IACnF,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,EAAG,CAAC;QAC5B,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,GAAG,QAAQ;YAAE,SAAS;QAC7D,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACnD,IAAI,IAAI,CAAC,KAAK,GAAG,QAAQ,EAAE,CAAC;YAC1B,MAAM,SAAS,GAAG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;YAC3E,KAAK,MAAM,CAAC,IAAI,SAAS;gBAAE,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,2FAA2F;AAC3F,SAAgB,kBAAkB;IAChC,UAAU,CAAC,KAAK,EAAE,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalPrefilter — spend a local model's tokens so the expensive one never sees the noise.
|
|
3
|
+
*
|
|
4
|
+
* A broad `search_workspace` returns 200 matches across 60 files. Four of them matter. The
|
|
5
|
+
* frontier model currently pays for all 200 — in the turn that produced them and in every
|
|
6
|
+
* turn afterwards, because tool results stay in the transcript. A 3B model running on the
|
|
7
|
+
* user's own machine can rank that list for free, in the local sense of free: no API cost,
|
|
8
|
+
* no context cost, no per-token billing.
|
|
9
|
+
*
|
|
10
|
+
* This is the one place in Lemma where a model's judgement decides what the main model
|
|
11
|
+
* sees, so the safety rules are stricter than anywhere else:
|
|
12
|
+
*
|
|
13
|
+
* 1. **It ranks; it does not delete.** The prefilter reorders candidates and the caller
|
|
14
|
+
* shows a prefix of that order. Nothing is claimed to be irrelevant — the output says
|
|
15
|
+
* how many were withheld and how to get them all back.
|
|
16
|
+
* 2. **Fail open, always.** No Ollama, wrong model, timeout, malformed reply, an index out
|
|
17
|
+
* of range — every one of those returns null and the caller sends the unfiltered list.
|
|
18
|
+
* A prefilter that fails closed would silently hide code from the model.
|
|
19
|
+
* 3. **Disclosed at the call site.** The caller must state that a local model did the
|
|
20
|
+
* ranking. A subset presented as if it were the whole search is a lie by omission,
|
|
21
|
+
* and the user cannot audit what they were not told happened.
|
|
22
|
+
*
|
|
23
|
+
* Opt-in: set `LEMMA_LOCAL_PREFILTER=1` (or `mcp.localPrefilter: true` in lemma.config.json).
|
|
24
|
+
* Off by default, because a wrong ranking costs a correct answer and the user should choose
|
|
25
|
+
* to accept that trade.
|
|
26
|
+
*/
|
|
27
|
+
export interface PrefilterConfig {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
url: string;
|
|
30
|
+
model: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function loadPrefilterConfig(cwd?: string): PrefilterConfig;
|
|
33
|
+
export declare function isLocalModelAvailable(config: PrefilterConfig): Promise<boolean>;
|
|
34
|
+
export interface Candidate {
|
|
35
|
+
/** Stable identifier the caller uses to map a ranking back to its own data. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** One short line describing the candidate — a path plus the matched text, typically. */
|
|
38
|
+
summary: string;
|
|
39
|
+
}
|
|
40
|
+
export interface RankResult {
|
|
41
|
+
/** Candidate ids, most relevant first. Contains every input id: ranking is not filtering. */
|
|
42
|
+
order: string[];
|
|
43
|
+
model: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse the model's reply into an ordering.
|
|
47
|
+
*
|
|
48
|
+
* Strict on purpose. A small model will sometimes emit prose, duplicate an index, or invent
|
|
49
|
+
* one; any of those means we do not actually know what it decided, and guessing at its
|
|
50
|
+
* intent is how the relevant file ends up ranked last. Anything not exactly parseable
|
|
51
|
+
* returns null, and the caller sends everything.
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseRanking(reply: string, candidates: Candidate[]): string[] | null;
|
|
54
|
+
/**
|
|
55
|
+
* Rank candidates with the local model. Returns null whenever ranking did not definitively
|
|
56
|
+
* succeed — the caller must then use its own original order and withhold nothing.
|
|
57
|
+
*/
|
|
58
|
+
export declare function rankCandidates(query: string, candidates: Candidate[], config?: PrefilterConfig): Promise<RankResult | null>;
|
|
59
|
+
/** Test seam — the availability probe is cached for the process lifetime. */
|
|
60
|
+
export declare function resetPrefilterProbe(): void;
|
|
61
|
+
//# sourceMappingURL=LocalPrefilter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalPrefilter.d.ts","sourceRoot":"","sources":["../../../src/utils/LocalPrefilter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAgBH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAsB,GAAG,eAAe,CAmBhF;AAUD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAerF;AAED,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,EAAE,EAAE,MAAM,CAAC;IACX,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,6FAA6F;IAC7F,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAkBD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,GAAG,IAAI,CAqBpF;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,SAAS,EAAE,EACvB,MAAM,GAAE,eAAuC,GAC9C,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAuB5B;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* LocalPrefilter — spend a local model's tokens so the expensive one never sees the noise.
|
|
4
|
+
*
|
|
5
|
+
* A broad `search_workspace` returns 200 matches across 60 files. Four of them matter. The
|
|
6
|
+
* frontier model currently pays for all 200 — in the turn that produced them and in every
|
|
7
|
+
* turn afterwards, because tool results stay in the transcript. A 3B model running on the
|
|
8
|
+
* user's own machine can rank that list for free, in the local sense of free: no API cost,
|
|
9
|
+
* no context cost, no per-token billing.
|
|
10
|
+
*
|
|
11
|
+
* This is the one place in Lemma where a model's judgement decides what the main model
|
|
12
|
+
* sees, so the safety rules are stricter than anywhere else:
|
|
13
|
+
*
|
|
14
|
+
* 1. **It ranks; it does not delete.** The prefilter reorders candidates and the caller
|
|
15
|
+
* shows a prefix of that order. Nothing is claimed to be irrelevant — the output says
|
|
16
|
+
* how many were withheld and how to get them all back.
|
|
17
|
+
* 2. **Fail open, always.** No Ollama, wrong model, timeout, malformed reply, an index out
|
|
18
|
+
* of range — every one of those returns null and the caller sends the unfiltered list.
|
|
19
|
+
* A prefilter that fails closed would silently hide code from the model.
|
|
20
|
+
* 3. **Disclosed at the call site.** The caller must state that a local model did the
|
|
21
|
+
* ranking. A subset presented as if it were the whole search is a lie by omission,
|
|
22
|
+
* and the user cannot audit what they were not told happened.
|
|
23
|
+
*
|
|
24
|
+
* Opt-in: set `LEMMA_LOCAL_PREFILTER=1` (or `mcp.localPrefilter: true` in lemma.config.json).
|
|
25
|
+
* Off by default, because a wrong ranking costs a correct answer and the user should choose
|
|
26
|
+
* to accept that trade.
|
|
27
|
+
*/
|
|
28
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
29
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
30
|
+
};
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.loadPrefilterConfig = loadPrefilterConfig;
|
|
33
|
+
exports.isLocalModelAvailable = isLocalModelAvailable;
|
|
34
|
+
exports.parseRanking = parseRanking;
|
|
35
|
+
exports.rankCandidates = rankCandidates;
|
|
36
|
+
exports.resetPrefilterProbe = resetPrefilterProbe;
|
|
37
|
+
const fs_1 = __importDefault(require("fs"));
|
|
38
|
+
const path_1 = __importDefault(require("path"));
|
|
39
|
+
const axios_1 = __importDefault(require("axios"));
|
|
40
|
+
const DEFAULT_MODEL = "qwen2.5-coder:3b";
|
|
41
|
+
const PROBE_TIMEOUT_MS = 1500;
|
|
42
|
+
const RANK_TIMEOUT_MS = 12000;
|
|
43
|
+
/**
|
|
44
|
+
* Below this many candidates the local round-trip (latency the user waits on) is not worth
|
|
45
|
+
* it — the full list was already cheap.
|
|
46
|
+
*/
|
|
47
|
+
const MIN_CANDIDATES = 25;
|
|
48
|
+
function loadPrefilterConfig(cwd = process.cwd()) {
|
|
49
|
+
let fileEnabled;
|
|
50
|
+
let fileModel;
|
|
51
|
+
try {
|
|
52
|
+
const raw = JSON.parse(fs_1.default.readFileSync(path_1.default.join(cwd, "lemma.config.json"), "utf8"));
|
|
53
|
+
if (typeof raw?.mcp?.localPrefilter === "boolean")
|
|
54
|
+
fileEnabled = raw.mcp.localPrefilter;
|
|
55
|
+
if (typeof raw?.mcp?.localPrefilterModel === "string")
|
|
56
|
+
fileModel = raw.mcp.localPrefilterModel;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// An unreadable config must never turn a feature on that the user did not ask for.
|
|
60
|
+
}
|
|
61
|
+
const envEnabled = process.env.LEMMA_LOCAL_PREFILTER;
|
|
62
|
+
const enabled = envEnabled === "1" || envEnabled === "true" ? true : envEnabled === "0" || envEnabled === "false" ? false : fileEnabled === true;
|
|
63
|
+
return {
|
|
64
|
+
enabled,
|
|
65
|
+
url: process.env.OLLAMA_URL || process.env.OLLAMA_HOST || "http://localhost:11434",
|
|
66
|
+
model: process.env.LEMMA_LOCAL_PREFILTER_MODEL || fileModel || DEFAULT_MODEL,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Availability is cached for the process: probing on every search would add a round-trip to
|
|
71
|
+
* the very path this is meant to make cheaper. A negative result is cached too — if Ollama
|
|
72
|
+
* isn't running at the start of a session it is almost never running by the end, and
|
|
73
|
+
* re-probing would tax every search with a timeout.
|
|
74
|
+
*/
|
|
75
|
+
let availability = { checked: false, available: false };
|
|
76
|
+
async function isLocalModelAvailable(config) {
|
|
77
|
+
if (!config.enabled)
|
|
78
|
+
return false;
|
|
79
|
+
if (availability.checked)
|
|
80
|
+
return availability.available;
|
|
81
|
+
try {
|
|
82
|
+
const res = await axios_1.default.get(`${config.url}/api/tags`, { timeout: PROBE_TIMEOUT_MS });
|
|
83
|
+
const models = (res.data?.models || []).map((m) => String(m?.name ?? ""));
|
|
84
|
+
// A configured model that isn't pulled would fail on every call; treat that as
|
|
85
|
+
// unavailable rather than discovering it once per search.
|
|
86
|
+
const base = config.model.split(":")[0];
|
|
87
|
+
availability = { checked: true, available: models.some((m) => m === config.model || m.split(":")[0] === base) };
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
availability = { checked: true, available: false };
|
|
91
|
+
}
|
|
92
|
+
return availability.available;
|
|
93
|
+
}
|
|
94
|
+
function buildPrompt(query, candidates) {
|
|
95
|
+
const list = candidates.map((c, i) => `${i}. ${c.summary}`).join("\n");
|
|
96
|
+
return [
|
|
97
|
+
`A developer is searching a codebase for: "${query}"`,
|
|
98
|
+
``,
|
|
99
|
+
`Below are search results, numbered. Rank them by how likely each is to be what the`,
|
|
100
|
+
`developer is looking for. Definitions and primary implementations rank above call`,
|
|
101
|
+
`sites; call sites rank above tests, mocks, generated files and build output.`,
|
|
102
|
+
``,
|
|
103
|
+
list,
|
|
104
|
+
``,
|
|
105
|
+
`Reply with ONLY the numbers, most relevant first, comma-separated. Include every`,
|
|
106
|
+
`number exactly once. No explanation, no other text.`,
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Parse the model's reply into an ordering.
|
|
111
|
+
*
|
|
112
|
+
* Strict on purpose. A small model will sometimes emit prose, duplicate an index, or invent
|
|
113
|
+
* one; any of those means we do not actually know what it decided, and guessing at its
|
|
114
|
+
* intent is how the relevant file ends up ranked last. Anything not exactly parseable
|
|
115
|
+
* returns null, and the caller sends everything.
|
|
116
|
+
*/
|
|
117
|
+
function parseRanking(reply, candidates) {
|
|
118
|
+
const numbers = (reply.match(/\d+/g) || []).map((n) => parseInt(n, 10));
|
|
119
|
+
if (numbers.length === 0)
|
|
120
|
+
return null;
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const order = [];
|
|
123
|
+
for (const n of numbers) {
|
|
124
|
+
if (n < 0 || n >= candidates.length)
|
|
125
|
+
return null; // hallucinated index — reply is untrustworthy
|
|
126
|
+
if (seen.has(n))
|
|
127
|
+
continue; // a repeat is sloppy, not wrong; the first position stands
|
|
128
|
+
seen.add(n);
|
|
129
|
+
order.push(candidates[n].id);
|
|
130
|
+
}
|
|
131
|
+
// Anything the model left out is appended in its original order rather than dropped.
|
|
132
|
+
// Ranking is not filtering — the caller decides what to show, and it must be able to
|
|
133
|
+
// show all of them.
|
|
134
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
135
|
+
if (!seen.has(i))
|
|
136
|
+
order.push(candidates[i].id);
|
|
137
|
+
}
|
|
138
|
+
return order;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Rank candidates with the local model. Returns null whenever ranking did not definitively
|
|
142
|
+
* succeed — the caller must then use its own original order and withhold nothing.
|
|
143
|
+
*/
|
|
144
|
+
async function rankCandidates(query, candidates, config = loadPrefilterConfig()) {
|
|
145
|
+
if (candidates.length < MIN_CANDIDATES)
|
|
146
|
+
return null;
|
|
147
|
+
if (!(await isLocalModelAvailable(config)))
|
|
148
|
+
return null;
|
|
149
|
+
try {
|
|
150
|
+
const res = await axios_1.default.post(`${config.url}/api/generate`, {
|
|
151
|
+
model: config.model,
|
|
152
|
+
prompt: buildPrompt(query, candidates),
|
|
153
|
+
stream: false,
|
|
154
|
+
// Deterministic: the same search should not reorder itself between calls, and
|
|
155
|
+
// creativity is worthless for a ranking task.
|
|
156
|
+
options: { temperature: 0, num_predict: 512 },
|
|
157
|
+
}, { timeout: RANK_TIMEOUT_MS });
|
|
158
|
+
const order = parseRanking(String(res.data?.response ?? ""), candidates);
|
|
159
|
+
return order ? { order, model: config.model } : null;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Test seam — the availability probe is cached for the process lifetime. */
|
|
166
|
+
function resetPrefilterProbe() {
|
|
167
|
+
availability = { checked: false, available: false };
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=LocalPrefilter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalPrefilter.js","sourceRoot":"","sources":["../../../src/utils/LocalPrefilter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OutputDelta — re-send only what changed, never the whole thing again.
|
|
3
|
+
*
|
|
4
|
+
* The problem: a debug loop reads the same file (or runs the same command) six times.
|
|
5
|
+
* Between two of those calls maybe three lines moved, but the model pays for the entire
|
|
6
|
+
* file or the entire 400-line `tsc` dump each time — and because tool results stay in the
|
|
7
|
+
* transcript, it keeps paying for every copy on every subsequent turn.
|
|
8
|
+
*
|
|
9
|
+
* `ReadWorkspaceCache` already suppresses the *identical* case. This module covers the far
|
|
10
|
+
* more common one: it changed, but barely.
|
|
11
|
+
*
|
|
12
|
+
* Two rules this module is built around:
|
|
13
|
+
*
|
|
14
|
+
* 1. The diff is computed against the exact text the model received. Not the file on
|
|
15
|
+
* disk, not the pre-squeeze source — the final emitted string. A diff against
|
|
16
|
+
* anything else describes a document the model never had, which is worse than
|
|
17
|
+
* re-sending the whole file.
|
|
18
|
+
* 2. A delta is only emitted when it is decisively smaller than the full text. Below
|
|
19
|
+
* that margin, reconstructing from a diff is cognitive work with no token payoff, so
|
|
20
|
+
* the full text wins. See `shouldEmitDelta`.
|
|
21
|
+
*/
|
|
22
|
+
export interface Hunk {
|
|
23
|
+
/** 1-indexed start line in the previous text. */
|
|
24
|
+
oldStart: number;
|
|
25
|
+
oldLines: number;
|
|
26
|
+
/** 1-indexed start line in the new text. */
|
|
27
|
+
newStart: number;
|
|
28
|
+
newLines: number;
|
|
29
|
+
/** Rendered lines, each prefixed with ' ', '-' or '+'. */
|
|
30
|
+
lines: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface DiffResult {
|
|
33
|
+
hunks: Hunk[];
|
|
34
|
+
added: number;
|
|
35
|
+
removed: number;
|
|
36
|
+
/** True when the two texts are byte-identical. */
|
|
37
|
+
identical: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** Line-level diff between the text the model already has and the text we would send now. */
|
|
40
|
+
export declare function computeDiff(previous: string, next: string): DiffResult | null;
|
|
41
|
+
/** Render hunks as a unified diff body (no ---/+++ file headers; the caller supplies context). */
|
|
42
|
+
export declare function renderDiff(diff: DiffResult): string;
|
|
43
|
+
export interface DeltaDecision {
|
|
44
|
+
emit: boolean;
|
|
45
|
+
/** The rendered diff body, present only when `emit` is true. */
|
|
46
|
+
body?: string;
|
|
47
|
+
added: number;
|
|
48
|
+
removed: number;
|
|
49
|
+
hunkCount: number;
|
|
50
|
+
/** Why a delta was declined — surfaced in logs and receipts, never guessed at. */
|
|
51
|
+
reason?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Decide whether to hand the model a delta instead of the full text.
|
|
55
|
+
*
|
|
56
|
+
* Declining is the safe direction: the caller falls back to sending everything, which is
|
|
57
|
+
* always correct. So every uncertain case here resolves to `emit: false`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function shouldEmitDelta(previous: string, next: string): DeltaDecision;
|
|
60
|
+
//# sourceMappingURL=OutputDelta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OutputDelta.d.ts","sourceRoot":"","sources":["../../../src/utils/OutputDelta.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA2BH,MAAM,WAAW,IAAI;IACnB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,SAAS,EAAE,OAAO,CAAC;CACpB;AA4HD,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAY7E;AAED,kGAAkG;AAClG,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAInD;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CA+B7E"}
|