grepmax 0.17.2 → 0.17.3
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 +17 -0
- package/dist/eval-graph-sanity.js +225 -0
- package/dist/eval-graph-spotcheck.js +83 -0
- package/dist/eval-graph-totals.js +131 -0
- package/dist/eval-oss.js +244 -0
- package/dist/eval.js +6 -1
- package/dist/lib/search/pagerank.js +267 -0
- package/dist/lib/search/searcher.js +44 -4
- package/mlx-embed-server/server.py +24 -0
- package/package.json +3 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/skills/grepmax/SKILL.md +15 -5
package/README.md
CHANGED
|
@@ -64,6 +64,7 @@ gmax log src/lib/auth.ts # Git commit history for a path or symb
|
|
|
64
64
|
gmax test handleAuth # Find tests via reverse call graph
|
|
65
65
|
gmax impact handleAuth # Dependents + affected tests
|
|
66
66
|
gmax similar handleAuth # Find similar code patterns
|
|
67
|
+
gmax dead handleAuth # Unused-symbol check via call graph (DEAD / PUBLIC EXPORT / LIVE)
|
|
67
68
|
gmax context "auth system" --budget 4000 # Token-budgeted topic summary
|
|
68
69
|
```
|
|
69
70
|
|
|
@@ -121,6 +122,7 @@ Plugins auto-update when you run `npm install -g grepmax@latest` — no need to
|
|
|
121
122
|
| `trace_calls` | Call graph: importers, callers (multi-hop), callees with file:line. |
|
|
122
123
|
| `extract_symbol` | Complete function/class body by symbol name. |
|
|
123
124
|
| `peek_symbol` | Compact overview: signature + callers + callees. |
|
|
125
|
+
| `dead` | Unused-symbol check via call graph. Returns `DEAD`, `PUBLIC EXPORT`, or `LIVE` with caller count. |
|
|
124
126
|
| `list_symbols` | Indexed symbols with role and export status. |
|
|
125
127
|
| `index_status` | Index health: chunks, files, projects, watcher status. |
|
|
126
128
|
| `summarize_project` | Project overview: languages, structure, key symbols, entry points. |
|
|
@@ -277,6 +279,7 @@ fixtures/
|
|
|
277
279
|
| `GMAX_WORKER_THREADS` | Worker threads for embedding | 50% of cores |
|
|
278
280
|
| `GMAX_DEBUG` | Debug logging | Off |
|
|
279
281
|
| `GMAX_SUMMARIZER` | Enable summarizer auto-start (`1`) | Off |
|
|
282
|
+
| `GMAX_RERANK` | Enable ColBERT rerank (`1`) — off by default since v0.17.1 ([why](docs/known-limitations.md)) | Off |
|
|
280
283
|
|
|
281
284
|
## Troubleshooting
|
|
282
285
|
|
|
@@ -293,6 +296,20 @@ gmax watch stop && gmax watch --daemon -b # Restart daemon
|
|
|
293
296
|
|
|
294
297
|
See [CLAUDE.md](CLAUDE.md) for development setup, commands, and architecture details.
|
|
295
298
|
|
|
299
|
+
### Benchmarks
|
|
300
|
+
|
|
301
|
+
Two evaluation harnesses live in the repo. Both emit stable JSON via `:json` variants.
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
pnpm bench:recall # 97-case internal eval against gmax's own repo
|
|
305
|
+
pnpm bench:recall:json
|
|
306
|
+
pnpm bench:oss # P1 definition-lookup across express, lodash, platform (sverklo-bench fixtures)
|
|
307
|
+
pnpm bench:oss:json
|
|
308
|
+
GMAX_EVAL_RERANK=1 pnpm bench:oss # toggle ColBERT rerank
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The OSS bench requires the fixture repos to be indexed first — see [`docs/known-limitations.md`](docs/known-limitations.md) for the most recent rerank-on-vs-off comparison across 4 datasets / 131 cases.
|
|
312
|
+
|
|
296
313
|
## Attribution
|
|
297
314
|
|
|
298
315
|
grepmax is built upon the foundation of [mgrep](https://github.com/mixedbread-ai/mgrep) by MixedBread. See the [NOTICE](NOTICE) file for details.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 0 sanity check for Bundle B G1' (graph-as-recall-recovery).
|
|
4
|
+
*
|
|
5
|
+
* For each platform hard-miss case (BeyondError, ErrorCodes, resolveActor,
|
|
6
|
+
* errorHandler), mirror the searcher's dense+FTS+RRF pipeline to produce
|
|
7
|
+
* the post-fusion top-200 candidate pool, then count how many of those
|
|
8
|
+
* 200 chunks have `referenced_symbols` containing the target symbol.
|
|
9
|
+
*
|
|
10
|
+
* ≥1 hit per case = 1-hop outbound graph walk can recover the target
|
|
11
|
+
* (the seed chunk's defined symbol points at the target via the call
|
|
12
|
+
* graph) → worth building the recovery layer.
|
|
13
|
+
*
|
|
14
|
+
* 0 hits across all 4 = the signal isn't in the graph at this seed
|
|
15
|
+
* depth → stop and report the negative result.
|
|
16
|
+
*
|
|
17
|
+
* Run: `npx tsx src/eval-graph-sanity.ts`
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
53
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
54
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
55
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
56
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
57
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
58
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
var _a, _b;
|
|
62
|
+
var _c;
|
|
63
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
64
|
+
(_a = (_c = process.env).GMAX_WORKER_COUNT) !== null && _a !== void 0 ? _a : (_c.GMAX_WORKER_COUNT = "1");
|
|
65
|
+
const path = __importStar(require("node:path"));
|
|
66
|
+
const config_1 = require("./config");
|
|
67
|
+
const vector_db_1 = require("./lib/store/vector-db");
|
|
68
|
+
const exit_1 = require("./lib/utils/exit");
|
|
69
|
+
const pool_1 = require("./lib/workers/pool");
|
|
70
|
+
const filter_builder_1 = require("./lib/utils/filter-builder");
|
|
71
|
+
const PLATFORM_ROOT = path.join((_b = process.env.HOME) !== null && _b !== void 0 ? _b : "", "Development/beyond/platform");
|
|
72
|
+
const HARD_MISS_TARGETS = [
|
|
73
|
+
"BeyondError",
|
|
74
|
+
"ErrorCodes",
|
|
75
|
+
"resolveActor",
|
|
76
|
+
"errorHandler",
|
|
77
|
+
];
|
|
78
|
+
const PRE_K = 500;
|
|
79
|
+
const STAGE1_K = 200;
|
|
80
|
+
const RRF_K = 60;
|
|
81
|
+
function toStringArray(val) {
|
|
82
|
+
if (!val)
|
|
83
|
+
return [];
|
|
84
|
+
if (Array.isArray(val)) {
|
|
85
|
+
return val.filter((v) => typeof v === "string");
|
|
86
|
+
}
|
|
87
|
+
const maybe = val;
|
|
88
|
+
if (typeof maybe.toArray === "function") {
|
|
89
|
+
try {
|
|
90
|
+
const arr = maybe.toArray();
|
|
91
|
+
return Array.isArray(arr)
|
|
92
|
+
? arr.filter((v) => typeof v === "string")
|
|
93
|
+
: [];
|
|
94
|
+
}
|
|
95
|
+
catch (_a) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
function probe(target) {
|
|
102
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
103
|
+
const db = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir);
|
|
104
|
+
const table = yield db.ensureTable();
|
|
105
|
+
const pool = (0, pool_1.getWorkerPool)();
|
|
106
|
+
const { dense } = yield pool.encodeQuery(target);
|
|
107
|
+
const pathPrefix = PLATFORM_ROOT.endsWith("/") ? PLATFORM_ROOT : `${PLATFORM_ROOT}/`;
|
|
108
|
+
const where = `path LIKE '${(0, filter_builder_1.escapeSqlString)(pathPrefix)}%'`;
|
|
109
|
+
const columns = [
|
|
110
|
+
"id",
|
|
111
|
+
"path",
|
|
112
|
+
"chunk_index",
|
|
113
|
+
"start_line",
|
|
114
|
+
"end_line",
|
|
115
|
+
"defined_symbols",
|
|
116
|
+
"referenced_symbols",
|
|
117
|
+
];
|
|
118
|
+
const vectorRows = (yield table
|
|
119
|
+
.vectorSearch(dense)
|
|
120
|
+
.select([...columns, "_distance"])
|
|
121
|
+
.where(where)
|
|
122
|
+
.limit(PRE_K)
|
|
123
|
+
.toArray());
|
|
124
|
+
let ftsRows = [];
|
|
125
|
+
try {
|
|
126
|
+
ftsRows = (yield table
|
|
127
|
+
.search(target)
|
|
128
|
+
.select([...columns, "_score"])
|
|
129
|
+
.where(where)
|
|
130
|
+
.limit(PRE_K)
|
|
131
|
+
.toArray());
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
console.warn(`[sanity] FTS unavailable for "${target}": ${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
const candidateScores = new Map();
|
|
137
|
+
const docMap = new Map();
|
|
138
|
+
vectorRows.forEach((doc, rank) => {
|
|
139
|
+
const key = doc.id ||
|
|
140
|
+
`${doc.path}:${doc.chunk_index}`;
|
|
141
|
+
docMap.set(key, doc);
|
|
142
|
+
candidateScores.set(key, (candidateScores.get(key) || 0) + 1.0 / (RRF_K + rank + 1));
|
|
143
|
+
});
|
|
144
|
+
ftsRows.forEach((doc, rank) => {
|
|
145
|
+
const key = doc.id ||
|
|
146
|
+
`${doc.path}:${doc.chunk_index}`;
|
|
147
|
+
if (!docMap.has(key))
|
|
148
|
+
docMap.set(key, doc);
|
|
149
|
+
candidateScores.set(key, (candidateScores.get(key) || 0) + 1.0 / (RRF_K + rank + 1));
|
|
150
|
+
});
|
|
151
|
+
const fused = Array.from(candidateScores.entries())
|
|
152
|
+
.sort((a, b) => b[1] - a[1])
|
|
153
|
+
.slice(0, STAGE1_K)
|
|
154
|
+
.map(([key]) => docMap.get(key))
|
|
155
|
+
.filter(Boolean);
|
|
156
|
+
let withRef = 0;
|
|
157
|
+
let withDef = 0;
|
|
158
|
+
let rankOfFirstRef = -1;
|
|
159
|
+
let rankOfFirstDef = -1;
|
|
160
|
+
const sampleRefHits = [];
|
|
161
|
+
fused.forEach((doc, i) => {
|
|
162
|
+
const refs = toStringArray(doc.referenced_symbols);
|
|
163
|
+
const defs = toStringArray(doc.defined_symbols);
|
|
164
|
+
if (refs.includes(target)) {
|
|
165
|
+
withRef++;
|
|
166
|
+
if (rankOfFirstRef < 0)
|
|
167
|
+
rankOfFirstRef = i + 1;
|
|
168
|
+
if (sampleRefHits.length < 3) {
|
|
169
|
+
sampleRefHits.push({ rank: i + 1, path: String(doc.path) });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (defs.includes(target)) {
|
|
173
|
+
withDef++;
|
|
174
|
+
if (rankOfFirstDef < 0)
|
|
175
|
+
rankOfFirstDef = i + 1;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
yield db.close();
|
|
179
|
+
return {
|
|
180
|
+
target,
|
|
181
|
+
poolSize: fused.length,
|
|
182
|
+
vectorRows: vectorRows.length,
|
|
183
|
+
ftsRows: ftsRows.length,
|
|
184
|
+
withRef,
|
|
185
|
+
withDef,
|
|
186
|
+
rankOfFirstRef,
|
|
187
|
+
rankOfFirstDef,
|
|
188
|
+
sampleRefHits,
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function main() {
|
|
193
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
194
|
+
console.log(`Phase 0 sanity check — graph reachability on platform hard-miss cases`);
|
|
195
|
+
console.log(`pathPrefix: ${PLATFORM_ROOT}`);
|
|
196
|
+
console.log(`STAGE1_K=${STAGE1_K}, PRE_K=${PRE_K}\n`);
|
|
197
|
+
let anyReachable = false;
|
|
198
|
+
const summary = [];
|
|
199
|
+
for (const target of HARD_MISS_TARGETS) {
|
|
200
|
+
const res = yield probe(target);
|
|
201
|
+
summary.push(res);
|
|
202
|
+
const refOK = res.withRef > 0 ? "✓" : "✗";
|
|
203
|
+
const defOK = res.withDef > 0 ? "✓" : "✗";
|
|
204
|
+
console.log(`${refOK} ${target.padEnd(16)} pool=${String(res.poolSize).padStart(3)} ` +
|
|
205
|
+
`refs=${String(res.withRef).padStart(3)}/200 defs=${defOK} (${res.withDef}) ` +
|
|
206
|
+
`1st-ref@${res.rankOfFirstRef > 0 ? res.rankOfFirstRef : "—"} ` +
|
|
207
|
+
`1st-def@${res.rankOfFirstDef > 0 ? res.rankOfFirstDef : "—"}`);
|
|
208
|
+
for (const s of res.sampleRefHits) {
|
|
209
|
+
console.log(` ↳ rank ${s.rank}: ${s.path}`);
|
|
210
|
+
}
|
|
211
|
+
if (res.withRef > 0)
|
|
212
|
+
anyReachable = true;
|
|
213
|
+
}
|
|
214
|
+
console.log(`\nVerdict: ${anyReachable ? "BUILD (≥1 case has graph signal in top-200)" : "ABORT (graph is empty at this depth — pick a different mechanism)"}`);
|
|
215
|
+
console.log(`\nJSON:`);
|
|
216
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
217
|
+
yield (0, exit_1.gracefulExit)(0);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (require.main === module) {
|
|
221
|
+
main().catch((e) => {
|
|
222
|
+
console.error(e);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pulls the chunks for a known BeyondError-using file out of the index
|
|
4
|
+
* and prints their `referenced_symbols` arrays. Lets us see what the
|
|
5
|
+
* chunker actually extracts vs what's missing — whether BeyondError is
|
|
6
|
+
* absent due to chunker scope (e.g., only same-function refs are tagged)
|
|
7
|
+
* or whether the tagging is just sparse.
|
|
8
|
+
*/
|
|
9
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
10
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
11
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
12
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
13
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
14
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
15
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
var _a;
|
|
19
|
+
var _b;
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
(_a = (_b = process.env).GMAX_WORKER_COUNT) !== null && _a !== void 0 ? _a : (_b.GMAX_WORKER_COUNT = "1");
|
|
22
|
+
const config_1 = require("./config");
|
|
23
|
+
const vector_db_1 = require("./lib/store/vector-db");
|
|
24
|
+
const exit_1 = require("./lib/utils/exit");
|
|
25
|
+
const filter_builder_1 = require("./lib/utils/filter-builder");
|
|
26
|
+
const CANDIDATES = [
|
|
27
|
+
"/Users/reoiv/Development/beyond/platform/packages/api/src/middleware/error.ts",
|
|
28
|
+
"/Users/reoiv/Development/beyond/platform/packages/api/src/graphql/shared/error-mapping.ts",
|
|
29
|
+
"/Users/reoiv/Development/beyond/platform/packages/api/src/lib/service-errors.ts",
|
|
30
|
+
];
|
|
31
|
+
function toStrArr(v) {
|
|
32
|
+
if (!v)
|
|
33
|
+
return [];
|
|
34
|
+
if (Array.isArray(v))
|
|
35
|
+
return v.filter((x) => typeof x === "string");
|
|
36
|
+
const m = v;
|
|
37
|
+
if (typeof m.toArray === "function") {
|
|
38
|
+
try {
|
|
39
|
+
const a = m.toArray();
|
|
40
|
+
return Array.isArray(a) ? a.filter((x) => typeof x === "string") : [];
|
|
41
|
+
}
|
|
42
|
+
catch (_a) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
function main() {
|
|
49
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
50
|
+
const db = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir);
|
|
51
|
+
const table = yield db.ensureTable();
|
|
52
|
+
for (const file of CANDIDATES) {
|
|
53
|
+
console.log(`\n── ${file}`);
|
|
54
|
+
const rows = (yield table
|
|
55
|
+
.query()
|
|
56
|
+
.select(["start_line", "end_line", "chunk_type", "defined_symbols", "referenced_symbols"])
|
|
57
|
+
.where(`path = '${(0, filter_builder_1.escapeSqlString)(file)}'`)
|
|
58
|
+
.limit(20)
|
|
59
|
+
.toArray());
|
|
60
|
+
if (rows.length === 0) {
|
|
61
|
+
console.log(" (no chunks indexed for this path)");
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
for (const r of rows) {
|
|
65
|
+
const defs = toStrArr(r.defined_symbols);
|
|
66
|
+
const refs = toStrArr(r.referenced_symbols);
|
|
67
|
+
const hasBE = refs.includes("BeyondError");
|
|
68
|
+
console.log(` lines ${r.start_line}-${r.end_line} (${r.chunk_type}) ` +
|
|
69
|
+
`defs=[${defs.slice(0, 4).join(",")}${defs.length > 4 ? "…" : ""}] ` +
|
|
70
|
+
`refs(${refs.length})=[${refs.slice(0, 8).join(",")}${refs.length > 8 ? "…" : ""}] ` +
|
|
71
|
+
`BeyondError-ref=${hasBE ? "✓" : "✗"}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
yield db.close();
|
|
75
|
+
yield (0, exit_1.gracefulExit)(0);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (require.main === module) {
|
|
79
|
+
main().catch((e) => {
|
|
80
|
+
console.error(e);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Diagnostic counterpart to eval-graph-sanity.ts.
|
|
4
|
+
*
|
|
5
|
+
* Phase 0 found that 0/200 fusion-pool chunks reference any of the 4
|
|
6
|
+
* hard-miss targets via `referenced_symbols`. That can mean either:
|
|
7
|
+
* (a) chunker isn't extracting these refs (upstream miss), OR
|
|
8
|
+
* (b) refs exist but live outside the fusion top-200.
|
|
9
|
+
*
|
|
10
|
+
* This script measures the *total* `array_contains(referenced_symbols, X)`
|
|
11
|
+
* count across the whole platform index for each target. If those counts
|
|
12
|
+
* are also ~0, the graph is empty at the data-extraction layer, not the
|
|
13
|
+
* retrieval layer.
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
49
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
50
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
51
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
52
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
53
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
54
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
var _a, _b;
|
|
58
|
+
var _c;
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
(_a = (_c = process.env).GMAX_WORKER_COUNT) !== null && _a !== void 0 ? _a : (_c.GMAX_WORKER_COUNT = "1");
|
|
61
|
+
const path = __importStar(require("node:path"));
|
|
62
|
+
const config_1 = require("./config");
|
|
63
|
+
const vector_db_1 = require("./lib/store/vector-db");
|
|
64
|
+
const exit_1 = require("./lib/utils/exit");
|
|
65
|
+
const filter_builder_1 = require("./lib/utils/filter-builder");
|
|
66
|
+
const PLATFORM_ROOT = path.join((_b = process.env.HOME) !== null && _b !== void 0 ? _b : "", "Development/beyond/platform");
|
|
67
|
+
const TARGETS = ["BeyondError", "ErrorCodes", "resolveActor", "errorHandler"];
|
|
68
|
+
function main() {
|
|
69
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
const db = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir);
|
|
71
|
+
const table = yield db.ensureTable();
|
|
72
|
+
const pathPrefix = PLATFORM_ROOT.endsWith("/") ? PLATFORM_ROOT : `${PLATFORM_ROOT}/`;
|
|
73
|
+
const scope = `path LIKE '${(0, filter_builder_1.escapeSqlString)(pathPrefix)}%'`;
|
|
74
|
+
console.log(`Platform graph density check — pathPrefix=${PLATFORM_ROOT}\n`);
|
|
75
|
+
for (const sym of TARGETS) {
|
|
76
|
+
const esc = (0, filter_builder_1.escapeSqlString)(sym);
|
|
77
|
+
const refRows = yield table
|
|
78
|
+
.query()
|
|
79
|
+
.select(["path"])
|
|
80
|
+
.where(`${scope} AND array_contains(referenced_symbols, '${esc}')`)
|
|
81
|
+
.limit(2000)
|
|
82
|
+
.toArray();
|
|
83
|
+
const defRows = yield table
|
|
84
|
+
.query()
|
|
85
|
+
.select(["path"])
|
|
86
|
+
.where(`${scope} AND array_contains(defined_symbols, '${esc}')`)
|
|
87
|
+
.limit(2000)
|
|
88
|
+
.toArray();
|
|
89
|
+
console.log(`${sym.padEnd(16)} def-chunks=${String(defRows.length).padStart(3)} ref-chunks=${String(refRows.length).padStart(4)}`);
|
|
90
|
+
}
|
|
91
|
+
// Also count chunks with non-empty referenced_symbols overall to baseline
|
|
92
|
+
// graph density.
|
|
93
|
+
const allRows = yield table
|
|
94
|
+
.query()
|
|
95
|
+
.select(["referenced_symbols"])
|
|
96
|
+
.where(scope)
|
|
97
|
+
.limit(20000)
|
|
98
|
+
.toArray();
|
|
99
|
+
let nonEmpty = 0;
|
|
100
|
+
let totalRefs = 0;
|
|
101
|
+
for (const row of allRows) {
|
|
102
|
+
const raw = row.referenced_symbols;
|
|
103
|
+
let arr = [];
|
|
104
|
+
if (Array.isArray(raw))
|
|
105
|
+
arr = raw.filter((v) => typeof v === "string");
|
|
106
|
+
else if (raw && typeof raw.toArray === "function") {
|
|
107
|
+
try {
|
|
108
|
+
const a = raw.toArray();
|
|
109
|
+
if (Array.isArray(a))
|
|
110
|
+
arr = a.filter((v) => typeof v === "string");
|
|
111
|
+
}
|
|
112
|
+
catch (_a) { }
|
|
113
|
+
}
|
|
114
|
+
if (arr.length > 0) {
|
|
115
|
+
nonEmpty++;
|
|
116
|
+
totalRefs += arr.length;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
console.log(`\nPlatform corpus: ${allRows.length} chunks sampled (cap 20k), ` +
|
|
120
|
+
`${nonEmpty} with non-empty referenced_symbols (${((nonEmpty / allRows.length) * 100).toFixed(1)}%), ` +
|
|
121
|
+
`avg refs/chunk = ${(totalRefs / Math.max(1, nonEmpty)).toFixed(1)}`);
|
|
122
|
+
yield db.close();
|
|
123
|
+
yield (0, exit_1.gracefulExit)(0);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (require.main === module) {
|
|
127
|
+
main().catch((e) => {
|
|
128
|
+
console.error(e);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
});
|
|
131
|
+
}
|
package/dist/eval-oss.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OSS-fixture evaluation harness — runs the search pipeline against
|
|
4
|
+
* sverklo-bench P1 (definition lookup) fixtures ported from
|
|
5
|
+
* github.com/sverklo/sverklo-bench (tasks/express.gen.ts,
|
|
6
|
+
* tasks/lodash.gen.ts). Used to answer "does ColBERT rerank help on a
|
|
7
|
+
* third-party fixture set, or are our 97 internal cases biased?"
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* pnpm tsx src/eval-oss.ts express # rerank off (default)
|
|
11
|
+
* GMAX_EVAL_RERANK=1 pnpm tsx src/eval-oss.ts express
|
|
12
|
+
* pnpm tsx src/eval-oss.ts lodash --json
|
|
13
|
+
* pnpm tsx src/eval-oss.ts all # all datasets
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
49
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
50
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
51
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
52
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
53
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
54
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
var _a, _b, _c, _d;
|
|
58
|
+
var _e;
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
// Same precaution as src/eval.ts — pin worker pool to 1 to avoid ONNX
|
|
61
|
+
// concurrency issues during the back-to-back search runs.
|
|
62
|
+
(_a = (_e = process.env).GMAX_WORKER_COUNT) !== null && _a !== void 0 ? _a : (_e.GMAX_WORKER_COUNT = "1");
|
|
63
|
+
const path = __importStar(require("node:path"));
|
|
64
|
+
const searcher_1 = require("./lib/search/searcher");
|
|
65
|
+
const vector_db_1 = require("./lib/store/vector-db");
|
|
66
|
+
const exit_1 = require("./lib/utils/exit");
|
|
67
|
+
const config_1 = require("./config");
|
|
68
|
+
// ─── express 4.21.1 P1 — sverklo-bench/tasks/express.gen.ts resolved against
|
|
69
|
+
// a fresh checkout. `merge` dropped: it's the utils-merge package, not
|
|
70
|
+
// an in-tree definition, so the sverklo resolver returns no location.
|
|
71
|
+
const EXPRESS_CASES = [
|
|
72
|
+
{ id: "ex-p1-01", query: "createApplication", expectedFile: "lib/express.js", expectedLine: 37 },
|
|
73
|
+
{ id: "ex-p1-02", query: "Route", expectedFile: "lib/router/route.js", expectedLine: 43 },
|
|
74
|
+
{ id: "ex-p1-03", query: "Layer", expectedFile: "lib/router/layer.js", expectedLine: 33 },
|
|
75
|
+
{ id: "ex-p1-04", query: "View", expectedFile: "lib/view.js", expectedLine: 52 },
|
|
76
|
+
{ id: "ex-p1-05", query: "query", expectedFile: "lib/express.js", expectedLine: 79, note: "re-export site — middleware/query.js doesn't match the function-decl patterns" },
|
|
77
|
+
{ id: "ex-p1-06", query: "init", expectedFile: "lib/middleware/init.js", expectedLine: 28 },
|
|
78
|
+
{ id: "ex-p1-07", query: "acceptParams", expectedFile: "lib/utils.js", expectedLine: 126 },
|
|
79
|
+
{ id: "ex-p1-08", query: "stringify", expectedFile: "lib/response.js", expectedLine: 1155 },
|
|
80
|
+
{ id: "ex-p1-09", query: "compileETag", expectedFile: "lib/utils.js", expectedLine: 150 },
|
|
81
|
+
];
|
|
82
|
+
// ─── lodash 4.17.21 P1 — hand-verified line numbers in lodash.js, copied
|
|
83
|
+
// verbatim from sverklo-bench/tasks/lodash.gen.ts. All 10 live in the
|
|
84
|
+
// same 17K-line UMD file, so line-window matching (NOT path-only match)
|
|
85
|
+
// is what makes this fixture set discriminating.
|
|
86
|
+
const LODASH_CASES = [
|
|
87
|
+
{ id: "ld-p1-01", query: "map", expectedFile: "lodash.js", expectedLine: 9620 },
|
|
88
|
+
{ id: "ld-p1-02", query: "filter", expectedFile: "lodash.js", expectedLine: 9239 },
|
|
89
|
+
{ id: "ld-p1-03", query: "reduce", expectedFile: "lodash.js", expectedLine: 9745 },
|
|
90
|
+
{ id: "ld-p1-04", query: "debounce", expectedFile: "lodash.js", expectedLine: 10372 },
|
|
91
|
+
{ id: "ld-p1-05", query: "throttle", expectedFile: "lodash.js", expectedLine: 10965 },
|
|
92
|
+
{ id: "ld-p1-06", query: "merge", expectedFile: "lodash.js", expectedLine: 13505, note: "var merge = createAssigner(...) binding site, not the re-export at 16689" },
|
|
93
|
+
{ id: "ld-p1-07", query: "cloneDeep", expectedFile: "lodash.js", expectedLine: 11155 },
|
|
94
|
+
{ id: "ld-p1-08", query: "get", expectedFile: "lodash.js", expectedLine: 13194 },
|
|
95
|
+
{ id: "ld-p1-09", query: "set", expectedFile: "lodash.js", expectedLine: 13741 },
|
|
96
|
+
{ id: "ld-p1-10", query: "chunk", expectedFile: "lodash.js", expectedLine: 6903 },
|
|
97
|
+
];
|
|
98
|
+
// ─── platform monorepo (private) — 15 hand-curated P1 cases across packages
|
|
99
|
+
// to test the "modular monorepo" shape that neither express nor lodash
|
|
100
|
+
// covers. Symbols resolved against the live checkout. Bare-symbol
|
|
101
|
+
// queries match sverklo's P1 methodology so results are comparable.
|
|
102
|
+
const PLATFORM_CASES = [
|
|
103
|
+
{ id: "pf-p1-01", query: "formatCents", expectedFile: "packages/shared/src/format.ts", expectedLine: 8 },
|
|
104
|
+
{ id: "pf-p1-02", query: "formatTimeAgo", expectedFile: "packages/shared/src/format.ts", expectedLine: 44 },
|
|
105
|
+
{ id: "pf-p1-03", query: "BeyondError", expectedFile: "packages/shared/src/errors.ts", expectedLine: 37 },
|
|
106
|
+
{ id: "pf-p1-04", query: "ErrorCodes", expectedFile: "packages/shared/src/errors.ts", expectedLine: 5 },
|
|
107
|
+
{ id: "pf-p1-05", query: "createDb", expectedFile: "packages/db/src/index.ts", expectedLine: 42 },
|
|
108
|
+
{ id: "pf-p1-06", query: "createDbAsync", expectedFile: "packages/db/src/index.ts", expectedLine: 50 },
|
|
109
|
+
{ id: "pf-p1-07", query: "authMiddleware", expectedFile: "packages/api/src/middleware/auth.ts", expectedLine: 39 },
|
|
110
|
+
{ id: "pf-p1-08", query: "requireAuth", expectedFile: "packages/api/src/middleware/auth.ts", expectedLine: 45 },
|
|
111
|
+
{ id: "pf-p1-09", query: "resolveActor", expectedFile: "packages/api/src/middleware/auth.ts", expectedLine: 71 },
|
|
112
|
+
{ id: "pf-p1-10", query: "getActor", expectedFile: "packages/api/src/middleware/auth.ts", expectedLine: 1136 },
|
|
113
|
+
{ id: "pf-p1-11", query: "rateLimit", expectedFile: "packages/api/src/middleware/rate-limit.ts", expectedLine: 94 },
|
|
114
|
+
{ id: "pf-p1-12", query: "checkRateLimitKey", expectedFile: "packages/api/src/middleware/rate-limit.ts", expectedLine: 158 },
|
|
115
|
+
{ id: "pf-p1-13", query: "errorHandler", expectedFile: "packages/api/src/middleware/error.ts", expectedLine: 128 },
|
|
116
|
+
{ id: "pf-p1-14", query: "activityTracker", expectedFile: "packages/api/src/middleware/activity-tracker.ts", expectedLine: 17 },
|
|
117
|
+
{ id: "pf-p1-15", query: "initializeApp", expectedFile: "packages/api/src/app.ts", expectedLine: 68 },
|
|
118
|
+
];
|
|
119
|
+
const DATASETS = {
|
|
120
|
+
express: {
|
|
121
|
+
name: "express",
|
|
122
|
+
projectRoot: path.join((_b = process.env.HOME) !== null && _b !== void 0 ? _b : "", "Development/sandbox/bench-fixtures/express"),
|
|
123
|
+
cases: EXPRESS_CASES,
|
|
124
|
+
},
|
|
125
|
+
lodash: {
|
|
126
|
+
name: "lodash",
|
|
127
|
+
projectRoot: path.join((_c = process.env.HOME) !== null && _c !== void 0 ? _c : "", "Development/sandbox/bench-fixtures/lodash"),
|
|
128
|
+
cases: LODASH_CASES,
|
|
129
|
+
},
|
|
130
|
+
platform: {
|
|
131
|
+
name: "platform",
|
|
132
|
+
projectRoot: path.join((_d = process.env.HOME) !== null && _d !== void 0 ? _d : "", "Development/beyond/platform"),
|
|
133
|
+
cases: PLATFORM_CASES,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
// A chunk matches when its file path ends with the expected file AND the
|
|
137
|
+
// expected line falls within [start_line, end_line]. end_line falls back
|
|
138
|
+
// to start_line + 200 when missing (lodash's mega-IIFE chunks are
|
|
139
|
+
// typically much smaller than this, but the guard avoids false negatives
|
|
140
|
+
// from short tail chunks).
|
|
141
|
+
function chunkMatches(chunk, expectedFile, expectedLine) {
|
|
142
|
+
var _a, _b, _c, _d, _e, _f;
|
|
143
|
+
const path = String(((_a = chunk.metadata) === null || _a === void 0 ? void 0 : _a.path) || "").toLowerCase();
|
|
144
|
+
if (!path.endsWith(`/${expectedFile.toLowerCase()}`) && !path.endsWith(expectedFile.toLowerCase())) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const start = Number((_d = (_c = (_b = chunk.generated_metadata) === null || _b === void 0 ? void 0 : _b.start_line) !== null && _c !== void 0 ? _c : chunk.start_line) !== null && _d !== void 0 ? _d : 0);
|
|
148
|
+
const numLines = Number((_f = (_e = chunk.generated_metadata) === null || _e === void 0 ? void 0 : _e.num_lines) !== null && _f !== void 0 ? _f : 0);
|
|
149
|
+
const end = numLines > 0 ? start + numLines : start + 200;
|
|
150
|
+
// chunks are 0-indexed start_line; expected line is 1-indexed
|
|
151
|
+
return expectedLine - 1 >= start && expectedLine - 1 <= end;
|
|
152
|
+
}
|
|
153
|
+
function evaluateOss(response, c, timeMs) {
|
|
154
|
+
const idx = response.data.findIndex((chunk) => chunkMatches(chunk, c.expectedFile, c.expectedLine));
|
|
155
|
+
const rank = idx + 1; // 0 = miss
|
|
156
|
+
const rr = rank > 0 ? 1 / rank : 0;
|
|
157
|
+
const recall10 = rank > 0 && rank <= 10 ? 1 : 0;
|
|
158
|
+
return {
|
|
159
|
+
id: c.id,
|
|
160
|
+
query: c.query,
|
|
161
|
+
expectedFile: c.expectedFile,
|
|
162
|
+
expectedLine: c.expectedLine,
|
|
163
|
+
rank,
|
|
164
|
+
rr,
|
|
165
|
+
recall10,
|
|
166
|
+
timeMs,
|
|
167
|
+
note: c.note,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function runDataset(ds, rerank, topK) {
|
|
171
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
172
|
+
const vectorDb = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir);
|
|
173
|
+
const searcher = new searcher_1.Searcher(vectorDb);
|
|
174
|
+
const pathPrefix = ds.projectRoot.endsWith("/") ? ds.projectRoot : `${ds.projectRoot}/`;
|
|
175
|
+
const results = [];
|
|
176
|
+
for (const c of ds.cases) {
|
|
177
|
+
const t0 = performance.now();
|
|
178
|
+
const res = yield searcher.search(c.query, topK, { rerank }, undefined, pathPrefix);
|
|
179
|
+
const timeMs = performance.now() - t0;
|
|
180
|
+
results.push(evaluateOss(res, c, timeMs));
|
|
181
|
+
}
|
|
182
|
+
yield vectorDb.close();
|
|
183
|
+
const hits = results.filter((r) => r.rank > 0).length;
|
|
184
|
+
const hitsAt1 = results.filter((r) => r.rank === 1).length;
|
|
185
|
+
const mrr = results.reduce((s, r) => s + r.rr, 0) / results.length;
|
|
186
|
+
const recall10 = results.reduce((s, r) => s + r.recall10, 0) / results.length;
|
|
187
|
+
const avgTime = results.reduce((s, r) => s + r.timeMs, 0) / results.length;
|
|
188
|
+
return {
|
|
189
|
+
summary: {
|
|
190
|
+
dataset: ds.name,
|
|
191
|
+
rerank,
|
|
192
|
+
cases: results.length,
|
|
193
|
+
hits,
|
|
194
|
+
hitsAt1,
|
|
195
|
+
mrrAt10: Number(mrr.toFixed(4)),
|
|
196
|
+
recallAt10: Number(recall10.toFixed(4)),
|
|
197
|
+
avgTimeMs: Math.round(avgTime),
|
|
198
|
+
},
|
|
199
|
+
results,
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function run() {
|
|
204
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
205
|
+
var _a;
|
|
206
|
+
const args = process.argv.slice(2);
|
|
207
|
+
const datasetArg = (_a = args.find((a) => !a.startsWith("--"))) !== null && _a !== void 0 ? _a : "all";
|
|
208
|
+
const jsonMode = args.includes("--json") || process.env.GMAX_EVAL_JSON === "1";
|
|
209
|
+
const rerank = process.env.GMAX_EVAL_RERANK === "1";
|
|
210
|
+
const topK = 20;
|
|
211
|
+
const wanted = datasetArg === "all" ? Object.keys(DATASETS) : [datasetArg];
|
|
212
|
+
const out = [];
|
|
213
|
+
const log = jsonMode ? console.error : console.log;
|
|
214
|
+
log(`OSS eval (rerank=${rerank ? "on" : "off"})`);
|
|
215
|
+
for (const name of wanted) {
|
|
216
|
+
const ds = DATASETS[name];
|
|
217
|
+
if (!ds) {
|
|
218
|
+
console.error(`Unknown dataset: ${name}. Known: ${Object.keys(DATASETS).join(", ")}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
log(`\n── ${ds.name} (${ds.cases.length} cases, ${ds.projectRoot})`);
|
|
222
|
+
const r = yield runDataset(ds, rerank, topK);
|
|
223
|
+
out.push(r);
|
|
224
|
+
if (!jsonMode) {
|
|
225
|
+
for (const res of r.results) {
|
|
226
|
+
const status = res.rank > 0 ? `rank ${res.rank}` : "miss";
|
|
227
|
+
console.log(` ${res.id} ${res.query.padEnd(20)} → ${status.padEnd(8)} ${res.expectedFile}:${res.expectedLine} [${res.timeMs.toFixed(0)}ms]`);
|
|
228
|
+
}
|
|
229
|
+
const s = r.summary;
|
|
230
|
+
console.log(` → MRR@10=${s.mrrAt10} Recall@10=${s.recallAt10} hits@1=${s.hitsAt1}/${s.cases} avg=${s.avgTimeMs}ms`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (jsonMode) {
|
|
234
|
+
process.stdout.write(`${JSON.stringify({ rerank, datasets: out }, null, 2)}\n`);
|
|
235
|
+
}
|
|
236
|
+
yield (0, exit_1.gracefulExit)(0);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (require.main === module && process.env.GMAX_EVAL_AUTORUN !== "0") {
|
|
240
|
+
run().catch((e) => {
|
|
241
|
+
console.error(e);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
});
|
|
244
|
+
}
|
package/dist/eval.js
CHANGED
|
@@ -582,9 +582,14 @@ function run() {
|
|
|
582
582
|
// changes show up clearly. Set GMAX_EVAL_RERANK=1 to measure the full
|
|
583
583
|
// production pipeline (slower, but more representative).
|
|
584
584
|
const rerank = process.env.GMAX_EVAL_RERANK === "1";
|
|
585
|
+
// Optional pathPrefix scoping — needed by PageRank tiebreaker (which is
|
|
586
|
+
// per-project) and to isolate this bench from cross-project chunks indexed
|
|
587
|
+
// by the same daemon. Defaults to unscoped so historical numbers stay
|
|
588
|
+
// comparable.
|
|
589
|
+
const evalPathPrefix = process.env.GMAX_EVAL_PATH_PREFIX || undefined;
|
|
585
590
|
for (const c of exports.cases) {
|
|
586
591
|
const queryStart = performance.now();
|
|
587
|
-
const res = yield searcher.search(c.query, topK, { rerank });
|
|
592
|
+
const res = yield searcher.search(c.query, topK, { rerank }, undefined, evalPathPrefix);
|
|
588
593
|
const queryEnd = performance.now();
|
|
589
594
|
const timeMs = queryEnd - queryStart;
|
|
590
595
|
results.push(evaluateCase(res, c, timeMs));
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.computePageRank = computePageRank;
|
|
46
|
+
exports.buildGraphFromDb = buildGraphFromDb;
|
|
47
|
+
exports.writeDiskCache = writeDiskCache;
|
|
48
|
+
exports.loadOrComputePageRank = loadOrComputePageRank;
|
|
49
|
+
exports.pageRankBoostForSymbols = pageRankBoostForSymbols;
|
|
50
|
+
exports._clearMemoryCacheForTests = _clearMemoryCacheForTests;
|
|
51
|
+
exports._cachePathForTests = _cachePathForTests;
|
|
52
|
+
const crypto = __importStar(require("node:crypto"));
|
|
53
|
+
const fs = __importStar(require("node:fs"));
|
|
54
|
+
const path = __importStar(require("node:path"));
|
|
55
|
+
const config_1 = require("../../config");
|
|
56
|
+
const filter_builder_1 = require("../utils/filter-builder");
|
|
57
|
+
const DEFAULT_DAMPING = 0.85;
|
|
58
|
+
const DEFAULT_MAX_ITER = 50;
|
|
59
|
+
const DEFAULT_TOL = 1e-6;
|
|
60
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1000;
|
|
61
|
+
const memoryCache = new Map();
|
|
62
|
+
function computePageRank(graph, damping = DEFAULT_DAMPING, maxIter = DEFAULT_MAX_ITER, tol = DEFAULT_TOL) {
|
|
63
|
+
const N = graph.nodes.length;
|
|
64
|
+
const result = new Map();
|
|
65
|
+
if (N === 0)
|
|
66
|
+
return result;
|
|
67
|
+
const idx = new Map();
|
|
68
|
+
for (let i = 0; i < N; i++)
|
|
69
|
+
idx.set(graph.nodes[i], i);
|
|
70
|
+
const outNeighbors = Array.from({ length: N }, () => []);
|
|
71
|
+
for (const [src, targets] of graph.edges) {
|
|
72
|
+
const si = idx.get(src);
|
|
73
|
+
if (si === undefined)
|
|
74
|
+
continue;
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
for (const tgt of targets) {
|
|
77
|
+
const ti = idx.get(tgt);
|
|
78
|
+
if (ti === undefined || ti === si || seen.has(ti))
|
|
79
|
+
continue;
|
|
80
|
+
seen.add(ti);
|
|
81
|
+
outNeighbors[si].push(ti);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const outDegree = new Int32Array(N);
|
|
85
|
+
for (let i = 0; i < N; i++)
|
|
86
|
+
outDegree[i] = outNeighbors[i].length;
|
|
87
|
+
const inNeighbors = Array.from({ length: N }, () => []);
|
|
88
|
+
for (let i = 0; i < N; i++) {
|
|
89
|
+
for (const j of outNeighbors[i])
|
|
90
|
+
inNeighbors[j].push(i);
|
|
91
|
+
}
|
|
92
|
+
let pr = new Float64Array(N).fill(1 / N);
|
|
93
|
+
let next = new Float64Array(N);
|
|
94
|
+
const teleport = (1 - damping) / N;
|
|
95
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
96
|
+
let dangling = 0;
|
|
97
|
+
for (let i = 0; i < N; i++) {
|
|
98
|
+
if (outDegree[i] === 0)
|
|
99
|
+
dangling += pr[i];
|
|
100
|
+
}
|
|
101
|
+
const danglingShare = (damping * dangling) / N;
|
|
102
|
+
for (let i = 0; i < N; i++) {
|
|
103
|
+
let sum = 0;
|
|
104
|
+
const ins = inNeighbors[i];
|
|
105
|
+
for (let k = 0; k < ins.length; k++) {
|
|
106
|
+
const j = ins[k];
|
|
107
|
+
sum += pr[j] / outDegree[j];
|
|
108
|
+
}
|
|
109
|
+
next[i] = teleport + danglingShare + damping * sum;
|
|
110
|
+
}
|
|
111
|
+
let delta = 0;
|
|
112
|
+
for (let i = 0; i < N; i++)
|
|
113
|
+
delta += Math.abs(next[i] - pr[i]);
|
|
114
|
+
const tmp = pr;
|
|
115
|
+
pr = next;
|
|
116
|
+
next = tmp;
|
|
117
|
+
if (delta < tol)
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
for (let i = 0; i < N; i++)
|
|
121
|
+
result.set(graph.nodes[i], pr[i]);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
function toStringArray(val) {
|
|
125
|
+
if (!val)
|
|
126
|
+
return [];
|
|
127
|
+
if (Array.isArray(val))
|
|
128
|
+
return val.filter((v) => typeof v === "string");
|
|
129
|
+
const maybe = val;
|
|
130
|
+
if (typeof maybe.toArray === "function") {
|
|
131
|
+
try {
|
|
132
|
+
const arr = maybe.toArray();
|
|
133
|
+
return Array.isArray(arr)
|
|
134
|
+
? arr.filter((v) => typeof v === "string")
|
|
135
|
+
: [];
|
|
136
|
+
}
|
|
137
|
+
catch (_a) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
function buildGraphFromDb(db, pathPrefix) {
|
|
144
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
145
|
+
const table = yield db.ensureTable();
|
|
146
|
+
const prefix = pathPrefix.endsWith("/") ? pathPrefix : `${pathPrefix}/`;
|
|
147
|
+
const rows = yield table
|
|
148
|
+
.query()
|
|
149
|
+
.select(["defined_symbols", "referenced_symbols"])
|
|
150
|
+
.where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
|
|
151
|
+
.toArray();
|
|
152
|
+
const nodes = new Set();
|
|
153
|
+
const edges = new Map();
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
const defs = toStringArray(row.defined_symbols);
|
|
156
|
+
const refs = toStringArray(row.referenced_symbols);
|
|
157
|
+
for (const d of defs)
|
|
158
|
+
nodes.add(d);
|
|
159
|
+
if (refs.length === 0)
|
|
160
|
+
continue;
|
|
161
|
+
for (const d of defs) {
|
|
162
|
+
let set = edges.get(d);
|
|
163
|
+
if (!set) {
|
|
164
|
+
set = new Set();
|
|
165
|
+
edges.set(d, set);
|
|
166
|
+
}
|
|
167
|
+
for (const r of refs)
|
|
168
|
+
set.add(r);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { nodes: Array.from(nodes), edges };
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function cachePathFor(pathPrefix) {
|
|
175
|
+
const hash = crypto
|
|
176
|
+
.createHash("sha1")
|
|
177
|
+
.update(pathPrefix)
|
|
178
|
+
.digest("hex")
|
|
179
|
+
.slice(0, 16);
|
|
180
|
+
return path.join(config_1.PATHS.globalRoot, "pagerank", `${hash}.json`);
|
|
181
|
+
}
|
|
182
|
+
function getTtlMs() {
|
|
183
|
+
var _a;
|
|
184
|
+
const env = Number.parseInt((_a = process.env.GMAX_PAGERANK_TTL_MS) !== null && _a !== void 0 ? _a : "", 10);
|
|
185
|
+
return Number.isFinite(env) && env > 0 ? env : DEFAULT_TTL_MS;
|
|
186
|
+
}
|
|
187
|
+
function readDiskCache(pathPrefix) {
|
|
188
|
+
const file = cachePathFor(pathPrefix);
|
|
189
|
+
if (!fs.existsSync(file))
|
|
190
|
+
return null;
|
|
191
|
+
try {
|
|
192
|
+
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
193
|
+
const computedAt = Date.parse(data.computedAt);
|
|
194
|
+
if (!Number.isFinite(computedAt))
|
|
195
|
+
return null;
|
|
196
|
+
if (Date.now() - computedAt > getTtlMs())
|
|
197
|
+
return null;
|
|
198
|
+
const scores = new Map();
|
|
199
|
+
let max = 0;
|
|
200
|
+
for (const [k, v] of Object.entries(data.scores)) {
|
|
201
|
+
const n = Number(v);
|
|
202
|
+
if (!Number.isFinite(n))
|
|
203
|
+
continue;
|
|
204
|
+
scores.set(k, n);
|
|
205
|
+
if (n > max)
|
|
206
|
+
max = n;
|
|
207
|
+
}
|
|
208
|
+
return { scores, max, computedAt };
|
|
209
|
+
}
|
|
210
|
+
catch (_a) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function writeDiskCache(pathPrefix, scores) {
|
|
215
|
+
const file = cachePathFor(pathPrefix);
|
|
216
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
217
|
+
const obj = {
|
|
218
|
+
pathPrefix,
|
|
219
|
+
computedAt: new Date().toISOString(),
|
|
220
|
+
nodeCount: scores.size,
|
|
221
|
+
scores: Object.fromEntries(scores),
|
|
222
|
+
};
|
|
223
|
+
fs.writeFileSync(file, JSON.stringify(obj), "utf8");
|
|
224
|
+
}
|
|
225
|
+
function loadOrComputePageRank(db, pathPrefix) {
|
|
226
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
227
|
+
const mem = memoryCache.get(pathPrefix);
|
|
228
|
+
if (mem && Date.now() - mem.computedAt < getTtlMs()) {
|
|
229
|
+
return { scores: mem.scores, max: mem.max };
|
|
230
|
+
}
|
|
231
|
+
const disk = readDiskCache(pathPrefix);
|
|
232
|
+
if (disk) {
|
|
233
|
+
memoryCache.set(pathPrefix, disk);
|
|
234
|
+
return { scores: disk.scores, max: disk.max };
|
|
235
|
+
}
|
|
236
|
+
const graph = yield buildGraphFromDb(db, pathPrefix);
|
|
237
|
+
const scores = computePageRank(graph);
|
|
238
|
+
let max = 0;
|
|
239
|
+
for (const v of scores.values())
|
|
240
|
+
if (v > max)
|
|
241
|
+
max = v;
|
|
242
|
+
const entry = { scores, max, computedAt: Date.now() };
|
|
243
|
+
memoryCache.set(pathPrefix, entry);
|
|
244
|
+
try {
|
|
245
|
+
writeDiskCache(pathPrefix, scores);
|
|
246
|
+
}
|
|
247
|
+
catch (_a) { }
|
|
248
|
+
return { scores, max };
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function pageRankBoostForSymbols(symbols, scores, max) {
|
|
252
|
+
if (!symbols || symbols.length === 0 || max <= 0)
|
|
253
|
+
return 0;
|
|
254
|
+
let best = 0;
|
|
255
|
+
for (const s of symbols) {
|
|
256
|
+
const v = scores.get(s);
|
|
257
|
+
if (v !== undefined && v > best)
|
|
258
|
+
best = v;
|
|
259
|
+
}
|
|
260
|
+
return best / max;
|
|
261
|
+
}
|
|
262
|
+
function _clearMemoryCacheForTests() {
|
|
263
|
+
memoryCache.clear();
|
|
264
|
+
}
|
|
265
|
+
function _cachePathForTests(pathPrefix) {
|
|
266
|
+
return cachePathFor(pathPrefix);
|
|
267
|
+
}
|
|
@@ -15,6 +15,7 @@ const config_1 = require("../../config");
|
|
|
15
15
|
const filter_builder_1 = require("../utils/filter-builder");
|
|
16
16
|
const pool_1 = require("../workers/pool");
|
|
17
17
|
const intent_1 = require("./intent");
|
|
18
|
+
const pagerank_1 = require("./pagerank");
|
|
18
19
|
function buildWhereClause(pathPrefix, filters, searchIntent) {
|
|
19
20
|
var _a;
|
|
20
21
|
const parts = [];
|
|
@@ -338,7 +339,7 @@ class Searcher {
|
|
|
338
339
|
}
|
|
339
340
|
search(query, top_k, _search_options, _filters, pathPrefix, intent, signal) {
|
|
340
341
|
return __awaiter(this, void 0, void 0, function* () {
|
|
341
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
342
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
342
343
|
const finalLimit = top_k !== null && top_k !== void 0 ? top_k : 10;
|
|
343
344
|
// ColBERT rerank is opt-in as of v0.17.1. On the 97-case eval it
|
|
344
345
|
// regresses MRR@10 by ~3% and doubles query latency; sweep across
|
|
@@ -371,7 +372,7 @@ class Searcher {
|
|
|
371
372
|
try {
|
|
372
373
|
table = yield this.db.ensureTable();
|
|
373
374
|
}
|
|
374
|
-
catch (
|
|
375
|
+
catch (_k) {
|
|
375
376
|
return { data: [] };
|
|
376
377
|
}
|
|
377
378
|
// Ensure FTS index exists (lazy init, retry periodically on failure)
|
|
@@ -391,10 +392,15 @@ class Searcher {
|
|
|
391
392
|
}
|
|
392
393
|
}
|
|
393
394
|
// Phase A: Lightweight retrieval — only columns needed for RRF, cosine, boost, dedup
|
|
395
|
+
// PageRank tiebreaker needs defined_symbols for the per-chunk lookup; include
|
|
396
|
+
// it in the lightweight path only when the flag is on so we don't bloat the
|
|
397
|
+
// default query path.
|
|
398
|
+
const pagerankEnabled = process.env.GMAX_PAGERANK === "1" && !!pathPrefix;
|
|
394
399
|
const LIGHTWEIGHT_COLUMNS = [
|
|
395
400
|
"id", "path", "hash", "chunk_index", "start_line", "end_line",
|
|
396
401
|
"is_anchor", "chunk_type", "role", "complexity", "is_exported",
|
|
397
402
|
"content", "parent_symbol", "referenced_symbols", "pooled_colbert_48d",
|
|
403
|
+
...(pagerankEnabled ? ["defined_symbols"] : []),
|
|
398
404
|
];
|
|
399
405
|
// _distance is auto-added by vectorSearch, _score by FTS — include each
|
|
400
406
|
// in the respective query to suppress LanceDB deprecation warnings
|
|
@@ -432,7 +438,7 @@ class Searcher {
|
|
|
432
438
|
this.ftsAvailable = true;
|
|
433
439
|
console.warn("[Searcher] Rebuilt FTS index with position support — retry search");
|
|
434
440
|
}
|
|
435
|
-
catch (
|
|
441
|
+
catch (_l) { }
|
|
436
442
|
}
|
|
437
443
|
else {
|
|
438
444
|
console.warn(`[Searcher] FTS search failed (will retry later): ${msg}`);
|
|
@@ -571,6 +577,40 @@ class Searcher {
|
|
|
571
577
|
: undefined,
|
|
572
578
|
};
|
|
573
579
|
});
|
|
580
|
+
// PageRank tiebreaker (opt-in via GMAX_PAGERANK=1). Small additive delta on
|
|
581
|
+
// top of the post-boost score, sourced from the per-project call graph. Read
|
|
582
|
+
// docs/plans/2026-05-25-semantic-search-landscape.md (Bundle B / G1) for the
|
|
583
|
+
// measurement criterion that decides whether this flag becomes default-on.
|
|
584
|
+
if (pagerankEnabled && pathPrefix) {
|
|
585
|
+
try {
|
|
586
|
+
const { scores: prScores, max: prMax } = yield (0, pagerank_1.loadOrComputePageRank)(this.db, pathPrefix);
|
|
587
|
+
if (prMax > 0) {
|
|
588
|
+
const envWeight = Number.parseFloat((_h = process.env.GMAX_PR_WEIGHT) !== null && _h !== void 0 ? _h : "");
|
|
589
|
+
const PR_WEIGHT = Number.isFinite(envWeight) && envWeight >= 0 ? envWeight : 0.05;
|
|
590
|
+
for (const item of scored) {
|
|
591
|
+
const raw = item.record.defined_symbols;
|
|
592
|
+
let defs = [];
|
|
593
|
+
if (Array.isArray(raw)) {
|
|
594
|
+
defs = raw.filter((v) => typeof v === "string");
|
|
595
|
+
}
|
|
596
|
+
else if (raw && typeof raw.toArray === "function") {
|
|
597
|
+
try {
|
|
598
|
+
const arr = raw.toArray();
|
|
599
|
+
if (Array.isArray(arr)) {
|
|
600
|
+
defs = arr.filter((v) => typeof v === "string");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
catch (_m) { }
|
|
604
|
+
}
|
|
605
|
+
const norm = (0, pagerank_1.pageRankBoostForSymbols)(defs, prScores, prMax);
|
|
606
|
+
item.score += PR_WEIGHT * norm;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (e) {
|
|
611
|
+
console.warn(`[Searcher] PageRank tiebreaker failed: ${e}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
574
614
|
// Note: "boosted" was not previously declared -- fix to use "scored"
|
|
575
615
|
scored.sort((a, b) => b.score - a.score);
|
|
576
616
|
// Item 11: Intelligent Deduplication
|
|
@@ -578,7 +618,7 @@ class Searcher {
|
|
|
578
618
|
// Item 10: Per-file diversification
|
|
579
619
|
const seenFiles = new Map();
|
|
580
620
|
const diversified = [];
|
|
581
|
-
const envMaxPerFile = Number.parseInt((
|
|
621
|
+
const envMaxPerFile = Number.parseInt((_j = process.env.GMAX_MAX_PER_FILE) !== null && _j !== void 0 ? _j : "", 10);
|
|
582
622
|
const MAX_PER_FILE = Number.isFinite(envMaxPerFile) && envMaxPerFile > 0 ? envMaxPerFile : 3;
|
|
583
623
|
for (const item of uniqueScored) {
|
|
584
624
|
const path = item.record.path || "";
|
|
@@ -25,6 +25,30 @@ os.environ["HF_HUB_DISABLE_IMPLICIT_TOKEN"] = "1"
|
|
|
25
25
|
os.environ["HF_HUB_VERBOSITY"] = "error"
|
|
26
26
|
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
|
|
27
27
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
28
|
+
|
|
29
|
+
# Auto-enable offline mode when the model is already cached, so steady-state
|
|
30
|
+
# startups don't HEAD-check huggingface.co (annoying firewall prompts, slow
|
|
31
|
+
# starts on flaky networks). First run with an empty cache stays online so
|
|
32
|
+
# the model can be fetched. Force one or the other with GMAX_HF_ONLINE=1
|
|
33
|
+
# or GMAX_HF_OFFLINE=1.
|
|
34
|
+
def _hf_cache_has_model(model_id: str) -> bool:
|
|
35
|
+
hf_home = os.environ.get("HF_HOME") or os.path.expanduser(
|
|
36
|
+
os.path.join("~", ".cache", "huggingface")
|
|
37
|
+
)
|
|
38
|
+
cache_dir = os.path.join(
|
|
39
|
+
hf_home, "hub", "models--" + model_id.replace("/", "--")
|
|
40
|
+
)
|
|
41
|
+
return os.path.isdir(cache_dir) and bool(os.listdir(cache_dir))
|
|
42
|
+
|
|
43
|
+
_model_id_for_cache_check = os.environ.get(
|
|
44
|
+
"MLX_EMBED_MODEL", "ibm-granite/granite-embedding-small-english-r2"
|
|
45
|
+
)
|
|
46
|
+
if os.environ.get("GMAX_HF_OFFLINE") == "1" or (
|
|
47
|
+
os.environ.get("GMAX_HF_ONLINE") != "1"
|
|
48
|
+
and _hf_cache_has_model(_model_id_for_cache_check)
|
|
49
|
+
):
|
|
50
|
+
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
51
|
+
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
|
28
52
|
warnings.filterwarnings("ignore", message=".*PyTorch.*")
|
|
29
53
|
warnings.filterwarnings("ignore", message=".*resource_tracker.*")
|
|
30
54
|
logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grepmax",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
4
4
|
"author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
|
|
5
5
|
"homepage": "https://github.com/reowens/grepmax",
|
|
6
6
|
"bugs": {
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"benchmark:chart": "npx tsx src/bench/generate-benchmark-chart.ts",
|
|
29
29
|
"bench:recall": "npx tsx src/eval.ts",
|
|
30
30
|
"bench:recall:json": "GMAX_EVAL_JSON=1 npx tsx src/eval.ts",
|
|
31
|
+
"bench:oss": "npx tsx src/eval-oss.ts all",
|
|
32
|
+
"bench:oss:json": "GMAX_EVAL_JSON=1 npx tsx src/eval-oss.ts all",
|
|
31
33
|
"format": "biome check --write .",
|
|
32
34
|
"format:check": "biome check .",
|
|
33
35
|
"lint": "biome lint .",
|
|
@@ -13,6 +13,7 @@ allowed-tools: "Bash(gmax:*), Read"
|
|
|
13
13
|
- **Quick symbol overview?** → `Bash(gmax peek <symbol>)` (signature + callers + callees)
|
|
14
14
|
- **Need file structure?** → `Bash(gmax skeleton <path>)`
|
|
15
15
|
- **Need call flow?** → `Bash(gmax trace <symbol>)`
|
|
16
|
+
- **Is a symbol unused?** → `Bash(gmax dead <symbol>)` (call-graph check — hypothesis, not proof)
|
|
16
17
|
|
|
17
18
|
## Quick start
|
|
18
19
|
|
|
@@ -40,7 +41,7 @@ If search returns "This project hasn't been added to gmax yet", run `Bash(gmax a
|
|
|
40
41
|
|
|
41
42
|
### Search — `gmax "query" --agent`
|
|
42
43
|
|
|
43
|
-
The `--agent` flag produces compact, token-efficient output for AI agents. It works on most commands — `search`, `peek`, `extract`, `trace`, `test`, `impact`, `similar`, `log`, `related`, `symbols`, `status`, `project`, `context`, `skeleton`, and `doctor`.
|
|
44
|
+
The `--agent` flag produces compact, token-efficient output for AI agents. It works on most commands — `search`, `peek`, `extract`, `trace`, `test`, `impact`, `similar`, `dead`, `log`, `related`, `symbols`, `status`, `project`, `context`, `skeleton`, and `doctor`.
|
|
44
45
|
|
|
45
46
|
```
|
|
46
47
|
gmax "where do we handle authentication" --agent
|
|
@@ -177,6 +178,14 @@ gmax similar src/lib/auth.ts # files with similar structure
|
|
|
177
178
|
gmax similar handleAuth -m 5 --agent # top 5, compact output
|
|
178
179
|
```
|
|
179
180
|
|
|
181
|
+
### Dead — `gmax dead <symbol>`
|
|
182
|
+
```
|
|
183
|
+
gmax dead handleAuth # zero-inbound-callers check via the call graph
|
|
184
|
+
gmax dead handleAuth --agent # TSV: status\tdef:line\tcaller_count\tcallers_top3
|
|
185
|
+
gmax dead handleAuth --in src/ # restrict to a sub-path
|
|
186
|
+
```
|
|
187
|
+
Status is `DEAD` (no callers, not exported), `PUBLIC EXPORT` (no internal callers but the defining chunk is exported — check external usage), or `LIVE` (with caller count + top-3 file:line). The call graph reflects what tree-sitter chunked: dynamic dispatch, reflection, eval, and string-built call sites won't show up — `DEAD` is a hypothesis, not a proof.
|
|
188
|
+
|
|
180
189
|
### Context — `gmax context <topic> --budget <tokens>`
|
|
181
190
|
```
|
|
182
191
|
gmax context "authentication system" --budget 4000
|
|
@@ -219,13 +228,14 @@ gmax llm on/off/start/stop/status # manage local LLM server
|
|
|
219
228
|
10. **Test** — `Bash(gmax test <symbol>)` to find tests covering a symbol before editing
|
|
220
229
|
11. **Impact** — `Bash(gmax impact <symbol>)` for blast radius before significant changes
|
|
221
230
|
12. **Similar** — `Bash(gmax similar <symbol>)` to find similar patterns for DRY analysis
|
|
222
|
-
13. **
|
|
223
|
-
14. **
|
|
224
|
-
15. **
|
|
231
|
+
13. **Dead** — `Bash(gmax dead <symbol>)` to check if a symbol has zero inbound callers (hypothesis, not proof)
|
|
232
|
+
14. **Context** — `Bash(gmax context "topic" --budget 4000)` for a token-budgeted topic summary
|
|
233
|
+
15. **Related** — `Bash(gmax related <file>)` to see what else to look at
|
|
234
|
+
16. **Status** — `Bash(gmax status)` to check index state across all projects
|
|
225
235
|
|
|
226
236
|
## Tips
|
|
227
237
|
|
|
228
|
-
- **Use `--agent` for compact output** — works on most commands: search, peek, extract, trace, log, test, impact, similar, related, status, project, doctor.
|
|
238
|
+
- **Use `--agent` for compact output** — works on most commands: search, peek, extract, trace, log, test, impact, similar, dead, related, status, project, doctor.
|
|
229
239
|
- **Be specific.** 5+ words. "auth" returns noise. "where does the server validate JWT tokens" is specific.
|
|
230
240
|
- **Use `--role ORCHESTRATION`** to skip type definitions and find the actual logic.
|
|
231
241
|
- **Use `--symbol`** when the query is a function/class name — gets search + trace in one call.
|