grepmax 0.17.23 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/audit.js +10 -2
- package/dist/commands/dead.js +3 -0
- package/dist/commands/doctor.js +103 -10
- package/dist/commands/impact.js +3 -0
- package/dist/commands/peek.js +3 -0
- package/dist/commands/related.js +3 -0
- package/dist/commands/search-output.js +531 -0
- package/dist/commands/search.js +46 -462
- package/dist/commands/similar.js +3 -0
- package/dist/commands/test-find.js +3 -0
- package/dist/commands/trace.js +3 -0
- package/dist/config.js +80 -5
- package/dist/eval-graph-nav.js +374 -0
- package/dist/eval-graph-sanity.js +138 -19
- package/dist/lib/daemon/daemon.js +29 -643
- package/dist/lib/daemon/mlx-server-manager.js +191 -0
- package/dist/lib/daemon/process-manager.js +139 -0
- package/dist/lib/daemon/watcher-manager.js +484 -0
- 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 +164 -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,484 @@
|
|
|
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
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
45
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
46
|
+
var m = o[Symbol.asyncIterator], i;
|
|
47
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
48
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
49
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
50
|
+
};
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.WatcherManager = void 0;
|
|
53
|
+
const watcher = __importStar(require("@parcel/watcher"));
|
|
54
|
+
const fs = __importStar(require("node:fs"));
|
|
55
|
+
const path = __importStar(require("node:path"));
|
|
56
|
+
const batch_processor_1 = require("../index/batch-processor");
|
|
57
|
+
const watcher_1 = require("../index/watcher");
|
|
58
|
+
const logger_1 = require("../utils/logger");
|
|
59
|
+
const project_registry_1 = require("../utils/project-registry");
|
|
60
|
+
const watcher_store_1 = require("../utils/watcher-store");
|
|
61
|
+
// Watcher health windows used for FSEvents auto-recovery.
|
|
62
|
+
const FSEVENTS_RECOVERY_INTERVAL_MS = 60 * 60 * 1000; // try recovery hourly
|
|
63
|
+
const FSEVENTS_HEALTH_WINDOW_MS = 5 * 60 * 1000; // 5 min of quiet = "healthy"
|
|
64
|
+
/**
|
|
65
|
+
* Owns per-project file watching: @parcel/watcher subscriptions, FSEvents
|
|
66
|
+
* overflow recovery, poll-mode fallback, and the offline catchup scan. Holds the
|
|
67
|
+
* watcher health/state maps; shares the `processors` and `subscriptions` maps
|
|
68
|
+
* with the daemon by reference so the streaming index ops and shutdown can still
|
|
69
|
+
* detach a project directly.
|
|
70
|
+
*
|
|
71
|
+
* Extracted from daemon.ts (Phase 2). Behavior-preserving — the daemon keeps
|
|
72
|
+
* thin watchProject/unwatchProject delegators over this manager.
|
|
73
|
+
*/
|
|
74
|
+
class WatcherManager {
|
|
75
|
+
constructor(deps) {
|
|
76
|
+
this.deps = deps;
|
|
77
|
+
this.pendingOps = new Set();
|
|
78
|
+
this.watcherFailCount = new Map();
|
|
79
|
+
this.pollIntervals = new Map();
|
|
80
|
+
this.pollRecoveryTimers = new Map();
|
|
81
|
+
this.lastOverflowMs = new Map();
|
|
82
|
+
this.lastCatchupEndMs = new Map();
|
|
83
|
+
}
|
|
84
|
+
watchProject(root) {
|
|
85
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
86
|
+
if (this.deps.processors.has(root) || this.pendingOps.has(root))
|
|
87
|
+
return;
|
|
88
|
+
const vectorDb = this.deps.getVectorDb();
|
|
89
|
+
const metaCache = this.deps.getMetaCache();
|
|
90
|
+
if (!vectorDb || !metaCache)
|
|
91
|
+
return;
|
|
92
|
+
this.pendingOps.add(root);
|
|
93
|
+
const processor = new batch_processor_1.ProjectBatchProcessor({
|
|
94
|
+
projectRoot: root,
|
|
95
|
+
vectorDb,
|
|
96
|
+
metaCache,
|
|
97
|
+
onReindex: (files, ms) => __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
console.log(`[daemon:${path.basename(root)}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
|
|
99
|
+
// Update project registry so gmax status shows fresh data
|
|
100
|
+
const proj = (0, project_registry_1.getProject)(root);
|
|
101
|
+
if (proj) {
|
|
102
|
+
let chunkCount = proj.chunkCount;
|
|
103
|
+
try {
|
|
104
|
+
chunkCount = yield vectorDb.countRowsForPath(root);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.warn(`[daemon:${path.basename(root)}] Failed to query chunk count: ${err}`);
|
|
108
|
+
}
|
|
109
|
+
(0, project_registry_1.registerProject)(Object.assign(Object.assign({}, proj), { lastIndexed: new Date().toISOString(), chunkCount }));
|
|
110
|
+
}
|
|
111
|
+
// Back to watching after batch completes
|
|
112
|
+
(0, watcher_store_1.registerWatcher)({
|
|
113
|
+
pid: process.pid,
|
|
114
|
+
projectRoot: root,
|
|
115
|
+
startTime: Date.now(),
|
|
116
|
+
status: "watching",
|
|
117
|
+
lastHeartbeat: Date.now(),
|
|
118
|
+
lastReindex: Date.now(),
|
|
119
|
+
});
|
|
120
|
+
}),
|
|
121
|
+
onActivity: () => {
|
|
122
|
+
this.deps.touchActivity();
|
|
123
|
+
// Mark as syncing while processing
|
|
124
|
+
(0, watcher_store_1.registerWatcher)({
|
|
125
|
+
pid: process.pid,
|
|
126
|
+
projectRoot: root,
|
|
127
|
+
startTime: Date.now(),
|
|
128
|
+
status: "syncing",
|
|
129
|
+
lastHeartbeat: Date.now(),
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
this.deps.processors.set(root, processor);
|
|
134
|
+
// Subscribe with @parcel/watcher — native backend, no polling.
|
|
135
|
+
// If the kernel refuses (e.g. FSEvents slots stuck after a prior kill -9),
|
|
136
|
+
// fall straight through to poll mode. The retry/backoff path inside
|
|
137
|
+
// recoverWatcher is for transient overflows, not hard kernel-level
|
|
138
|
+
// subscribe failures, so we skip it on startup by priming failCount past
|
|
139
|
+
// MAX before invoking it.
|
|
140
|
+
try {
|
|
141
|
+
yield this.subscribeWatcher(root, processor);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const name = path.basename(root);
|
|
145
|
+
console.error(`[daemon:${name}] Subscribe failed at startup (${err instanceof Error ? err.message : err}) — switching to poll mode`);
|
|
146
|
+
this.watcherFailCount.set(root, 1000); // > MAX_WATCHER_RETRIES
|
|
147
|
+
this.lastOverflowMs.set(root, Date.now());
|
|
148
|
+
this.recoverWatcher(root, processor);
|
|
149
|
+
}
|
|
150
|
+
(0, watcher_store_1.registerWatcher)({
|
|
151
|
+
pid: process.pid,
|
|
152
|
+
projectRoot: root,
|
|
153
|
+
startTime: Date.now(),
|
|
154
|
+
status: "watching",
|
|
155
|
+
lastHeartbeat: Date.now(),
|
|
156
|
+
});
|
|
157
|
+
// Catchup scan — find files changed while daemon was offline
|
|
158
|
+
this.catchupScan(root, processor).catch((err) => {
|
|
159
|
+
console.error(`[daemon:${path.basename(root)}] Catchup scan failed:`, err);
|
|
160
|
+
});
|
|
161
|
+
this.pendingOps.delete(root);
|
|
162
|
+
console.log(`[daemon] Watching ${root}`);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
subscribeWatcher(root, processor) {
|
|
166
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
167
|
+
const name = path.basename(root);
|
|
168
|
+
// Unsubscribe existing watcher if any (e.g. during recovery)
|
|
169
|
+
const existingSub = this.deps.subscriptions.get(root);
|
|
170
|
+
if (existingSub) {
|
|
171
|
+
try {
|
|
172
|
+
yield existingSub.unsubscribe();
|
|
173
|
+
}
|
|
174
|
+
catch (_a) { }
|
|
175
|
+
this.deps.subscriptions.delete(root);
|
|
176
|
+
}
|
|
177
|
+
const sub = yield watcher.subscribe(root, (err, events) => {
|
|
178
|
+
var _a;
|
|
179
|
+
if (err) {
|
|
180
|
+
console.error(`[daemon:${name}] Watcher error:`, err);
|
|
181
|
+
this.recoverWatcher(root, processor);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Only reset fail counter after sustained health (5min since last overflow)
|
|
185
|
+
const lastOverflow = (_a = this.lastOverflowMs.get(root)) !== null && _a !== void 0 ? _a : 0;
|
|
186
|
+
if (Date.now() - lastOverflow > 5 * 60 * 1000) {
|
|
187
|
+
this.watcherFailCount.delete(root);
|
|
188
|
+
}
|
|
189
|
+
for (const event of events) {
|
|
190
|
+
processor.handleFileEvent(event.type === "delete" ? "unlink" : "change", event.path);
|
|
191
|
+
}
|
|
192
|
+
this.deps.touchActivity();
|
|
193
|
+
}, { ignore: watcher_1.WATCHER_IGNORE_GLOBS });
|
|
194
|
+
this.deps.subscriptions.set(root, sub);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
recoverWatcher(root, processor) {
|
|
198
|
+
var _a;
|
|
199
|
+
const name = path.basename(root);
|
|
200
|
+
if (this.deps.getShuttingDown())
|
|
201
|
+
return;
|
|
202
|
+
// Debounce: avoid multiple overlapping recovery attempts
|
|
203
|
+
const recoveryKey = `recover:${root}`;
|
|
204
|
+
if (this.pendingOps.has(recoveryKey))
|
|
205
|
+
return;
|
|
206
|
+
this.pendingOps.add(recoveryKey);
|
|
207
|
+
const fails = ((_a = this.watcherFailCount.get(root)) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
208
|
+
this.watcherFailCount.set(root, fails);
|
|
209
|
+
this.lastOverflowMs.set(root, Date.now());
|
|
210
|
+
const MAX_WATCHER_RETRIES = 3;
|
|
211
|
+
const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
212
|
+
if (fails > MAX_WATCHER_RETRIES) {
|
|
213
|
+
// FSEvents can't handle this project — degrade to periodic catchup scans
|
|
214
|
+
// Always tear down the broken sub, even if poll mode is already active —
|
|
215
|
+
// this can happen if a recovery attempt resubscribed successfully then
|
|
216
|
+
// re-overflowed during the 5-min health window.
|
|
217
|
+
const sub = this.deps.subscriptions.get(root);
|
|
218
|
+
if (sub) {
|
|
219
|
+
sub.unsubscribe().catch(() => { });
|
|
220
|
+
this.deps.subscriptions.delete(root);
|
|
221
|
+
}
|
|
222
|
+
if (!this.pollIntervals.has(root)) {
|
|
223
|
+
console.error(`[daemon:${name}] FSEvents unreliable after ${fails} failures — switching to poll mode (${POLL_INTERVAL_MS / 60000}min interval)`);
|
|
224
|
+
// Run an immediate catchup, then schedule periodic ones
|
|
225
|
+
this.catchupScan(root, processor).catch((err) => {
|
|
226
|
+
console.error(`[daemon:${name}] Poll catchup failed:`, err);
|
|
227
|
+
});
|
|
228
|
+
const interval = setInterval(() => {
|
|
229
|
+
if (this.deps.getShuttingDown())
|
|
230
|
+
return;
|
|
231
|
+
this.deps.touchActivity();
|
|
232
|
+
this.catchupScan(root, processor).catch((err) => {
|
|
233
|
+
console.error(`[daemon:${name}] Poll catchup failed:`, err);
|
|
234
|
+
});
|
|
235
|
+
}, POLL_INTERVAL_MS);
|
|
236
|
+
this.pollIntervals.set(root, interval);
|
|
237
|
+
// Schedule periodic attempts to climb back to native FSEvents — after
|
|
238
|
+
// a transient burst (large git checkout, npm install) the kernel
|
|
239
|
+
// buffer often calms down within an hour.
|
|
240
|
+
this.schedulePollModeRecovery(root, processor);
|
|
241
|
+
(0, watcher_store_1.registerWatcher)({
|
|
242
|
+
pid: process.pid,
|
|
243
|
+
projectRoot: root,
|
|
244
|
+
startTime: Date.now(),
|
|
245
|
+
status: "watching",
|
|
246
|
+
lastHeartbeat: Date.now(),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
this.pendingOps.delete(recoveryKey);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// Backoff: wait before re-subscribing (3s, 6s, 12s)
|
|
253
|
+
const delayMs = 3000 * Math.pow(2, fails - 1);
|
|
254
|
+
console.error(`[daemon:${name}] Recovering watcher (attempt ${fails}/${MAX_WATCHER_RETRIES}, backoff ${delayMs}ms)...`);
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
if (this.deps.getShuttingDown()) {
|
|
257
|
+
this.pendingOps.delete(recoveryKey);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
(() => __awaiter(this, void 0, void 0, function* () {
|
|
261
|
+
var _a;
|
|
262
|
+
try {
|
|
263
|
+
yield this.subscribeWatcher(root, processor);
|
|
264
|
+
const lastCatchup = (_a = this.lastCatchupEndMs.get(root)) !== null && _a !== void 0 ? _a : 0;
|
|
265
|
+
const CATCHUP_COOLDOWN_MS = 60000;
|
|
266
|
+
if (Date.now() - lastCatchup < CATCHUP_COOLDOWN_MS) {
|
|
267
|
+
console.log(`[daemon:${name}] Skipping catchup scan (last completed ${Math.round((Date.now() - lastCatchup) / 1000)}s ago)`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
yield this.catchupScan(root, processor);
|
|
271
|
+
}
|
|
272
|
+
console.log(`[daemon:${name}] Watcher recovered`);
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
console.error(`[daemon:${name}] Watcher recovery failed:`, err);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
this.pendingOps.delete(recoveryKey);
|
|
279
|
+
}
|
|
280
|
+
}))();
|
|
281
|
+
}, delayMs);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Once a project has fallen back to poll mode, periodically try to upgrade
|
|
285
|
+
* back to native FSEvents. The buffer overflows that triggered the fallback
|
|
286
|
+
* are usually transient (big git checkout, npm install, build output) — no
|
|
287
|
+
* point staying in 5-min poll mode forever.
|
|
288
|
+
*/
|
|
289
|
+
schedulePollModeRecovery(root, processor) {
|
|
290
|
+
if (this.pollRecoveryTimers.has(root))
|
|
291
|
+
return;
|
|
292
|
+
const name = path.basename(root);
|
|
293
|
+
const timer = setInterval(() => {
|
|
294
|
+
if (this.deps.getShuttingDown())
|
|
295
|
+
return;
|
|
296
|
+
// Skip if a watcher recovery is already in flight or we're not in poll mode anymore.
|
|
297
|
+
if (!this.pollIntervals.has(root)) {
|
|
298
|
+
const t = this.pollRecoveryTimers.get(root);
|
|
299
|
+
if (t)
|
|
300
|
+
clearInterval(t);
|
|
301
|
+
this.pollRecoveryTimers.delete(root);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (this.pendingOps.has(`recover:${root}`))
|
|
305
|
+
return;
|
|
306
|
+
void (() => __awaiter(this, void 0, void 0, function* () {
|
|
307
|
+
var _a;
|
|
308
|
+
console.log(`[daemon:${name}] Attempting to leave poll mode and reattach FSEvents...`);
|
|
309
|
+
try {
|
|
310
|
+
// Reset failure counter so subscribeWatcher's error path treats this
|
|
311
|
+
// as a fresh start. If it fails again, we'll fall right back into
|
|
312
|
+
// poll mode via the same recoverWatcher path.
|
|
313
|
+
this.watcherFailCount.delete(root);
|
|
314
|
+
yield this.subscribeWatcher(root, processor);
|
|
315
|
+
// Wait one health window — if the new subscription survives without
|
|
316
|
+
// another overflow, we consider it recovered and tear down poll mode.
|
|
317
|
+
yield new Promise((r) => setTimeout(r, FSEVENTS_HEALTH_WINDOW_MS));
|
|
318
|
+
if (this.deps.getShuttingDown())
|
|
319
|
+
return;
|
|
320
|
+
const lastOverflow = (_a = this.lastOverflowMs.get(root)) !== null && _a !== void 0 ? _a : 0;
|
|
321
|
+
if (Date.now() - lastOverflow < FSEVENTS_HEALTH_WINDOW_MS) {
|
|
322
|
+
console.log(`[daemon:${name}] FSEvents recovery aborted — fresh overflow within health window, staying in poll mode`);
|
|
323
|
+
return; // recoverWatcher will have re-armed poll mode if needed
|
|
324
|
+
}
|
|
325
|
+
// Healthy — drop poll mode.
|
|
326
|
+
const pollInterval = this.pollIntervals.get(root);
|
|
327
|
+
if (pollInterval) {
|
|
328
|
+
clearInterval(pollInterval);
|
|
329
|
+
this.pollIntervals.delete(root);
|
|
330
|
+
}
|
|
331
|
+
const recoveryTimer = this.pollRecoveryTimers.get(root);
|
|
332
|
+
if (recoveryTimer) {
|
|
333
|
+
clearInterval(recoveryTimer);
|
|
334
|
+
this.pollRecoveryTimers.delete(root);
|
|
335
|
+
}
|
|
336
|
+
console.log(`[daemon:${name}] FSEvents recovered — poll mode disabled`);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
console.error(`[daemon:${name}] Poll-mode recovery attempt failed:`, err);
|
|
340
|
+
}
|
|
341
|
+
}))();
|
|
342
|
+
}, FSEVENTS_RECOVERY_INTERVAL_MS);
|
|
343
|
+
timer.unref();
|
|
344
|
+
this.pollRecoveryTimers.set(root, timer);
|
|
345
|
+
}
|
|
346
|
+
catchupScan(root, processor) {
|
|
347
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
348
|
+
var _a, e_1, _b, _c;
|
|
349
|
+
const { walk } = yield Promise.resolve().then(() => __importStar(require("../index/walker")));
|
|
350
|
+
const { INDEXABLE_EXTENSIONS, MAX_FILE_SIZE_BYTES } = yield Promise.resolve().then(() => __importStar(require("../../config")));
|
|
351
|
+
const { isFileCached } = yield Promise.resolve().then(() => __importStar(require("../utils/cache-check")));
|
|
352
|
+
const metaCache = this.deps.getMetaCache();
|
|
353
|
+
const rootPrefix = root.endsWith("/") ? root : `${root}/`;
|
|
354
|
+
const cachedPaths = yield metaCache.getKeysWithPrefix(rootPrefix);
|
|
355
|
+
const seenPaths = new Set();
|
|
356
|
+
let queued = 0;
|
|
357
|
+
let skipped = 0;
|
|
358
|
+
let debugSamples = 0;
|
|
359
|
+
try {
|
|
360
|
+
for (var _d = true, _e = __asyncValues(walk(root, {
|
|
361
|
+
additionalPatterns: ["**/.git/**", "**/.gmax/**"],
|
|
362
|
+
})), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
363
|
+
_c = _f.value;
|
|
364
|
+
_d = false;
|
|
365
|
+
const relPath = _c;
|
|
366
|
+
const absPath = path.join(root, relPath);
|
|
367
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
368
|
+
const bn = path.basename(absPath).toLowerCase();
|
|
369
|
+
if (!INDEXABLE_EXTENSIONS.has(ext) && !INDEXABLE_EXTENSIONS.has(bn))
|
|
370
|
+
continue;
|
|
371
|
+
seenPaths.add(absPath);
|
|
372
|
+
try {
|
|
373
|
+
const stats = yield fs.promises.stat(absPath);
|
|
374
|
+
// Skip files that are too large or empty — they'll never be indexed
|
|
375
|
+
if (stats.size === 0 || stats.size > MAX_FILE_SIZE_BYTES)
|
|
376
|
+
continue;
|
|
377
|
+
const cached = metaCache.get(absPath);
|
|
378
|
+
if (!isFileCached(cached, stats)) {
|
|
379
|
+
// Fast path: if only mtime changed but size is identical and we have a hash,
|
|
380
|
+
// just verify the hash in-process instead of sending to a worker.
|
|
381
|
+
if (cached && cached.hash && cached.size === stats.size) {
|
|
382
|
+
const { computeBufferHash } = yield Promise.resolve().then(() => __importStar(require("../utils/file-utils")));
|
|
383
|
+
const buf = yield fs.promises.readFile(absPath);
|
|
384
|
+
const hash = computeBufferHash(buf);
|
|
385
|
+
if (hash === cached.hash) {
|
|
386
|
+
// Content unchanged — update mtime in cache and skip worker
|
|
387
|
+
metaCache.put(absPath, Object.assign(Object.assign({}, cached), { mtimeMs: stats.mtimeMs }));
|
|
388
|
+
skipped++;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Debug: log first few misses to diagnose re-queue loops
|
|
393
|
+
if (debugSamples < 5) {
|
|
394
|
+
(0, logger_1.debug)("catchup", `miss ${relPath}: cached=${cached ? `mtime=${Math.trunc(cached.mtimeMs)} size=${cached.size}` : "null"} stat=mtime=${Math.trunc(stats.mtimeMs)} size=${stats.size}`);
|
|
395
|
+
debugSamples++;
|
|
396
|
+
}
|
|
397
|
+
processor.handleFileEvent("change", absPath);
|
|
398
|
+
queued++;
|
|
399
|
+
// Throttle: pause periodically during large catchup scans to let the
|
|
400
|
+
// batch processor drain and compaction run between bursts.
|
|
401
|
+
if (queued % 500 === 0) {
|
|
402
|
+
(0, logger_1.debug)("catchup", `${path.basename(root)}: throttle pause at ${queued} queued`);
|
|
403
|
+
yield new Promise(r => setTimeout(r, 5000));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
skipped++;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (_g) { }
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
414
|
+
finally {
|
|
415
|
+
try {
|
|
416
|
+
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
417
|
+
}
|
|
418
|
+
finally { if (e_1) throw e_1.error; }
|
|
419
|
+
}
|
|
420
|
+
(0, logger_1.debug)("catchup", `${path.basename(root)}: ${queued} queued, ${skipped} skipped (cached ok), ${seenPaths.size} total`);
|
|
421
|
+
// Purge files deleted while daemon was offline
|
|
422
|
+
let purged = 0;
|
|
423
|
+
for (const cachedPath of cachedPaths) {
|
|
424
|
+
if (!seenPaths.has(cachedPath)) {
|
|
425
|
+
processor.handleFileEvent("unlink", cachedPath);
|
|
426
|
+
purged++;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (queued > 0 || purged > 0) {
|
|
430
|
+
const parts = [];
|
|
431
|
+
if (queued > 0)
|
|
432
|
+
parts.push(`${queued} changed`);
|
|
433
|
+
if (purged > 0)
|
|
434
|
+
parts.push(`${purged} deleted`);
|
|
435
|
+
console.log(`[daemon:${path.basename(root)}] Catchup: ${parts.join(", ")} file(s) while offline`);
|
|
436
|
+
}
|
|
437
|
+
this.lastCatchupEndMs.set(root, Date.now());
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
unwatchProject(root) {
|
|
441
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
442
|
+
const processor = this.deps.processors.get(root);
|
|
443
|
+
if (!processor)
|
|
444
|
+
return;
|
|
445
|
+
yield processor.close();
|
|
446
|
+
const sub = this.deps.subscriptions.get(root);
|
|
447
|
+
if (sub) {
|
|
448
|
+
yield sub.unsubscribe();
|
|
449
|
+
this.deps.subscriptions.delete(root);
|
|
450
|
+
}
|
|
451
|
+
this.deps.processors.delete(root);
|
|
452
|
+
this.deps.evictSearcher(root);
|
|
453
|
+
this.lastOverflowMs.delete(root);
|
|
454
|
+
this.lastCatchupEndMs.delete(root);
|
|
455
|
+
(0, watcher_store_1.unregisterWatcherByRoot)(root);
|
|
456
|
+
console.log(`[daemon] Unwatched ${root}`);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Stop all poll intervals + their FSEvents recovery probes and unsubscribe
|
|
461
|
+
* every watcher. Called from the daemon's shutdown after the worker pool is
|
|
462
|
+
* destroyed (the subscriptions map is shared, so it is cleared here).
|
|
463
|
+
*/
|
|
464
|
+
teardown() {
|
|
465
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
466
|
+
for (const interval of this.pollIntervals.values()) {
|
|
467
|
+
clearInterval(interval);
|
|
468
|
+
}
|
|
469
|
+
this.pollIntervals.clear();
|
|
470
|
+
for (const interval of this.pollRecoveryTimers.values()) {
|
|
471
|
+
clearInterval(interval);
|
|
472
|
+
}
|
|
473
|
+
this.pollRecoveryTimers.clear();
|
|
474
|
+
for (const sub of this.deps.subscriptions.values()) {
|
|
475
|
+
try {
|
|
476
|
+
yield sub.unsubscribe();
|
|
477
|
+
}
|
|
478
|
+
catch (_a) { }
|
|
479
|
+
}
|
|
480
|
+
this.deps.subscriptions.clear();
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
exports.WatcherManager = WatcherManager;
|
|
@@ -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) {
|