grepmax 0.26.27 → 0.26.28

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.
@@ -0,0 +1,853 @@
1
+ "use strict";
2
+ /**
3
+ * Daemon-side handlers for the `graph.*` read verbs.
4
+ *
5
+ * The commands that used to open LanceDB themselves (`test`, `impact`, `trace`,
6
+ * `peek`, `dead`, `audit`) now send one IPC line and render the answer. The work
7
+ * they used to do between `ensureTable()` and the first `console.log` lives
8
+ * here, so both paths run the *same* code:
9
+ *
10
+ * - the daemon runs `runGraph*` against its warm `VectorDB`
11
+ * - a CLI with no daemon runs the identical `runGraph*` against a local one
12
+ *
13
+ * That is what makes the two paths byte-identical: the commands hold only
14
+ * argument parsing and rendering, and every clamp lives inside the `runGraph*`
15
+ * function rather than in one of the two callers.
16
+ *
17
+ * Results are finished answers, never raw rows. `graph.audit` reads up to 500k
18
+ * chunks and returns a few dozen ranked lines; `graph.dead` reads the whole
19
+ * caller set and returns a count plus three locations. Nothing here streams.
20
+ *
21
+ * Scope on the wire: the CLI resolves `--in`/`--exclude` locally (that needs its
22
+ * cwd and the filesystem) and sends absolute prefixes. The daemon re-validates
23
+ * every one with `resolveContainedPath` — exactly as the `search` case does —
24
+ * and rebuilds the same `ResolvedScope` with `scopeFromPrefixes`, including the
25
+ * single-`--in`-collapses-into-pathPrefix rule, so the WHERE clauses match.
26
+ */
27
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ var desc = Object.getOwnPropertyDescriptor(m, k);
30
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
31
+ desc = { enumerable: true, get: function() { return m[k]; } };
32
+ }
33
+ Object.defineProperty(o, k2, desc);
34
+ }) : (function(o, m, k, k2) {
35
+ if (k2 === undefined) k2 = k;
36
+ o[k2] = m[k];
37
+ }));
38
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
39
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
40
+ }) : function(o, v) {
41
+ o["default"] = v;
42
+ });
43
+ var __importStar = (this && this.__importStar) || (function () {
44
+ var ownKeys = function(o) {
45
+ ownKeys = Object.getOwnPropertyNames || function (o) {
46
+ var ar = [];
47
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
48
+ return ar;
49
+ };
50
+ return ownKeys(o);
51
+ };
52
+ return function (mod) {
53
+ if (mod && mod.__esModule) return mod;
54
+ var result = {};
55
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
56
+ __setModuleDefault(result, mod);
57
+ return result;
58
+ };
59
+ })();
60
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
61
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
62
+ return new (P || (P = Promise))(function (resolve, reject) {
63
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
64
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
65
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
66
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
67
+ });
68
+ };
69
+ Object.defineProperty(exports, "__esModule", { value: true });
70
+ exports.GRAPH_VERB_TIMEOUT_MS = void 0;
71
+ exports.scopeFromPrefixes = scopeFromPrefixes;
72
+ exports.inPrefixesOf = inPrefixesOf;
73
+ exports.encodeScope = encodeScope;
74
+ exports.encodeSymbolFamilies = encodeSymbolFamilies;
75
+ exports.decodeSymbolFamilies = decodeSymbolFamilies;
76
+ exports.isDaemonReadableProject = isDaemonReadableProject;
77
+ exports.runGraphResolve = runGraphResolve;
78
+ exports.runGraphTests = runGraphTests;
79
+ exports.runGraphDependents = runGraphDependents;
80
+ exports.runGraphTrace = runGraphTrace;
81
+ exports.runGraphPeek = runGraphPeek;
82
+ exports.runGraphDead = runGraphDead;
83
+ exports.runGraphNeighbors = runGraphNeighbors;
84
+ exports.runGraphPaths = runGraphPaths;
85
+ exports.runGraphSubgraph = runGraphSubgraph;
86
+ exports.runGraphRisk = runGraphRisk;
87
+ exports.computeAudit = computeAudit;
88
+ exports.runGraphAudit = runGraphAudit;
89
+ exports.createGraphVerbs = createGraphVerbs;
90
+ exports.callGraphVerb = callGraphVerb;
91
+ const path = __importStar(require("node:path"));
92
+ const languages_1 = require("../core/languages");
93
+ const callsites_1 = require("../graph/callsites");
94
+ const graph_builder_1 = require("../graph/graph-builder");
95
+ const impact_1 = require("../graph/impact");
96
+ const arrow_1 = require("../utils/arrow");
97
+ const daemon_client_1 = require("../utils/daemon-client");
98
+ const filter_builder_1 = require("../utils/filter-builder");
99
+ const path_containment_1 = require("../utils/path-containment");
100
+ const project_registry_1 = require("../utils/project-registry");
101
+ const scope_filter_1 = require("../utils/scope-filter");
102
+ const store_access_1 = require("../utils/store-access");
103
+ const tests_footer_1 = require("../utils/tests-footer");
104
+ const MAX_DEPTH = 3;
105
+ const AUDIT_ROW_LIMIT = 500000;
106
+ const MAX_AUDIT_TOP = 1000;
107
+ const DEAD_TOP_CALLERS = 3;
108
+ /** `get_neighbors` caps hops at 5; the daemon re-clamps rather than trusting it. */
109
+ const MAX_NEIGHBOR_HOPS = 5;
110
+ /** `find_paths` caps hops at 10. */
111
+ const MAX_PATH_HOPS = 10;
112
+ /** A subgraph request builds one OR clause per file; bound it. */
113
+ const MAX_SUBGRAPH_FILES = 500;
114
+ /** A diff can name many symbols; each costs three graph queries. */
115
+ const MAX_RISK_SYMBOLS = 200;
116
+ function clampInt(value, min, max, fallback) {
117
+ const n = typeof value === "number" ? value : Number.NaN;
118
+ if (!Number.isFinite(n))
119
+ return fallback;
120
+ return Math.min(Math.max(Math.trunc(n), min), max);
121
+ }
122
+ function withTrailingSlash(p) {
123
+ return p.endsWith("/") ? p : `${p}/`;
124
+ }
125
+ /**
126
+ * Rebuild the CLI's `ResolvedScope` from the prefixes on the wire.
127
+ *
128
+ * Mirrors `resolveScope`: a single `--in` collapses into `pathPrefix` (which
129
+ * keeps the WHERE clause simple), two or more stay in `inPrefixes` with
130
+ * `pathPrefix` back at the project root.
131
+ */
132
+ function scopeFromPrefixes(projectRoot, inPrefixes, excludePrefixes) {
133
+ if (inPrefixes.length === 1) {
134
+ return { pathPrefix: inPrefixes[0], inPrefixes: [], excludePrefixes };
135
+ }
136
+ return {
137
+ pathPrefix: withTrailingSlash(projectRoot),
138
+ inPrefixes,
139
+ excludePrefixes,
140
+ };
141
+ }
142
+ /**
143
+ * The inverse, for the client: flatten a `ResolvedScope` back into the
144
+ * uncollapsed prefix list the wire carries.
145
+ */
146
+ function inPrefixesOf(projectRoot, scope) {
147
+ if (scope.inPrefixes.length > 0)
148
+ return scope.inPrefixes;
149
+ return scope.pathPrefix === withTrailingSlash(projectRoot)
150
+ ? []
151
+ : [scope.pathPrefix];
152
+ }
153
+ /** Build the scope half of any graph verb payload. */
154
+ function encodeScope(projectRoot, scope) {
155
+ return {
156
+ projectRoot,
157
+ inPrefixes: inPrefixesOf(projectRoot, scope),
158
+ excludePrefixes: scope.excludePrefixes,
159
+ };
160
+ }
161
+ function encodeSymbolFamilies(families) {
162
+ return families ? [...families.entries()] : null;
163
+ }
164
+ function decodeSymbolFamilies(wire) {
165
+ if (!Array.isArray(wire))
166
+ return undefined;
167
+ const map = new Map();
168
+ for (const entry of wire) {
169
+ if (!Array.isArray(entry) || typeof entry[0] !== "string")
170
+ continue;
171
+ map.set(entry[0], typeof entry[1] === "string" ? entry[1] : null);
172
+ }
173
+ return map;
174
+ }
175
+ /**
176
+ * True when a project is eligible for the daemon read path at all.
177
+ *
178
+ * The client consults this before sending: an unregistered directory has no
179
+ * chunks in the shared table, and the daemon rejects it (fail closed). Falling
180
+ * straight to the in-process path there keeps the old answer — "Symbol not
181
+ * found" — instead of turning an empty result into a daemon error.
182
+ */
183
+ function isDaemonReadableProject(root) {
184
+ const project = (0, project_registry_1.getProject)(root);
185
+ return project !== undefined && project.status !== "error";
186
+ }
187
+ function requireString(payload, key) {
188
+ const value = payload[key];
189
+ if (typeof value !== "string" || value === "") {
190
+ throw new Error(`missing ${key}`);
191
+ }
192
+ return value;
193
+ }
194
+ function requirePrefixes(projectRoot, raw, key) {
195
+ if (raw === undefined || raw === null)
196
+ return [];
197
+ if (!Array.isArray(raw))
198
+ throw new Error(`invalid ${key}`);
199
+ return raw.map((prefix) => {
200
+ if (typeof prefix !== "string")
201
+ throw new Error(`invalid ${key}`);
202
+ // Same containment rule the `search` case applies to its scope prefixes;
203
+ // throwing here surfaces as an ordinary error response.
204
+ return withTrailingSlash((0, path_containment_1.resolveContainedPath)(projectRoot, prefix));
205
+ });
206
+ }
207
+ /**
208
+ * Validate the scope half of a payload: the project must be registered and not
209
+ * in `error` status, and every prefix must resolve inside it.
210
+ */
211
+ function decodeScopePayload(payload) {
212
+ const projectRoot = path.resolve(requireString(payload, "projectRoot"));
213
+ if (!isDaemonReadableProject(projectRoot)) {
214
+ throw new Error("project not registered");
215
+ }
216
+ const inPrefixes = requirePrefixes(projectRoot, payload.inPrefixes, "inPrefixes");
217
+ const excludePrefixes = requirePrefixes(projectRoot, payload.excludePrefixes, "excludePrefixes");
218
+ return {
219
+ projectRoot,
220
+ scope: scopeFromPrefixes(projectRoot, inPrefixes, excludePrefixes),
221
+ scoped: inPrefixes.length > 0,
222
+ };
223
+ }
224
+ function requireStore(deps) {
225
+ if (!deps.vectorDb)
226
+ throw new Error("daemon not ready");
227
+ deps.touchActivity();
228
+ return deps.vectorDb;
229
+ }
230
+ function requireSymbols(payload) {
231
+ const raw = payload.symbols;
232
+ if (!Array.isArray(raw) || raw.length === 0)
233
+ throw new Error("missing symbols");
234
+ return raw.map((s) => {
235
+ if (typeof s !== "string")
236
+ throw new Error("invalid symbols");
237
+ return s;
238
+ });
239
+ }
240
+ /**
241
+ * The traversal direction. Anything other than the two literals is a bug in the
242
+ * client, so it is rejected rather than silently defaulted — `callers` and
243
+ * `callees` answer opposite questions.
244
+ */
245
+ function requireDirection(payload) {
246
+ const raw = payload.direction;
247
+ if (raw === undefined || raw === null)
248
+ return "callees";
249
+ if (raw !== "callers" && raw !== "callees") {
250
+ throw new Error("invalid direction");
251
+ }
252
+ return raw;
253
+ }
254
+ /**
255
+ * A file list for `graph.subgraph`, every entry re-resolved inside the project.
256
+ * The paths land in an `OR` of `path = '…'` equalities, so containment is what
257
+ * keeps a caller from naming rows belonging to another project in the shared
258
+ * table.
259
+ */
260
+ function requireContainedFiles(projectRoot, raw) {
261
+ if (!Array.isArray(raw) || raw.length === 0) {
262
+ throw new Error("missing files");
263
+ }
264
+ return raw.map((file) => {
265
+ if (typeof file !== "string" || file === "") {
266
+ throw new Error("invalid files");
267
+ }
268
+ return (0, path_containment_1.resolveContainedPath)(projectRoot, file);
269
+ });
270
+ }
271
+ /**
272
+ * The root `findTests`/`findDependents` query against. With any `--in` the CLI
273
+ * narrows to the scope prefix; otherwise it is the project root.
274
+ */
275
+ function queryRootFor(decoded) {
276
+ return decoded.scoped
277
+ ? decoded.scope.pathPrefix.replace(/\/$/, "")
278
+ : decoded.projectRoot;
279
+ }
280
+ // --- Shared implementations (daemon and in-process run these) ---------------
281
+ function runGraphResolve(db, args) {
282
+ return __awaiter(this, void 0, void 0, function* () {
283
+ const resolved = yield (0, impact_1.resolveTargetSymbols)(args.target, db, args.projectRoot);
284
+ return {
285
+ symbols: resolved.symbols,
286
+ resolvedAsFile: resolved.resolvedAsFile,
287
+ symbolFamilies: encodeSymbolFamilies(resolved.symbolFamilies),
288
+ };
289
+ });
290
+ }
291
+ function runGraphTests(db, args) {
292
+ return __awaiter(this, void 0, void 0, function* () {
293
+ return (0, impact_1.findTests)(args.symbols, db, args.queryRoot, clampInt(args.depth, 1, MAX_DEPTH, 1), args.excludePrefixes, args.families);
294
+ });
295
+ }
296
+ function runGraphDependents(db, args) {
297
+ return __awaiter(this, void 0, void 0, function* () {
298
+ const excludePaths = args.excludePaths && args.excludePaths.length > 0
299
+ ? new Set(args.excludePaths)
300
+ : undefined;
301
+ // `undefined` keeps findDependents' own default of 10 — the shape the flat
302
+ // (non-rollup) call has always used.
303
+ const limit = typeof args.limit === "number"
304
+ ? clampInt(args.limit, 1, 10000, 10)
305
+ : undefined;
306
+ return args.detailed
307
+ ? (0, impact_1.findDependentsDetailed)(args.symbols, db, args.queryRoot, excludePaths, limit, args.excludePrefixes, args.families)
308
+ : (0, impact_1.findDependents)(args.symbols, db, args.queryRoot, excludePaths, limit, args.excludePrefixes, args.families);
309
+ });
310
+ }
311
+ function runGraphTrace(db, args) {
312
+ return __awaiter(this, void 0, void 0, function* () {
313
+ const builder = new graph_builder_1.GraphBuilder(db, args.scope.pathPrefix, args.scope.excludePrefixes);
314
+ return builder.buildGraphMultiHop(args.symbol, clampInt(args.hops, 1, MAX_DEPTH, 1));
315
+ });
316
+ }
317
+ function runGraphPeek(db, args) {
318
+ return __awaiter(this, void 0, void 0, function* () {
319
+ const depth = clampInt(args.depth, 1, MAX_DEPTH, 1);
320
+ const scopeWhere = (cond) => (0, scope_filter_1.buildScopeWhere)(args.scope, cond);
321
+ const definedWhere = scopeWhere(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(args.symbol)}')`);
322
+ const table = yield db.ensureTable();
323
+ const defRows = yield table
324
+ .query()
325
+ .select(["path", "start_line"])
326
+ .where(definedWhere)
327
+ .limit(20)
328
+ .toArray();
329
+ const defChunks = defRows.map((row) => ({
330
+ path: String(row.path || ""),
331
+ startLine: Number(row.start_line || 0),
332
+ }));
333
+ const builder = new graph_builder_1.GraphBuilder(db, args.scope.pathPrefix, args.scope.excludePrefixes);
334
+ const graph = yield builder.buildGraph(args.symbol);
335
+ const metaRows = yield table
336
+ .query()
337
+ .select(["is_exported", "start_line", "end_line"])
338
+ .where(definedWhere)
339
+ .limit(1)
340
+ .toArray();
341
+ const metaRow = metaRows[0];
342
+ const meta = metaRow
343
+ ? {
344
+ isExported: Boolean(metaRow.is_exported),
345
+ startLine: Number(metaRow.start_line || 0),
346
+ endLine: Number(metaRow.end_line || 0),
347
+ }
348
+ : null;
349
+ const callerTree = depth > 1
350
+ ? (yield builder.buildGraphMultiHop(args.symbol, depth)).callerTree
351
+ : null;
352
+ const footerTests = args.includeTests
353
+ ? yield (0, tests_footer_1.fetchTestsForFooter)(args.symbol, db, args.scope.pathPrefix, args.scope.excludePrefixes)
354
+ : null;
355
+ return { defChunks, graph, meta, callerTree, footerTests };
356
+ });
357
+ }
358
+ function runGraphDead(db, args) {
359
+ return __awaiter(this, void 0, void 0, function* () {
360
+ const table = yield db.ensureTable();
361
+ const defRows = yield table
362
+ .query()
363
+ .select(["path", "start_line", "is_exported"])
364
+ .where((0, scope_filter_1.buildScopeWhere)(args.scope, `array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(args.symbol)}')`))
365
+ .limit(1)
366
+ .toArray();
367
+ if (defRows.length === 0) {
368
+ return {
369
+ found: false,
370
+ defPath: "",
371
+ defLine: 0,
372
+ isExported: false,
373
+ callerCount: 0,
374
+ topCallers: [],
375
+ };
376
+ }
377
+ const defRow = defRows[0];
378
+ const defPath = String(defRow.path || "");
379
+ const builder = new graph_builder_1.GraphBuilder(db, args.scope.pathPrefix, args.scope.excludePrefixes);
380
+ const callers = yield builder.getCallers(args.symbol, (0, languages_1.languageFamilyForPath)(defPath));
381
+ return {
382
+ found: true,
383
+ defPath,
384
+ defLine: Number(defRow.start_line || 0),
385
+ isExported: Boolean(defRow.is_exported),
386
+ callerCount: callers.length,
387
+ topCallers: callers
388
+ .slice(0, DEAD_TOP_CALLERS)
389
+ .map((c) => ({ file: c.file, line: c.line })),
390
+ };
391
+ });
392
+ }
393
+ // --- graph primitives (MCP's get_neighbors / find_paths / subgraph_for_files)
394
+ /**
395
+ * `GraphBuilder.getNeighbors` behind a verb.
396
+ *
397
+ * `graph.trace` cannot serve this: it walks a *caller tree* at a fixed shape,
398
+ * while these three are the raw traversal primitives MCP exposes — bounded BFS
399
+ * in either direction, shortest path between two symbols, and the local
400
+ * subgraph of a file set. Each is a distinct GraphBuilder entry point with its
401
+ * own bound, so each gets its own verb rather than an option on trace.
402
+ */
403
+ function runGraphNeighbors(db, args) {
404
+ return __awaiter(this, void 0, void 0, function* () {
405
+ const builder = new graph_builder_1.GraphBuilder(db, args.scope.pathPrefix, args.scope.excludePrefixes);
406
+ return builder.getNeighbors(args.symbol, args.direction, clampInt(args.maxHops, 1, MAX_NEIGHBOR_HOPS, 2));
407
+ });
408
+ }
409
+ /** `GraphBuilder.findPaths` behind a verb; null when unreachable. */
410
+ function runGraphPaths(db, args) {
411
+ return __awaiter(this, void 0, void 0, function* () {
412
+ const builder = new graph_builder_1.GraphBuilder(db, args.scope.pathPrefix, args.scope.excludePrefixes);
413
+ return builder.findPaths(args.from, args.to, args.direction, clampInt(args.maxHops, 1, MAX_PATH_HOPS, 6));
414
+ });
415
+ }
416
+ /** `GraphBuilder.subgraphForFiles` behind a verb. Paths arrive absolute. */
417
+ function runGraphSubgraph(db, args) {
418
+ return __awaiter(this, void 0, void 0, function* () {
419
+ const builder = new graph_builder_1.GraphBuilder(db, args.scope.pathPrefix, args.scope.excludePrefixes);
420
+ return builder.subgraphForFiles(args.files.slice(0, MAX_SUBGRAPH_FILES));
421
+ });
422
+ }
423
+ /**
424
+ * The store-backed half of the risk ranking: for each changed symbol, its
425
+ * inbound caller count, its defining location, and whether any test reaches it.
426
+ *
427
+ * Split this way because `gatherRiskInputs` mixes two kinds of work — git
428
+ * (`extractDiff`, `fileChurn`) and the graph (`callersOf`, `resolveLocation`,
429
+ * `findTests`) — and only the second half belongs on the daemon's warm store.
430
+ * The caller keeps the git half and assembles the `RiskInput` rows, so the
431
+ * scoring stays exactly where it was.
432
+ */
433
+ function runGraphRisk(db, args) {
434
+ return __awaiter(this, void 0, void 0, function* () {
435
+ const builder = new graph_builder_1.GraphBuilder(db, args.scope.pathPrefix, args.scope.excludePrefixes);
436
+ return Promise.all(args.symbols.slice(0, MAX_RISK_SYMBOLS).map((symbol) => __awaiter(this, void 0, void 0, function* () {
437
+ var _a, _b;
438
+ const [callers, loc, tests] = yield Promise.all([
439
+ builder.callersOf(symbol).catch(() => []),
440
+ builder.resolveLocation(symbol).catch(() => null),
441
+ (0, impact_1.findTests)([symbol], db, args.queryRoot).catch(() => []),
442
+ ]);
443
+ return {
444
+ symbol,
445
+ file: (_a = loc === null || loc === void 0 ? void 0 : loc.file) !== null && _a !== void 0 ? _a : "",
446
+ line: (_b = loc === null || loc === void 0 ? void 0 : loc.line) !== null && _b !== void 0 ? _b : 0,
447
+ callerCount: callers.length,
448
+ hasTests: tests.length > 0,
449
+ };
450
+ })));
451
+ });
452
+ }
453
+ // Names too generic to be useful as god-node signal (`id`, `el`, `fn`, …).
454
+ const MIN_GOD_NAME_LEN = 3;
455
+ function rel(p, prefix) {
456
+ return p.startsWith(prefix) ? p.slice(prefix.length) : p;
457
+ }
458
+ function findFileCycles(deps, prefix, top) {
459
+ const indexByFile = new Map();
460
+ const lowlink = new Map();
461
+ const stack = [];
462
+ const onStack = new Set();
463
+ const components = [];
464
+ let index = 0;
465
+ const strongConnect = (file) => {
466
+ var _a, _b, _c, _d, _e;
467
+ indexByFile.set(file, index);
468
+ lowlink.set(file, index);
469
+ index++;
470
+ stack.push(file);
471
+ onStack.add(file);
472
+ for (const dep of (_a = deps.get(file)) !== null && _a !== void 0 ? _a : []) {
473
+ if (!indexByFile.has(dep)) {
474
+ strongConnect(dep);
475
+ lowlink.set(file, Math.min((_b = lowlink.get(file)) !== null && _b !== void 0 ? _b : 0, (_c = lowlink.get(dep)) !== null && _c !== void 0 ? _c : 0));
476
+ }
477
+ else if (onStack.has(dep)) {
478
+ lowlink.set(file, Math.min((_d = lowlink.get(file)) !== null && _d !== void 0 ? _d : 0, (_e = indexByFile.get(dep)) !== null && _e !== void 0 ? _e : 0));
479
+ }
480
+ }
481
+ if (lowlink.get(file) !== indexByFile.get(file))
482
+ return;
483
+ const component = [];
484
+ while (stack.length > 0) {
485
+ const dep = stack.pop();
486
+ onStack.delete(dep);
487
+ component.push(dep);
488
+ if (dep === file)
489
+ break;
490
+ }
491
+ if (component.length > 1)
492
+ components.push(component);
493
+ };
494
+ const files = new Set();
495
+ for (const [file, targets] of deps) {
496
+ files.add(file);
497
+ for (const target of targets)
498
+ files.add(target);
499
+ }
500
+ for (const file of [...files].sort()) {
501
+ if (!indexByFile.has(file))
502
+ strongConnect(file);
503
+ }
504
+ return components
505
+ .map((component) => {
506
+ var _a;
507
+ const set = new Set(component);
508
+ let edgeCount = 0;
509
+ for (const file of component) {
510
+ for (const dep of (_a = deps.get(file)) !== null && _a !== void 0 ? _a : []) {
511
+ if (set.has(dep))
512
+ edgeCount++;
513
+ }
514
+ }
515
+ return {
516
+ files: component.map((f) => rel(f, prefix)).sort(),
517
+ edgeCount,
518
+ };
519
+ })
520
+ .sort((a, b) => b.files.length - a.files.length ||
521
+ b.edgeCount - a.edgeCount ||
522
+ a.files.join("\0").localeCompare(b.files.join("\0")))
523
+ .slice(0, top);
524
+ }
525
+ /**
526
+ * Pure aggregation over chunk rows — no DB, no I/O. Builds the symbol→def map,
527
+ * cross-file inbound edges, and per-file fan-in/out, then derives god nodes
528
+ * (most depended-upon symbols), hub files (most depended-upon files), and
529
+ * dead-code candidates (non-exported symbols with zero inbound references).
530
+ * `top` caps each list; `deadTotal` reports the full pre-cap dead count.
531
+ */
532
+ function computeAudit(rows, prefix, top) {
533
+ var _a, _b;
534
+ // First definition of a symbol wins (matches GraphBuilder semantics).
535
+ const defs = new Map();
536
+ // Distinct files defining each name — name-based edges can't tell same-name
537
+ // symbols apart, so multi-file definitions get flagged in the output.
538
+ const defFileCounts = new Map();
539
+ // Distinct files that reference a symbol (cross-file inbound edges).
540
+ const inboundFiles = new Map();
541
+ const inboundTotal = new Map();
542
+ // Per-file aggregates.
543
+ const fileDefs = new Map();
544
+ const fileOutRefs = new Map();
545
+ const files = new Set();
546
+ for (const row of rows) {
547
+ const file = String(row.path || "");
548
+ const line = Number(row.start_line || 0);
549
+ const exported = Boolean(row.is_exported);
550
+ const defSyms = (0, arrow_1.toArr)(row.defined_symbols);
551
+ const refSyms = (0, arrow_1.toArr)(row.referenced_symbols);
552
+ files.add(file);
553
+ for (const s of defSyms) {
554
+ if (!defs.has(s)) {
555
+ defs.set(s, { file, line, exported, complexity: 0 });
556
+ }
557
+ if (!defFileCounts.has(s))
558
+ defFileCounts.set(s, new Set());
559
+ defFileCounts.get(s).add(file);
560
+ if (!fileDefs.has(file))
561
+ fileDefs.set(file, new Set());
562
+ fileDefs.get(file).add(s);
563
+ }
564
+ for (const s of refSyms) {
565
+ if (!inboundFiles.has(s))
566
+ inboundFiles.set(s, new Set());
567
+ inboundFiles.get(s).add(file);
568
+ inboundTotal.set(s, (inboundTotal.get(s) || 0) + 1);
569
+ if (!fileOutRefs.has(file))
570
+ fileOutRefs.set(file, new Set());
571
+ fileOutRefs.get(file).add(s);
572
+ }
573
+ }
574
+ // God nodes — in-project symbols by distinct external inbound files.
575
+ const godNodes = [];
576
+ for (const [symbol, info] of defs) {
577
+ if (symbol.length < MIN_GOD_NAME_LEN)
578
+ continue;
579
+ // Builtin method names (get, set, push, …) leak in via prototype/member
580
+ // definitions and their inbound counts are meaningless name collisions.
581
+ if ((0, callsites_1.isBuiltinCallee)(symbol))
582
+ continue;
583
+ const refFiles = inboundFiles.get(symbol);
584
+ if (!refFiles)
585
+ continue;
586
+ let external = 0;
587
+ for (const f of refFiles)
588
+ if (f !== info.file)
589
+ external++;
590
+ if (external === 0)
591
+ continue;
592
+ godNodes.push({
593
+ symbol,
594
+ file: rel(info.file, prefix),
595
+ line: info.line,
596
+ inboundFiles: external,
597
+ totalRefs: inboundTotal.get(symbol) || 0,
598
+ defFiles: (_b = (_a = defFileCounts.get(symbol)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 1,
599
+ });
600
+ }
601
+ godNodes.sort((a, b) => b.inboundFiles - a.inboundFiles || b.totalRefs - a.totalRefs);
602
+ // Hub files — distinct external files depending on each file (a file G
603
+ // depends on F if G references any symbol F defines).
604
+ const hubFiles = [];
605
+ for (const [file, syms] of fileDefs) {
606
+ const dependents = new Set();
607
+ for (const s of syms) {
608
+ const refFiles = inboundFiles.get(s);
609
+ if (!refFiles)
610
+ continue;
611
+ for (const f of refFiles)
612
+ if (f !== file)
613
+ dependents.add(f);
614
+ }
615
+ // Fan-out: distinct referenced symbols that are defined somewhere
616
+ // in-project (external-library calls don't count as coupling).
617
+ let fanOut = 0;
618
+ const out = fileOutRefs.get(file);
619
+ if (out)
620
+ for (const s of out)
621
+ if (defs.has(s))
622
+ fanOut++;
623
+ hubFiles.push({
624
+ file: rel(file, prefix),
625
+ dependents: dependents.size,
626
+ defines: syms.size,
627
+ fanOut,
628
+ });
629
+ }
630
+ hubFiles.sort((a, b) => b.dependents - a.dependents || b.defines - a.defines);
631
+ const fileDeps = new Map();
632
+ for (const [file, refs] of fileOutRefs) {
633
+ for (const s of refs) {
634
+ if ((0, callsites_1.isBuiltinCallee)(s))
635
+ continue;
636
+ const defFiles = defFileCounts.get(s);
637
+ if (!defFiles || defFiles.size !== 1)
638
+ continue;
639
+ const [depFile] = defFiles;
640
+ if (!depFile || depFile === file)
641
+ continue;
642
+ if (!fileDeps.has(file))
643
+ fileDeps.set(file, new Set());
644
+ fileDeps.get(file).add(depFile);
645
+ }
646
+ }
647
+ const fileCycles = findFileCycles(fileDeps, prefix, top);
648
+ // Dead candidates — non-exported in-project symbols with zero inbound
649
+ // references anywhere (including their own file).
650
+ const deadAll = [];
651
+ for (const [symbol, info] of defs) {
652
+ if (info.exported)
653
+ continue;
654
+ if ((inboundTotal.get(symbol) || 0) > 0)
655
+ continue;
656
+ deadAll.push({ symbol, file: rel(info.file, prefix), line: info.line });
657
+ }
658
+ deadAll.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
659
+ return {
660
+ scannedChunks: rows.length,
661
+ scannedFiles: files.size,
662
+ godNodes: godNodes.slice(0, top),
663
+ hubFiles: hubFiles.filter((h) => h.dependents > 0).slice(0, top),
664
+ fileCycles,
665
+ deadCandidates: deadAll.slice(0, top),
666
+ deadTotal: deadAll.length,
667
+ };
668
+ }
669
+ /**
670
+ * One pass over the scoped chunk rows, aggregated into the audit report.
671
+ *
672
+ * Returns null for an empty scope so the caller can print its own "no indexed
673
+ * data" line. Never returns rows: the 500k-row read stays daemon-side.
674
+ */
675
+ function runGraphAudit(db, args) {
676
+ return __awaiter(this, void 0, void 0, function* () {
677
+ const top = clampInt(args.top, 1, MAX_AUDIT_TOP, 10);
678
+ const prefix = withTrailingSlash(args.projectRoot);
679
+ const table = yield db.ensureTable();
680
+ const rows = yield table
681
+ .query()
682
+ .select([
683
+ "path",
684
+ "start_line",
685
+ "defined_symbols",
686
+ "referenced_symbols",
687
+ "type_referenced_symbols",
688
+ "is_exported",
689
+ ])
690
+ .where((0, scope_filter_1.buildScopeWhere)(args.scope))
691
+ .limit(AUDIT_ROW_LIMIT)
692
+ .toArray();
693
+ if (rows.length === 0)
694
+ return null;
695
+ return computeAudit(rows.map((r) => {
696
+ const row = r;
697
+ return {
698
+ path: String(row.path || ""),
699
+ start_line: Number(row.start_line || 0),
700
+ is_exported: Boolean(row.is_exported),
701
+ defined_symbols: (0, arrow_1.toArr)(row.defined_symbols),
702
+ // Union call-position + type-position edges so god-node ranking and
703
+ // dead-candidate detection see references made purely in type position.
704
+ referenced_symbols: [
705
+ ...new Set([
706
+ ...(0, arrow_1.toArr)(row.referenced_symbols),
707
+ ...(0, arrow_1.toArr)(row.type_referenced_symbols),
708
+ ]),
709
+ ],
710
+ };
711
+ }), prefix, top);
712
+ });
713
+ }
714
+ // --- Verb handlers ---------------------------------------------------------
715
+ /**
716
+ * Build the `graph.*` handler table over a deps supplier. `read-verbs.ts`
717
+ * passes one that reads the live daemon's store; tests pass a temp VectorDB.
718
+ */
719
+ function createGraphVerbs(getDeps) {
720
+ const handlers = {
721
+ "graph.resolve": (payload, db) => __awaiter(this, void 0, void 0, function* () {
722
+ const { projectRoot } = decodeScopePayload(payload);
723
+ const result = yield runGraphResolve(db, {
724
+ target: requireString(payload, "target"),
725
+ projectRoot,
726
+ });
727
+ return Object.assign({ ok: true }, result);
728
+ }),
729
+ "graph.tests": (payload, db) => __awaiter(this, void 0, void 0, function* () {
730
+ const decoded = decodeScopePayload(payload);
731
+ const hits = yield runGraphTests(db, {
732
+ symbols: requireSymbols(payload),
733
+ queryRoot: queryRootFor(decoded),
734
+ depth: clampInt(payload.depth, 1, MAX_DEPTH, 1),
735
+ excludePrefixes: decoded.scope.excludePrefixes,
736
+ families: decodeSymbolFamilies(payload.families),
737
+ });
738
+ return { ok: true, hits };
739
+ }),
740
+ "graph.dependents": (payload, db) => __awaiter(this, void 0, void 0, function* () {
741
+ const decoded = decodeScopePayload(payload);
742
+ const excludePaths = Array.isArray(payload.excludePaths)
743
+ ? payload.excludePaths.filter((p) => typeof p === "string")
744
+ : undefined;
745
+ const dependents = yield runGraphDependents(db, {
746
+ symbols: requireSymbols(payload),
747
+ queryRoot: queryRootFor(decoded),
748
+ detailed: payload.detailed === true,
749
+ excludePaths,
750
+ limit: typeof payload.limit === "number" ? payload.limit : undefined,
751
+ excludePrefixes: decoded.scope.excludePrefixes,
752
+ families: decodeSymbolFamilies(payload.families),
753
+ });
754
+ return { ok: true, dependents };
755
+ }),
756
+ "graph.trace": (payload, db) => __awaiter(this, void 0, void 0, function* () {
757
+ const decoded = decodeScopePayload(payload);
758
+ const graph = yield runGraphTrace(db, {
759
+ symbol: requireString(payload, "target"),
760
+ hops: clampInt(payload.hops, 1, MAX_DEPTH, 1),
761
+ scope: decoded.scope,
762
+ });
763
+ return { ok: true, graph };
764
+ }),
765
+ "graph.peek": (payload, db) => __awaiter(this, void 0, void 0, function* () {
766
+ const decoded = decodeScopePayload(payload);
767
+ const result = yield runGraphPeek(db, {
768
+ symbol: requireString(payload, "target"),
769
+ depth: clampInt(payload.depth, 1, MAX_DEPTH, 1),
770
+ scope: decoded.scope,
771
+ includeTests: payload.includeTests === true,
772
+ });
773
+ return { ok: true, peek: result };
774
+ }),
775
+ "graph.dead": (payload, db) => __awaiter(this, void 0, void 0, function* () {
776
+ const decoded = decodeScopePayload(payload);
777
+ const facts = yield runGraphDead(db, {
778
+ symbol: requireString(payload, "target"),
779
+ scope: decoded.scope,
780
+ });
781
+ return { ok: true, dead: facts };
782
+ }),
783
+ "graph.neighbors": (payload, db) => __awaiter(this, void 0, void 0, function* () {
784
+ const decoded = decodeScopePayload(payload);
785
+ const hits = yield runGraphNeighbors(db, {
786
+ symbol: requireString(payload, "symbol"),
787
+ direction: requireDirection(payload),
788
+ maxHops: clampInt(payload.maxHops, 1, MAX_NEIGHBOR_HOPS, 2),
789
+ scope: decoded.scope,
790
+ });
791
+ return { ok: true, hits };
792
+ }),
793
+ "graph.paths": (payload, db) => __awaiter(this, void 0, void 0, function* () {
794
+ const decoded = decodeScopePayload(payload);
795
+ const path = yield runGraphPaths(db, {
796
+ from: requireString(payload, "from"),
797
+ to: requireString(payload, "to"),
798
+ direction: requireDirection(payload),
799
+ maxHops: clampInt(payload.maxHops, 1, MAX_PATH_HOPS, 6),
800
+ scope: decoded.scope,
801
+ });
802
+ // `null` (no path) is an answer, not an error — send it as one.
803
+ return { ok: true, path };
804
+ }),
805
+ "graph.subgraph": (payload, db) => __awaiter(this, void 0, void 0, function* () {
806
+ const decoded = decodeScopePayload(payload);
807
+ const subgraph = yield runGraphSubgraph(db, {
808
+ files: requireContainedFiles(decoded.projectRoot, payload.files),
809
+ scope: decoded.scope,
810
+ });
811
+ return { ok: true, subgraph };
812
+ }),
813
+ "graph.risk": (payload, db) => __awaiter(this, void 0, void 0, function* () {
814
+ const decoded = decodeScopePayload(payload);
815
+ const facts = yield runGraphRisk(db, {
816
+ symbols: requireSymbols(payload),
817
+ queryRoot: queryRootFor(decoded),
818
+ scope: decoded.scope,
819
+ });
820
+ return { ok: true, facts };
821
+ }),
822
+ "graph.audit": (payload, db) => __awaiter(this, void 0, void 0, function* () {
823
+ const decoded = decodeScopePayload(payload);
824
+ const report = yield runGraphAudit(db, {
825
+ projectRoot: decoded.projectRoot,
826
+ scope: decoded.scope,
827
+ top: clampInt(payload.top, 1, MAX_AUDIT_TOP, 10),
828
+ });
829
+ return { ok: true, audit: report };
830
+ }),
831
+ };
832
+ const wired = {};
833
+ for (const [name, run] of Object.entries(handlers)) {
834
+ wired[name] = (payload, ctx) => __awaiter(this, void 0, void 0, function* () { return run(payload, requireStore(getDeps(ctx))); });
835
+ }
836
+ return wired;
837
+ }
838
+ // --- Client side -----------------------------------------------------------
839
+ /**
840
+ * A graph verb can walk several hops over a large table; the 5 s default would
841
+ * time out on a big project. Same budget `search` gives the daemon.
842
+ */
843
+ exports.GRAPH_VERB_TIMEOUT_MS = 60000;
844
+ /**
845
+ * Send one graph verb through the store-access policy.
846
+ *
847
+ * `fallbackOnUnknownVerb` is the one-release skew allowance: a daemon older
848
+ * than this CLI answers `unknown command`, and the command still works. An
849
+ * unregistered project skips the daemon outright — see isDaemonReadableProject.
850
+ */
851
+ function callGraphVerb(verb, call) {
852
+ return (0, store_access_1.withStoreRead)(verb, Object.assign({ skipDaemon: !isDaemonReadableProject(call.projectRoot), fallbackOnUnknownVerb: true, daemon: () => (0, daemon_client_1.sendDaemonCommand)(Object.assign(Object.assign({ cmd: verb }, encodeScope(call.projectRoot, call.scope)), call.payload), { timeoutMs: exports.GRAPH_VERB_TIMEOUT_MS }), render: call.render, inProcess: call.inProcess }, call.fallback));
853
+ }