grepmax 0.17.23 → 0.17.24
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/dist/commands/audit.js +10 -2
- package/dist/commands/dead.js +2 -0
- package/dist/commands/doctor.js +59 -10
- package/dist/commands/impact.js +2 -0
- package/dist/commands/peek.js +2 -0
- package/dist/commands/related.js +2 -0
- package/dist/commands/search.js +2 -0
- package/dist/commands/similar.js +2 -0
- package/dist/commands/test-find.js +2 -0
- package/dist/commands/trace.js +2 -0
- package/dist/config.js +46 -5
- package/dist/eval-graph-nav.js +374 -0
- package/dist/eval-graph-sanity.js +138 -19
- package/dist/lib/graph/graph-builder.js +30 -8
- package/dist/lib/graph/impact.js +5 -3
- package/dist/lib/index/chunker.js +149 -4
- package/dist/lib/llm/tools.js +58 -16
- package/dist/lib/store/vector-db.js +43 -13
- package/dist/lib/utils/stale-hint.js +62 -0
- package/dist/lib/workers/orchestrator.js +2 -1
- package/package.json +17 -3
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Navigation-precision fixture — the measurement gate for the type-position
|
|
4
|
+
* reference cluster (chunker type-position edges, Phase 10 graph-distance,
|
|
5
|
+
* `gmax dead` precision). Both standing plans defer that cluster behind one
|
|
6
|
+
* unmet condition: a `trace --inbound` / `dead` truth set that EXISTS and
|
|
7
|
+
* MOVES. This is that instrument.
|
|
8
|
+
*
|
|
9
|
+
* WHAT IT MEASURES. The call graph is built from tree-sitter call-expression
|
|
10
|
+
* captures, so `referenced_symbols` holds call/construct sites (`foo()`,
|
|
11
|
+
* `new Foo()`) but NOT type-position uses (`: Foo`, `<Foo>`, `extends Foo`,
|
|
12
|
+
* `x as Foo`). `gmax trace --inbound Foo` and `gmax dead Foo` both read that
|
|
13
|
+
* graph, so a type used purely in annotations looks like it has no callers —
|
|
14
|
+
* trace under-reports, dead false-positives. This harness quantifies the gap by
|
|
15
|
+
* contrast: call-position recall (the baseline the graph achieves on the shapes
|
|
16
|
+
* it does capture) vs type-position recall (≈0 today).
|
|
17
|
+
*
|
|
18
|
+
* IT IS A GAUGE, NOT A PASS/FAIL GATE. Call-position recall is already <100%
|
|
19
|
+
* (chunk rollup, uncaptured shapes) — that is the status quo, not a regression,
|
|
20
|
+
* so the run always exits 0. The load-bearing number is type-position recall:
|
|
21
|
+
* low today = standing evidence to build chunker type-position edges; re-run
|
|
22
|
+
* after building them to confirm it moved. (For a true regression gate on the
|
|
23
|
+
* call graph, see the caller-count guard in eval-graph-sanity.ts.)
|
|
24
|
+
*
|
|
25
|
+
* HOW IT STAYS HONEST (no rot). Ground truth is derived live: for each symbol,
|
|
26
|
+
* `git grep` every reference and classify each file as a call-position caller or
|
|
27
|
+
* a type-position-only caller; `getCallers` is the measured set. The only
|
|
28
|
+
* hand-annotation is each symbol's character (callable vs type-only). The
|
|
29
|
+
* harness excludes its own file from both grep and the graph so listing the
|
|
30
|
+
* fixture symbols here can't contaminate the measurement. Run after a reindex.
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* npx tsx src/eval-graph-nav.ts # table output
|
|
34
|
+
* npx tsx src/eval-graph-nav.ts --json # machine-readable
|
|
35
|
+
*/
|
|
36
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
37
|
+
if (k2 === undefined) k2 = k;
|
|
38
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
39
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
40
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
41
|
+
}
|
|
42
|
+
Object.defineProperty(o, k2, desc);
|
|
43
|
+
}) : (function(o, m, k, k2) {
|
|
44
|
+
if (k2 === undefined) k2 = k;
|
|
45
|
+
o[k2] = m[k];
|
|
46
|
+
}));
|
|
47
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
48
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
49
|
+
}) : function(o, v) {
|
|
50
|
+
o["default"] = v;
|
|
51
|
+
});
|
|
52
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
53
|
+
var ownKeys = function(o) {
|
|
54
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
55
|
+
var ar = [];
|
|
56
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
57
|
+
return ar;
|
|
58
|
+
};
|
|
59
|
+
return ownKeys(o);
|
|
60
|
+
};
|
|
61
|
+
return function (mod) {
|
|
62
|
+
if (mod && mod.__esModule) return mod;
|
|
63
|
+
var result = {};
|
|
64
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
65
|
+
__setModuleDefault(result, mod);
|
|
66
|
+
return result;
|
|
67
|
+
};
|
|
68
|
+
})();
|
|
69
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
70
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
71
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
72
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
73
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
74
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
75
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
var _a;
|
|
79
|
+
var _b;
|
|
80
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
81
|
+
(_a = (_b = process.env).GMAX_WORKER_COUNT) !== null && _a !== void 0 ? _a : (_b.GMAX_WORKER_COUNT = "1");
|
|
82
|
+
const node_child_process_1 = require("node:child_process");
|
|
83
|
+
const path = __importStar(require("node:path"));
|
|
84
|
+
const config_1 = require("./config");
|
|
85
|
+
const graph_builder_1 = require("./lib/graph/graph-builder");
|
|
86
|
+
const vector_db_1 = require("./lib/store/vector-db");
|
|
87
|
+
const exit_1 = require("./lib/utils/exit");
|
|
88
|
+
const filter_builder_1 = require("./lib/utils/filter-builder");
|
|
89
|
+
const GMAX_ROOT = path.resolve(__dirname, "..");
|
|
90
|
+
const SELF = "src/eval-graph-nav.ts"; // exclude from grep + graph (self-reference)
|
|
91
|
+
// Files that embed the fixture symbols as *string* data (template-literal
|
|
92
|
+
// source, `.toContain("…")` assertions) rather than real type/call references.
|
|
93
|
+
// grep can't tell those apart from genuine uses, so they would manufacture
|
|
94
|
+
// phantom "expected callers" that no correct graph ever surfaces. Excluded from
|
|
95
|
+
// both the grep truth and the getCallers set so the exclusion stays symmetric.
|
|
96
|
+
const FIXTURE_FILES = new Set([SELF, "tests/graph-edges.type-position.test.ts"]);
|
|
97
|
+
const rel = (p) => p.replace(`${GMAX_ROOT}/`, "");
|
|
98
|
+
const INBOUND_SYMBOLS = [
|
|
99
|
+
// ── Baselines: called/constructed, so the graph captures them. ──────────────
|
|
100
|
+
{
|
|
101
|
+
symbol: "getWorkerPool",
|
|
102
|
+
character: "callable",
|
|
103
|
+
note: "plain function call sites",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
symbol: "GraphBuilder",
|
|
107
|
+
character: "callable",
|
|
108
|
+
note: "class via `new GraphBuilder(...)`",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
symbol: "withQueryTimeout",
|
|
112
|
+
character: "callable",
|
|
113
|
+
note: "wrapper function call sites",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
symbol: "isFileCached",
|
|
117
|
+
character: "callable",
|
|
118
|
+
note: "predicate function call sites",
|
|
119
|
+
},
|
|
120
|
+
// ── The gap: interfaces / type aliases used only in `: T` / `<T>` position. ──
|
|
121
|
+
{
|
|
122
|
+
symbol: "SearchResponse",
|
|
123
|
+
character: "type-only",
|
|
124
|
+
note: "return/param annotation across search + eval",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
symbol: "VectorRecord",
|
|
128
|
+
character: "type-only",
|
|
129
|
+
note: "row interface annotated across index/store/worker",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
symbol: "GraphNode",
|
|
133
|
+
character: "type-only",
|
|
134
|
+
note: "graph row interface in dead/formatter consumers",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
symbol: "NeighborHit",
|
|
138
|
+
character: "type-only",
|
|
139
|
+
note: "traversal interface, type position only",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
symbol: "CallerTree",
|
|
143
|
+
character: "type-only",
|
|
144
|
+
note: "recursive caller interface, type position only",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
symbol: "EdgeDirection",
|
|
148
|
+
character: "type-only",
|
|
149
|
+
note: "string-literal type alias, pure type position",
|
|
150
|
+
},
|
|
151
|
+
// ── Python (mlx-embed-server/server.py): proves Shape 6 closes the gap in a
|
|
152
|
+
// grammar with no `type_identifier` node. ─────────────────────────────────
|
|
153
|
+
{
|
|
154
|
+
symbol: "EmbedResponse",
|
|
155
|
+
character: "callable",
|
|
156
|
+
note: "Python: Pydantic model via `EmbedResponse(...)` construct site",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
symbol: "EmbedRequest",
|
|
160
|
+
character: "type-only",
|
|
161
|
+
note: "Python: Pydantic model in `request: EmbedRequest` annotation only",
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
// Dead-precision truth is derived (LIVE iff grep finds any use beyond def/import).
|
|
165
|
+
const DEAD_SYMBOLS = [
|
|
166
|
+
"getWorkerPool", // LIVE baseline (call users) — dead must read LIVE
|
|
167
|
+
"GraphBuilder", // LIVE baseline (construct users)
|
|
168
|
+
"DeadResult", // non-exported, type-only users → today DEAD (false positive)
|
|
169
|
+
"EdgeDirection", // exported, type-only users → today PUBLIC_EXPORT (masked)
|
|
170
|
+
"ResolvedCaller", // exported, type-only users → today PUBLIC_EXPORT (masked)
|
|
171
|
+
"EmbedRequest", // Python: annotation-only Pydantic model — the canonical false-dead, now LIVE via Shape 6
|
|
172
|
+
];
|
|
173
|
+
function classify(symbol, text) {
|
|
174
|
+
const s = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
175
|
+
if (/\bimport\b/.test(text))
|
|
176
|
+
return "import";
|
|
177
|
+
if (new RegExp(`\\b(interface|type|class|function|enum)\\s+${s}\\b`).test(text))
|
|
178
|
+
return "def";
|
|
179
|
+
if (new RegExp(`\\b(const|let|var)\\s+${s}\\b\\s*[:=]`).test(text))
|
|
180
|
+
return "def";
|
|
181
|
+
if (new RegExp(`(\\bnew\\s+${s}\\b|\\b${s}\\s*\\()`).test(text))
|
|
182
|
+
return "call";
|
|
183
|
+
if (new RegExp(`([:<,|&(]\\s*${s}\\b|\\bextends\\s+${s}\\b|\\bimplements\\s+${s}\\b|\\bas\\s+${s}\\b|\\b${s}\\s*[[<])`).test(text))
|
|
184
|
+
return "type";
|
|
185
|
+
return "other";
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Live ground truth for `symbol`: files that reference it from a call position
|
|
189
|
+
* vs a type-only position. Definition-only / import-only files are not callers.
|
|
190
|
+
* A file with any call site counts as a call caller even if it also uses the
|
|
191
|
+
* symbol as a type (the graph should find it via the call). The harness's own
|
|
192
|
+
* file is excluded so the fixture list can't contaminate the truth.
|
|
193
|
+
*/
|
|
194
|
+
function referenceTruth(symbol) {
|
|
195
|
+
let raw;
|
|
196
|
+
try {
|
|
197
|
+
raw = (0, node_child_process_1.execFileSync)("git", [
|
|
198
|
+
"grep",
|
|
199
|
+
"-nwF",
|
|
200
|
+
symbol,
|
|
201
|
+
"--",
|
|
202
|
+
"*.ts",
|
|
203
|
+
"*.py",
|
|
204
|
+
...[...FIXTURE_FILES].map((f) => `:!${f}`),
|
|
205
|
+
], { cwd: GMAX_ROOT, encoding: "utf-8" });
|
|
206
|
+
}
|
|
207
|
+
catch (_a) {
|
|
208
|
+
return { callFiles: new Set(), typeOnlyFiles: new Set() };
|
|
209
|
+
}
|
|
210
|
+
const callFiles = new Set();
|
|
211
|
+
const typeFiles = new Set();
|
|
212
|
+
for (const line of raw.split("\n")) {
|
|
213
|
+
if (!line)
|
|
214
|
+
continue;
|
|
215
|
+
const parts = line.split(":");
|
|
216
|
+
const file = parts[0];
|
|
217
|
+
const text = parts.slice(2).join(":");
|
|
218
|
+
const kind = classify(symbol, text);
|
|
219
|
+
if (kind === "call")
|
|
220
|
+
callFiles.add(file);
|
|
221
|
+
else if (kind === "type")
|
|
222
|
+
typeFiles.add(file);
|
|
223
|
+
}
|
|
224
|
+
const typeOnlyFiles = new Set([...typeFiles].filter((f) => !callFiles.has(f)));
|
|
225
|
+
return { callFiles, typeOnlyFiles };
|
|
226
|
+
}
|
|
227
|
+
/** getCallers file set, excluding the harness's own (possibly stale) chunks. */
|
|
228
|
+
function callerFiles(builder, symbol) {
|
|
229
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
230
|
+
const callers = yield builder.getCallers(symbol);
|
|
231
|
+
return new Set(callers.map((g) => rel(g.file)).filter((f) => !FIXTURE_FILES.has(f)));
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function recall(found, expected) {
|
|
235
|
+
let hit = 0;
|
|
236
|
+
for (const f of expected)
|
|
237
|
+
if (found.has(f))
|
|
238
|
+
hit++;
|
|
239
|
+
return { hit, total: expected.size };
|
|
240
|
+
}
|
|
241
|
+
function runInbound(builder) {
|
|
242
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
243
|
+
const out = [];
|
|
244
|
+
for (const c of INBOUND_SYMBOLS) {
|
|
245
|
+
const { callFiles, typeOnlyFiles } = referenceTruth(c.symbol);
|
|
246
|
+
const found = yield callerFiles(builder, c.symbol);
|
|
247
|
+
out.push({
|
|
248
|
+
symbol: c.symbol,
|
|
249
|
+
character: c.character,
|
|
250
|
+
call: recall(found, callFiles),
|
|
251
|
+
type: recall(found, typeOnlyFiles),
|
|
252
|
+
missedType: [...typeOnlyFiles].filter((f) => !found.has(f)),
|
|
253
|
+
note: c.note,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
function runDead(db, builder) {
|
|
260
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
261
|
+
const table = yield db.ensureTable();
|
|
262
|
+
const out = [];
|
|
263
|
+
for (const symbol of DEAD_SYMBOLS) {
|
|
264
|
+
const { callFiles, typeOnlyFiles } = referenceTruth(symbol);
|
|
265
|
+
const trueLive = callFiles.size + typeOnlyFiles.size > 0;
|
|
266
|
+
const defRows = yield table
|
|
267
|
+
.query()
|
|
268
|
+
.select(["is_exported"])
|
|
269
|
+
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}')`)
|
|
270
|
+
.limit(1)
|
|
271
|
+
.toArray();
|
|
272
|
+
const found = yield callerFiles(builder, symbol);
|
|
273
|
+
let status;
|
|
274
|
+
if (defRows.length === 0)
|
|
275
|
+
status = "NO_DEF";
|
|
276
|
+
else if (found.size > 0)
|
|
277
|
+
status = "LIVE";
|
|
278
|
+
else
|
|
279
|
+
status = defRows[0].is_exported ? "PUBLIC_EXPORT" : "DEAD";
|
|
280
|
+
let verdict;
|
|
281
|
+
if (trueLive && status === "LIVE")
|
|
282
|
+
verdict = "correct";
|
|
283
|
+
else if (!trueLive && status === "DEAD")
|
|
284
|
+
verdict = "correct";
|
|
285
|
+
else if (trueLive && status === "DEAD")
|
|
286
|
+
verdict = "false-dead";
|
|
287
|
+
else if (trueLive && status === "PUBLIC_EXPORT")
|
|
288
|
+
verdict = "masked-by-export";
|
|
289
|
+
else
|
|
290
|
+
verdict = "over-live";
|
|
291
|
+
out.push({ symbol, trueLive, status, verdict });
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function pool(rows) {
|
|
297
|
+
const hit = rows.reduce((a, r) => a + r.hit, 0);
|
|
298
|
+
const total = rows.reduce((a, r) => a + r.total, 0);
|
|
299
|
+
return { hit, total, pct: total === 0 ? 0 : (100 * hit) / total };
|
|
300
|
+
}
|
|
301
|
+
function main() {
|
|
302
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
303
|
+
const jsonMode = process.argv.includes("--json") || process.env.GMAX_EVAL_JSON === "1";
|
|
304
|
+
const db = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir);
|
|
305
|
+
const builder = new graph_builder_1.GraphBuilder(db, GMAX_ROOT);
|
|
306
|
+
const inbound = yield runInbound(builder);
|
|
307
|
+
const dead = yield runDead(db, builder);
|
|
308
|
+
yield db.close();
|
|
309
|
+
const callBaseline = pool(inbound.filter((r) => r.character === "callable").map((r) => r.call));
|
|
310
|
+
const typeGap = pool(inbound.filter((r) => r.character === "type-only").map((r) => r.type));
|
|
311
|
+
const falseDead = dead.filter((r) => r.verdict === "false-dead").length;
|
|
312
|
+
const maskedByExport = dead.filter((r) => r.verdict === "masked-by-export").length;
|
|
313
|
+
const summary = {
|
|
314
|
+
callPositionRecall: callBaseline,
|
|
315
|
+
typePositionRecall: typeGap,
|
|
316
|
+
deadFalsePositives: falseDead,
|
|
317
|
+
deadMaskedByExport: maskedByExport,
|
|
318
|
+
};
|
|
319
|
+
if (jsonMode) {
|
|
320
|
+
process.stdout.write(`${JSON.stringify({ summary, inbound, dead }, null, 2)}\n`);
|
|
321
|
+
yield (0, exit_1.gracefulExit)(0);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const pct = (r) => r.total === 0
|
|
325
|
+
? " n/a"
|
|
326
|
+
: `${Math.round((100 * r.hit) / r.total)}%`.padStart(4);
|
|
327
|
+
console.log(`Navigation-precision fixture (trace --inbound / dead)`);
|
|
328
|
+
console.log(`root: ${GMAX_ROOT}\n`);
|
|
329
|
+
console.log(`Inbound caller recall (getCallers vs grep truth, by reference position):`);
|
|
330
|
+
for (const r of inbound) {
|
|
331
|
+
const tag = r.character === "callable" ? "callable " : "type-only";
|
|
332
|
+
console.log(` ${r.symbol.padEnd(16)} [${tag}] ` +
|
|
333
|
+
`call ${pct(r.call)} (${String(r.call.hit).padStart(2)}/${String(r.call.total).padStart(2)}) ` +
|
|
334
|
+
`type ${pct(r.type)} (${String(r.type.hit).padStart(2)}/${String(r.type.total).padStart(2)})` +
|
|
335
|
+
(r.character === "type-only" && r.missedType.length
|
|
336
|
+
? ` misses: ${r.missedType.slice(0, 3).join(", ")}${r.missedType.length > 3 ? ", …" : ""}`
|
|
337
|
+
: ""));
|
|
338
|
+
}
|
|
339
|
+
console.log(`\nDead-code precision (truth derived from grep):`);
|
|
340
|
+
for (const r of dead) {
|
|
341
|
+
const flag = r.verdict === "false-dead"
|
|
342
|
+
? " ← FALSE POSITIVE (really LIVE)"
|
|
343
|
+
: r.verdict === "masked-by-export"
|
|
344
|
+
? " ← type-only users hidden behind PUBLIC EXPORT"
|
|
345
|
+
: "";
|
|
346
|
+
console.log(` ${r.symbol.padEnd(16)} truth=${(r.trueLive ? "LIVE" : "DEAD").padEnd(4)} reported=${r.status.padEnd(13)}${flag}`);
|
|
347
|
+
}
|
|
348
|
+
console.log(`\nHeadline` +
|
|
349
|
+
`\n call-position recall (baseline): ${callBaseline.hit}/${callBaseline.total} (${Math.round(callBaseline.pct)}%)` +
|
|
350
|
+
`\n type-position recall (the gap): ${typeGap.hit}/${typeGap.total} (${Math.round(typeGap.pct)}%)` +
|
|
351
|
+
`\n dead false-positives: ${falseDead} · dead masked-by-export: ${maskedByExport}`);
|
|
352
|
+
const delta = Math.round(callBaseline.pct - typeGap.pct);
|
|
353
|
+
if (callBaseline.pct < 40) {
|
|
354
|
+
console.log(`\nVerdict: INCONCLUSIVE — call-position baseline is unexpectedly low (${Math.round(callBaseline.pct)}%); ` +
|
|
355
|
+
`the index may be stale. Reindex (gmax index) and re-run.`);
|
|
356
|
+
}
|
|
357
|
+
else if (typeGap.pct < 50) {
|
|
358
|
+
console.log(`\nVerdict: GAP OPEN (${delta} pts) — navigation recovers call sites at ${Math.round(callBaseline.pct)}% ` +
|
|
359
|
+
`but type-position references at only ${Math.round(typeGap.pct)}%. Standing evidence to build chunker ` +
|
|
360
|
+
`type-position edges; re-run after to confirm the gap closes.`);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
console.log(`\nVerdict: GAP CLOSING — type-position recall is up to ${Math.round(typeGap.pct)}% ` +
|
|
364
|
+
`(call baseline ${Math.round(callBaseline.pct)}%).`);
|
|
365
|
+
}
|
|
366
|
+
yield (0, exit_1.gracefulExit)(0);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (require.main === module) {
|
|
370
|
+
main().catch((e) => {
|
|
371
|
+
console.error(e);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Graph sanity checks.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Default mode — caller-count guard (regression net for the chunker-v2 /
|
|
6
|
+
* call-site-dedup fix). Before that fix, `splitIfTooBig` copied a parent
|
|
7
|
+
* chunk's full `referenced_symbols` onto every sub-chunk, so `getCallers`
|
|
8
|
+
* multiplied chunk rows: a symbol with 3 real call sites reported 66 callers
|
|
9
|
+
* (22x). For a set of known gmax symbols this asserts `getCallers`' raw count
|
|
10
|
+
* stays within a small multiple of grep-truth (word-boundary line matches
|
|
11
|
+
* across tracked source). Exits nonzero if any symbol blows past the ceiling.
|
|
12
|
+
* Run after a reindex so the index matches the working tree.
|
|
9
13
|
*
|
|
10
|
-
*
|
|
11
|
-
* (the seed chunk's defined symbol points at the target via the call
|
|
12
|
-
* graph) → worth building the recovery layer.
|
|
14
|
+
* npx tsx src/eval-graph-sanity.ts
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* Platform mode (--platform) — the original Phase 0 probe for Bundle B G1'
|
|
17
|
+
* (graph-as-recall-recovery). For each platform hard-miss case (BeyondError,
|
|
18
|
+
* ErrorCodes, resolveActor, errorHandler), mirror the searcher's dense+FTS+RRF
|
|
19
|
+
* pipeline to the post-fusion top-200 pool, then count how many of those chunks
|
|
20
|
+
* carry the target in `referenced_symbols`. ≥1 hit = a 1-hop outbound graph
|
|
21
|
+
* walk can recover the target → worth building the recovery layer; 0 across all
|
|
22
|
+
* 4 = the signal isn't in the graph at this depth. Requires
|
|
23
|
+
* ~/Development/beyond/platform to be indexed.
|
|
16
24
|
*
|
|
17
|
-
*
|
|
25
|
+
* npx tsx src/eval-graph-sanity.ts --platform
|
|
18
26
|
*/
|
|
19
27
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
28
|
if (k2 === undefined) k2 = k;
|
|
@@ -62,12 +70,15 @@ var _a, _b;
|
|
|
62
70
|
var _c;
|
|
63
71
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
64
72
|
(_a = (_c = process.env).GMAX_WORKER_COUNT) !== null && _a !== void 0 ? _a : (_c.GMAX_WORKER_COUNT = "1");
|
|
73
|
+
const node_child_process_1 = require("node:child_process");
|
|
65
74
|
const path = __importStar(require("node:path"));
|
|
66
75
|
const config_1 = require("./config");
|
|
76
|
+
const callsites_1 = require("./lib/graph/callsites");
|
|
77
|
+
const graph_builder_1 = require("./lib/graph/graph-builder");
|
|
67
78
|
const vector_db_1 = require("./lib/store/vector-db");
|
|
68
79
|
const exit_1 = require("./lib/utils/exit");
|
|
69
|
-
const pool_1 = require("./lib/workers/pool");
|
|
70
80
|
const filter_builder_1 = require("./lib/utils/filter-builder");
|
|
81
|
+
const pool_1 = require("./lib/workers/pool");
|
|
71
82
|
const PLATFORM_ROOT = path.join((_b = process.env.HOME) !== null && _b !== void 0 ? _b : "", "Development/beyond/platform");
|
|
72
83
|
const HARD_MISS_TARGETS = [
|
|
73
84
|
"BeyondError",
|
|
@@ -104,7 +115,9 @@ function probe(target) {
|
|
|
104
115
|
const table = yield db.ensureTable();
|
|
105
116
|
const pool = (0, pool_1.getWorkerPool)();
|
|
106
117
|
const { dense } = yield pool.encodeQuery(target);
|
|
107
|
-
const pathPrefix = PLATFORM_ROOT.endsWith("/")
|
|
118
|
+
const pathPrefix = PLATFORM_ROOT.endsWith("/")
|
|
119
|
+
? PLATFORM_ROOT
|
|
120
|
+
: `${PLATFORM_ROOT}/`;
|
|
108
121
|
const where = `path LIKE '${(0, filter_builder_1.escapeSqlString)(pathPrefix)}%'`;
|
|
109
122
|
const columns = [
|
|
110
123
|
"id",
|
|
@@ -136,14 +149,12 @@ function probe(target) {
|
|
|
136
149
|
const candidateScores = new Map();
|
|
137
150
|
const docMap = new Map();
|
|
138
151
|
vectorRows.forEach((doc, rank) => {
|
|
139
|
-
const key = doc.id ||
|
|
140
|
-
`${doc.path}:${doc.chunk_index}`;
|
|
152
|
+
const key = doc.id || `${doc.path}:${doc.chunk_index}`;
|
|
141
153
|
docMap.set(key, doc);
|
|
142
154
|
candidateScores.set(key, (candidateScores.get(key) || 0) + 1.0 / (RRF_K + rank + 1));
|
|
143
155
|
});
|
|
144
156
|
ftsRows.forEach((doc, rank) => {
|
|
145
|
-
const key = doc.id ||
|
|
146
|
-
`${doc.path}:${doc.chunk_index}`;
|
|
157
|
+
const key = doc.id || `${doc.path}:${doc.chunk_index}`;
|
|
147
158
|
if (!docMap.has(key))
|
|
148
159
|
docMap.set(key, doc);
|
|
149
160
|
candidateScores.set(key, (candidateScores.get(key) || 0) + 1.0 / (RRF_K + rank + 1));
|
|
@@ -189,7 +200,7 @@ function probe(target) {
|
|
|
189
200
|
};
|
|
190
201
|
});
|
|
191
202
|
}
|
|
192
|
-
function
|
|
203
|
+
function platformProbe() {
|
|
193
204
|
return __awaiter(this, void 0, void 0, function* () {
|
|
194
205
|
console.log(`Phase 0 sanity check — graph reachability on platform hard-miss cases`);
|
|
195
206
|
console.log(`pathPrefix: ${PLATFORM_ROOT}`);
|
|
@@ -214,7 +225,115 @@ function main() {
|
|
|
214
225
|
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
226
|
console.log(`\nJSON:`);
|
|
216
227
|
console.log(JSON.stringify(summary, null, 2));
|
|
217
|
-
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Caller-count guard (default mode)
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
const GMAX_ROOT = path.resolve(__dirname, "..");
|
|
234
|
+
/**
|
|
235
|
+
* Distinctive gmax symbols with a spread of real usage. All sit comfortably
|
|
236
|
+
* below getCallers' internal `.limit(100)`, so the cap can never silently mask
|
|
237
|
+
* an explosion — if the chunker regresses, the raw count blows past the
|
|
238
|
+
* ceiling rather than getting clamped to 100. Names are intentionally unusual
|
|
239
|
+
* (no bare words) so `git grep -w` truth isn't polluted by unrelated matches.
|
|
240
|
+
*/
|
|
241
|
+
const GUARD_SYMBOLS = [
|
|
242
|
+
"resolveCallSites",
|
|
243
|
+
"findCallSiteSnippet",
|
|
244
|
+
"withQueryTimeout",
|
|
245
|
+
"isFileCached",
|
|
246
|
+
"getWorkerPool",
|
|
247
|
+
"isBuiltinCallee",
|
|
248
|
+
"findDependents",
|
|
249
|
+
"buildFileSubgraph",
|
|
250
|
+
];
|
|
251
|
+
/**
|
|
252
|
+
* getCallers returns one chunk-row per referencing chunk. Healthy, that count
|
|
253
|
+
* is ≤ the number of source lines mentioning the symbol (several mentions in
|
|
254
|
+
* one chunk collapse to one row; def/import lines aren't call references). So
|
|
255
|
+
* the healthy ratio is ≤ ~1. A small multiple absorbs index-vs-worktree skew
|
|
256
|
+
* and chunk-boundary splits while still catching the 22x chunker-v2 blowup.
|
|
257
|
+
*/
|
|
258
|
+
const CEILING_MULTIPLE = 3;
|
|
259
|
+
/** Mirror of getCallers' internal `.limit(100)` — at the cap the guard is blind. */
|
|
260
|
+
const CALLER_LIMIT = 100;
|
|
261
|
+
/**
|
|
262
|
+
* grep-truth: word-boundary line + file counts for `symbol` across tracked
|
|
263
|
+
* `.ts` (repo-wide, so experiments/ and scripts/ count too — they're indexed).
|
|
264
|
+
* `git grep` skips gitignored paths (dist/), matching the indexed corpus.
|
|
265
|
+
*/
|
|
266
|
+
function grepTruth(symbol) {
|
|
267
|
+
let out;
|
|
268
|
+
try {
|
|
269
|
+
out = (0, node_child_process_1.execFileSync)("git", ["grep", "-Fwc", symbol, "--", "*.ts"], {
|
|
270
|
+
cwd: GMAX_ROOT,
|
|
271
|
+
encoding: "utf-8",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
catch (_a) {
|
|
275
|
+
// git grep exits 1 when there are zero matches.
|
|
276
|
+
return { lines: 0, files: 0 };
|
|
277
|
+
}
|
|
278
|
+
let lines = 0;
|
|
279
|
+
let files = 0;
|
|
280
|
+
for (const row of out.split("\n")) {
|
|
281
|
+
const idx = row.lastIndexOf(":");
|
|
282
|
+
if (idx < 0)
|
|
283
|
+
continue;
|
|
284
|
+
const n = Number(row.slice(idx + 1));
|
|
285
|
+
if (Number.isFinite(n) && n > 0) {
|
|
286
|
+
lines += n;
|
|
287
|
+
files++;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { lines, files };
|
|
291
|
+
}
|
|
292
|
+
function callerCountGuard() {
|
|
293
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
294
|
+
const db = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir);
|
|
295
|
+
yield db.ensureTable();
|
|
296
|
+
const builder = new graph_builder_1.GraphBuilder(db, GMAX_ROOT);
|
|
297
|
+
const fileCache = new Map();
|
|
298
|
+
console.log(`Caller-count guard — getCallers vs grep-truth call sites`);
|
|
299
|
+
console.log(`root: ${GMAX_ROOT}`);
|
|
300
|
+
console.log(`ceiling = ${CEILING_MULTIPLE}x grep lines · getCallers cap = ${CALLER_LIMIT}`);
|
|
301
|
+
console.log(`(run after a reindex so the index matches the working tree)\n`);
|
|
302
|
+
let allOK = true;
|
|
303
|
+
for (const sym of GUARD_SYMBOLS) {
|
|
304
|
+
const { lines: grepLines, files: grepFiles } = grepTruth(sym);
|
|
305
|
+
const callers = yield builder.getCallers(sym);
|
|
306
|
+
const raw = callers.length;
|
|
307
|
+
const deduped = (0, callsites_1.resolveCallSites)(callers.map((c) => ({ symbol: c.symbol, file: c.file, line: c.line })), sym, fileCache).length;
|
|
308
|
+
const ceiling = CEILING_MULTIPLE * Math.max(1, grepLines);
|
|
309
|
+
const capped = raw >= CALLER_LIMIT;
|
|
310
|
+
const ratio = grepLines > 0 ? raw / grepLines : raw;
|
|
311
|
+
// capped is a failure too: the fixture grew too common, so the cap now
|
|
312
|
+
// hides explosions — swap it for a lower-usage symbol.
|
|
313
|
+
const ok = !capped && raw <= ceiling;
|
|
314
|
+
if (!ok)
|
|
315
|
+
allOK = false;
|
|
316
|
+
console.log(`${ok ? "✓" : "✗"} ${sym.padEnd(20)} ` +
|
|
317
|
+
`callers=${String(raw).padStart(3)} (dedup ${String(deduped).padStart(3)}) ` +
|
|
318
|
+
`grep=${String(grepLines).padStart(3)}L/${String(grepFiles).padStart(2)}f ` +
|
|
319
|
+
`ratio=${ratio.toFixed(2)} ceiling=${ceiling}` +
|
|
320
|
+
(capped ? " [CAPPED — fixture too common]" : ""));
|
|
321
|
+
}
|
|
322
|
+
console.log(`\nVerdict: ${allOK ? "PASS (no caller-count explosion)" : "FAIL (getCallers exceeds grep-truth ceiling — chunker symbol dedup regressed)"}`);
|
|
323
|
+
yield db.close();
|
|
324
|
+
return allOK;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
function main() {
|
|
328
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
329
|
+
const platform = process.argv.includes("--platform");
|
|
330
|
+
if (platform) {
|
|
331
|
+
yield platformProbe();
|
|
332
|
+
yield (0, exit_1.gracefulExit)(0);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const ok = yield callerCountGuard();
|
|
336
|
+
yield (0, exit_1.gracefulExit)(ok ? 0 : 1);
|
|
218
337
|
});
|
|
219
338
|
}
|
|
220
339
|
if (require.main === module) {
|
|
@@ -17,7 +17,11 @@ class GraphBuilder {
|
|
|
17
17
|
constructor(db, pathPrefix, excludePrefixes) {
|
|
18
18
|
this.db = db;
|
|
19
19
|
// Normalize to ensure trailing slash for LIKE queries
|
|
20
|
-
this.pathPrefix = pathPrefix
|
|
20
|
+
this.pathPrefix = pathPrefix
|
|
21
|
+
? pathPrefix.endsWith("/")
|
|
22
|
+
? pathPrefix
|
|
23
|
+
: `${pathPrefix}/`
|
|
24
|
+
: undefined;
|
|
21
25
|
this.excludePrefixes = (excludePrefixes !== null && excludePrefixes !== void 0 ? excludePrefixes : []).map((p) => p.endsWith("/") ? p : `${p}/`);
|
|
22
26
|
}
|
|
23
27
|
scopeWhere(condition) {
|
|
@@ -37,14 +41,22 @@ class GraphBuilder {
|
|
|
37
41
|
return __awaiter(this, void 0, void 0, function* () {
|
|
38
42
|
const table = yield this.db.ensureTable();
|
|
39
43
|
const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
|
|
40
|
-
// Find chunks
|
|
44
|
+
// Find chunks that reference the symbol from a call position
|
|
45
|
+
// (referenced_symbols) OR a type position (type_referenced_symbols). The
|
|
46
|
+
// two are stored separately so type edges never inflate the call-edge count
|
|
47
|
+
// used for ranking; navigation unions them.
|
|
41
48
|
const rows = yield table
|
|
42
49
|
.query()
|
|
43
50
|
.select([
|
|
44
|
-
"path",
|
|
45
|
-
"
|
|
51
|
+
"path",
|
|
52
|
+
"start_line",
|
|
53
|
+
"defined_symbols",
|
|
54
|
+
"referenced_symbols",
|
|
55
|
+
"role",
|
|
56
|
+
"parent_symbol",
|
|
57
|
+
"complexity",
|
|
46
58
|
])
|
|
47
|
-
.where(this.scopeWhere(`array_contains(referenced_symbols, '${escaped}')`))
|
|
59
|
+
.where(this.scopeWhere(`(array_contains(referenced_symbols, '${escaped}') OR array_contains(type_referenced_symbols, '${escaped}'))`))
|
|
48
60
|
.limit(100)
|
|
49
61
|
.toArray();
|
|
50
62
|
return rows.map((row) => this.mapRowToNode(row, symbol, "caller"));
|
|
@@ -82,8 +94,13 @@ class GraphBuilder {
|
|
|
82
94
|
const centerRows = yield table
|
|
83
95
|
.query()
|
|
84
96
|
.select([
|
|
85
|
-
"path",
|
|
86
|
-
"
|
|
97
|
+
"path",
|
|
98
|
+
"start_line",
|
|
99
|
+
"defined_symbols",
|
|
100
|
+
"referenced_symbols",
|
|
101
|
+
"role",
|
|
102
|
+
"parent_symbol",
|
|
103
|
+
"complexity",
|
|
87
104
|
])
|
|
88
105
|
.where(this.scopeWhere(`array_contains(defined_symbols, '${escaped}')`))
|
|
89
106
|
.limit(1)
|
|
@@ -165,7 +182,12 @@ class GraphBuilder {
|
|
|
165
182
|
}
|
|
166
183
|
const visited = new Set([symbol]);
|
|
167
184
|
const callerTree = yield this.expandCallers(graph.callers, depth - 1, visited);
|
|
168
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
center: graph.center,
|
|
187
|
+
callerTree,
|
|
188
|
+
callees: graph.callees,
|
|
189
|
+
importers,
|
|
190
|
+
};
|
|
169
191
|
});
|
|
170
192
|
}
|
|
171
193
|
expandCallers(callers, remainingDepth, visited) {
|
package/dist/lib/graph/impact.js
CHANGED
|
@@ -22,8 +22,10 @@ const TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/i;
|
|
|
22
22
|
const NATIVE_TEST_DIR_RE = /(^|\/)\w+Tests?(\/|$)/;
|
|
23
23
|
const NATIVE_TEST_FILE_RE = /Tests?\.(swift|kt|java)$/;
|
|
24
24
|
function isTestPath(filePath) {
|
|
25
|
-
return TEST_DIR_RE.test(filePath) ||
|
|
26
|
-
|
|
25
|
+
return (TEST_DIR_RE.test(filePath) ||
|
|
26
|
+
TEST_FILE_RE.test(filePath) ||
|
|
27
|
+
NATIVE_TEST_DIR_RE.test(filePath) ||
|
|
28
|
+
NATIVE_TEST_FILE_RE.test(filePath));
|
|
27
29
|
}
|
|
28
30
|
const arrow_1 = require("../utils/arrow");
|
|
29
31
|
/**
|
|
@@ -214,7 +216,7 @@ function findDependents(symbols_1, vectorDb_1, projectRoot_1, excludePaths_1) {
|
|
|
214
216
|
const rows = yield table
|
|
215
217
|
.query()
|
|
216
218
|
.select(["path"])
|
|
217
|
-
.where(`array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') AND ${pathScope}`)
|
|
219
|
+
.where(`(array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') OR array_contains(type_referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}')) AND ${pathScope}`)
|
|
218
220
|
.limit(200)
|
|
219
221
|
.toArray();
|
|
220
222
|
for (const row of rows) {
|