grepmax 0.26.40 → 0.26.41
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +4 -0
- package/dist/commands/codex.js +22 -16
- package/dist/commands/impact.js +2 -2
- package/dist/commands/mcp.js +37 -14
- package/dist/commands/peek.js +2 -2
- package/dist/commands/status.js +32 -28
- package/dist/commands/test-find.js +2 -2
- package/dist/commands/trace.js +1 -1
- package/dist/commands/watch.js +7 -0
- package/dist/lib/daemon/daemon.js +47 -15
- package/dist/lib/daemon/graph-handler.js +1 -1
- package/dist/lib/daemon/ipc-handler.js +10 -8
- package/dist/lib/daemon/search-handler.js +18 -8
- package/dist/lib/daemon/watcher-manager.js +37 -4
- package/dist/lib/graph/graph-builder.js +52 -4
- package/dist/lib/graph/impact-rollup.js +2 -2
- package/dist/lib/graph/impact.js +20 -8
- package/dist/lib/graph/test-hits.js +13 -7
- package/dist/lib/index/batch-processor.js +27 -2
- package/dist/lib/index/grammar-loader.js +22 -9
- package/dist/lib/output/formatter.js +1 -1
- package/dist/lib/output/index-state-footer.js +26 -6
- package/dist/lib/utils/resource-snapshot.js +20 -0
- package/dist/lib/utils/tests-footer.js +8 -16
- package/dist/lib/workers/embedding-error.js +16 -0
- package/dist/lib/workers/embeddings/mlx-client.js +94 -66
- package/dist/lib/workers/orchestrator.js +3 -2
- package/dist/lib/workers/pool.js +1 -1
- package/dist/lib/workers/process-child.js +2 -1
- package/package.json +1 -2
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -44,6 +44,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.handleDaemonSearch = handleDaemonSearch;
|
|
46
46
|
const embedding_generation_1 = require("../index/embedding-generation");
|
|
47
|
+
const index_state_footer_1 = require("../output/index-state-footer");
|
|
47
48
|
const searcher_1 = require("../search/searcher");
|
|
48
49
|
const retriever_1 = require("../skeleton/retriever");
|
|
49
50
|
const project_registry_1 = require("../utils/project-registry");
|
|
@@ -55,7 +56,7 @@ const project_registry_1 = require("../utils/project-registry");
|
|
|
55
56
|
function handleDaemonSearch(deps, payload, signal) {
|
|
56
57
|
return __awaiter(this, void 0, void 0, function* () {
|
|
57
58
|
var _a, _b, _c, _d;
|
|
58
|
-
var _e, _f;
|
|
59
|
+
var _e, _f, _g;
|
|
59
60
|
const { vectorDb, workerPool } = deps;
|
|
60
61
|
if (!vectorDb || !workerPool) {
|
|
61
62
|
return { ok: false, error: "daemon not ready" };
|
|
@@ -116,11 +117,20 @@ function handleDaemonSearch(deps, payload, signal) {
|
|
|
116
117
|
if ((_c = result.warnings) === null || _c === void 0 ? void 0 : _c.length)
|
|
117
118
|
response.warnings = result.warnings;
|
|
118
119
|
// Annotate partial results when the index is still catching up, so an
|
|
119
|
-
// agent can caveat or retry.
|
|
120
|
-
// formatter suppresses the settled case anyway).
|
|
120
|
+
// agent can caveat or retry. Settled failures remain visible too.
|
|
121
121
|
const idx = deps.getIndexState(root);
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
response.indexState = idx;
|
|
123
|
+
for (const candidateRoot of roots) {
|
|
124
|
+
if (candidateRoot === root)
|
|
125
|
+
continue;
|
|
126
|
+
const health = (0, index_state_footer_1.formatIndexStateFooter)(deps.getIndexState(candidateRoot), {
|
|
127
|
+
agent: true,
|
|
128
|
+
});
|
|
129
|
+
if (health) {
|
|
130
|
+
(_f = response.warnings) !== null && _f !== void 0 ? _f : (response.warnings = []);
|
|
131
|
+
response.warnings.push(`${candidateRoot}: ${health}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
124
134
|
// --skeleton support: fetch per-file skeletons inline so the CLI doesn't
|
|
125
135
|
// have to open its own VectorDB. getStoredSkeleton is a single LIMIT-1
|
|
126
136
|
// lookup; cheap enough to call for the top N distinct paths.
|
|
@@ -131,7 +141,7 @@ function handleDaemonSearch(deps, payload, signal) {
|
|
|
131
141
|
const seen = new Set();
|
|
132
142
|
const skeletons = {};
|
|
133
143
|
for (const chunk of result.data) {
|
|
134
|
-
const p = (
|
|
144
|
+
const p = (_g = chunk.path) !== null && _g !== void 0 ? _g : (_d = chunk.metadata) === null || _d === void 0 ? void 0 : _d.path;
|
|
135
145
|
if (!p || seen.has(p))
|
|
136
146
|
continue;
|
|
137
147
|
seen.add(p);
|
|
@@ -142,7 +152,7 @@ function handleDaemonSearch(deps, payload, signal) {
|
|
|
142
152
|
if (sk)
|
|
143
153
|
skeletons[p] = sk;
|
|
144
154
|
}
|
|
145
|
-
catch (
|
|
155
|
+
catch (_h) {
|
|
146
156
|
// best-effort — drop the entry, keep the search result
|
|
147
157
|
}
|
|
148
158
|
}
|
|
@@ -157,7 +167,7 @@ function handleDaemonSearch(deps, payload, signal) {
|
|
|
157
167
|
const builder = new GraphBuilder(vectorDb, root);
|
|
158
168
|
response.graph = yield builder.buildGraphMultiHop(payload.query, 1);
|
|
159
169
|
}
|
|
160
|
-
catch (
|
|
170
|
+
catch (_j) {
|
|
161
171
|
// best-effort — drop graph, keep results
|
|
162
172
|
}
|
|
163
173
|
}
|
|
@@ -104,6 +104,10 @@ class WatcherManager {
|
|
|
104
104
|
this.pollRecoveryTimers = new Map();
|
|
105
105
|
this.recoveryTimeouts = new Map();
|
|
106
106
|
this.lastOverflowMs = new Map();
|
|
107
|
+
this.lastWatcherErrorLogMs = new Map();
|
|
108
|
+
this.overflowCounts = new Map();
|
|
109
|
+
this.reconciledAt = new Map();
|
|
110
|
+
this.catchupDurations = new Map();
|
|
107
111
|
this.lastCatchupEndMs = new Map();
|
|
108
112
|
this.deferredCatchups = new Map();
|
|
109
113
|
this.catchups = new Map();
|
|
@@ -111,6 +115,24 @@ class WatcherManager {
|
|
|
111
115
|
this.terminalFailures = new Map();
|
|
112
116
|
this.watchLifecycles = new Map();
|
|
113
117
|
}
|
|
118
|
+
health(root) {
|
|
119
|
+
var _a;
|
|
120
|
+
var _b, _c;
|
|
121
|
+
return {
|
|
122
|
+
failedFiles: (_b = (_a = this.terminalFailures.get(root)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0,
|
|
123
|
+
degraded: this.isRootDegraded(root),
|
|
124
|
+
watcherMode: this.pollIntervals.has(root)
|
|
125
|
+
? "polling"
|
|
126
|
+
: this.pendingOps.has(`recover:${root}`) ||
|
|
127
|
+
this.deferredCatchups.has(root)
|
|
128
|
+
? "recovering"
|
|
129
|
+
: "native",
|
|
130
|
+
catchupRunning: this.catchups.has(root),
|
|
131
|
+
lastReconciledAt: this.reconciledAt.get(root),
|
|
132
|
+
overflowCount: (_c = this.overflowCounts.get(root)) !== null && _c !== void 0 ? _c : 0,
|
|
133
|
+
catchupMs: this.catchupDurations.get(root),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
114
136
|
watchProject(root_1) {
|
|
115
137
|
return __awaiter(this, arguments, void 0, function* (root, options = {}) {
|
|
116
138
|
var _a, _b;
|
|
@@ -268,14 +290,20 @@ class WatcherManager {
|
|
|
268
290
|
this.deps.subscriptions.delete(root);
|
|
269
291
|
}
|
|
270
292
|
const sub = yield watcher.subscribe(root, (err, events) => {
|
|
271
|
-
var _a;
|
|
293
|
+
var _a, _b;
|
|
272
294
|
if (err) {
|
|
273
|
-
|
|
295
|
+
this.overflowCounts.set(root, ((_a = this.overflowCounts.get(root)) !== null && _a !== void 0 ? _a : 0) + 1);
|
|
296
|
+
const now = Date.now();
|
|
297
|
+
if (!this.lastWatcherErrorLogMs.has(root) ||
|
|
298
|
+
now - this.lastWatcherErrorLogMs.get(root) >= 60000) {
|
|
299
|
+
this.lastWatcherErrorLogMs.set(root, now);
|
|
300
|
+
console.error(`[daemon:${name}] Watcher error (total=${this.overflowCounts.get(root)}):`, err);
|
|
301
|
+
}
|
|
274
302
|
this.recoverWatcher(root, processor);
|
|
275
303
|
return;
|
|
276
304
|
}
|
|
277
305
|
// Only reset fail counter after sustained health (5min since last overflow)
|
|
278
|
-
const lastOverflow = (
|
|
306
|
+
const lastOverflow = (_b = this.lastOverflowMs.get(root)) !== null && _b !== void 0 ? _b : 0;
|
|
279
307
|
if (Date.now() - lastOverflow > 5 * 60 * 1000) {
|
|
280
308
|
this.watcherFailCount.delete(root);
|
|
281
309
|
}
|
|
@@ -490,6 +518,7 @@ class WatcherManager {
|
|
|
490
518
|
var _a, e_1, _b, _c;
|
|
491
519
|
var _d;
|
|
492
520
|
var _e;
|
|
521
|
+
const scanStart = Date.now();
|
|
493
522
|
const { isFileCached } = yield Promise.resolve().then(() => __importStar(require("../utils/cache-check")));
|
|
494
523
|
const metaCache = this.deps.getMetaCache();
|
|
495
524
|
const vectorDb = this.deps.getVectorDb();
|
|
@@ -641,7 +670,11 @@ class WatcherManager {
|
|
|
641
670
|
parts.push(`${purged} deleted`);
|
|
642
671
|
console.log(`[daemon:${path.basename(root)}] Catchup: ${parts.join(", ")} file(s) while offline`);
|
|
643
672
|
}
|
|
644
|
-
|
|
673
|
+
const ended = Date.now();
|
|
674
|
+
this.lastCatchupEndMs.set(root, ended);
|
|
675
|
+
this.catchupDurations.set(root, ended - scanStart);
|
|
676
|
+
if (complete)
|
|
677
|
+
this.reconciledAt.set(root, ended);
|
|
645
678
|
return complete;
|
|
646
679
|
});
|
|
647
680
|
}
|
|
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.GraphBuilder = void 0;
|
|
13
13
|
const languages_1 = require("../core/languages");
|
|
14
|
+
const arrow_1 = require("../utils/arrow");
|
|
14
15
|
const filter_builder_1 = require("../utils/filter-builder");
|
|
15
16
|
const query_timeout_1 = require("../utils/query-timeout");
|
|
16
17
|
const callsites_1 = require("./callsites");
|
|
@@ -118,19 +119,43 @@ class GraphBuilder {
|
|
|
118
119
|
*/
|
|
119
120
|
getCallees(symbol) {
|
|
120
121
|
return __awaiter(this, void 0, void 0, function* () {
|
|
122
|
+
var _a;
|
|
121
123
|
const table = yield this.db.ensureTable();
|
|
122
124
|
const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
|
|
123
125
|
// Find the definition of the symbol
|
|
124
126
|
const rows = yield table
|
|
125
127
|
.query()
|
|
126
|
-
.select([
|
|
128
|
+
.select([
|
|
129
|
+
"path",
|
|
130
|
+
"referenced_symbols",
|
|
131
|
+
"member_referenced_symbols",
|
|
132
|
+
"content",
|
|
133
|
+
])
|
|
127
134
|
.where(this.scopeWhere(`array_contains(defined_symbols, '${escaped}')`))
|
|
128
135
|
.limit(1)
|
|
129
136
|
.toArray();
|
|
130
137
|
if (rows.length === 0)
|
|
131
138
|
return [];
|
|
132
139
|
const record = rows[0];
|
|
133
|
-
|
|
140
|
+
const memberNames = (0, arrow_1.toArr)(record.member_referenced_symbols);
|
|
141
|
+
const content = String((_a = record.content) !== null && _a !== void 0 ? _a : "");
|
|
142
|
+
let localDefinitions = [];
|
|
143
|
+
if (record.path && memberNames.some(callsites_1.isBuiltinCallee)) {
|
|
144
|
+
const localRows = yield table
|
|
145
|
+
.query()
|
|
146
|
+
.select(["defined_symbols"])
|
|
147
|
+
.where(this.scopeWhere(`path = '${(0, filter_builder_1.escapeSqlString)(String(record.path))}'`))
|
|
148
|
+
.toArray();
|
|
149
|
+
localDefinitions = localRows.flatMap((row) => (0, arrow_1.toArr)(row.defined_symbols));
|
|
150
|
+
}
|
|
151
|
+
return (0, arrow_1.toArr)(record.referenced_symbols).filter((name) => {
|
|
152
|
+
if (!(0, callsites_1.isBuiltinCallee)(name) ||
|
|
153
|
+
!memberNames.includes(name) ||
|
|
154
|
+
localDefinitions.includes(name))
|
|
155
|
+
return true;
|
|
156
|
+
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
157
|
+
return new RegExp(`(?<![\\w.$])${escapedName}\\s*\\(`).test(content);
|
|
158
|
+
});
|
|
134
159
|
});
|
|
135
160
|
}
|
|
136
161
|
/**
|
|
@@ -138,7 +163,8 @@ class GraphBuilder {
|
|
|
138
163
|
*/
|
|
139
164
|
buildGraph(symbol) {
|
|
140
165
|
return __awaiter(this, void 0, void 0, function* () {
|
|
141
|
-
var _a;
|
|
166
|
+
var _a, _b;
|
|
167
|
+
var _c, _d;
|
|
142
168
|
const table = yield this.db.ensureTable();
|
|
143
169
|
const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
|
|
144
170
|
// 1. Get Center (Definition)
|
|
@@ -149,6 +175,8 @@ class GraphBuilder {
|
|
|
149
175
|
"start_line",
|
|
150
176
|
"defined_symbols",
|
|
151
177
|
"referenced_symbols",
|
|
178
|
+
"member_referenced_symbols",
|
|
179
|
+
"content",
|
|
152
180
|
"role",
|
|
153
181
|
"parent_symbol",
|
|
154
182
|
"complexity",
|
|
@@ -187,6 +215,26 @@ class GraphBuilder {
|
|
|
187
215
|
])
|
|
188
216
|
.limit(25)
|
|
189
217
|
.toArray();
|
|
218
|
+
const memberNames = (0, arrow_1.toArr)((_a = centerRows[0]) === null || _a === void 0 ? void 0 : _a.member_referenced_symbols);
|
|
219
|
+
const content = String((_c = (_b = centerRows[0]) === null || _b === void 0 ? void 0 : _b.content) !== null && _c !== void 0 ? _c : "");
|
|
220
|
+
const bareCall = new RegExp(`(?<![\\w.$])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`).test(content);
|
|
221
|
+
const selfDefinition = rows.find((r) => { var _a; return String((_a = r.path) !== null && _a !== void 0 ? _a : "") === centerFile; });
|
|
222
|
+
if ((0, callsites_1.isBuiltinCallee)(name) &&
|
|
223
|
+
memberNames.includes(name) &&
|
|
224
|
+
!bareCall &&
|
|
225
|
+
!selfDefinition) {
|
|
226
|
+
calleeNodes.push({
|
|
227
|
+
symbol: name,
|
|
228
|
+
file: "",
|
|
229
|
+
line: 0,
|
|
230
|
+
role: "",
|
|
231
|
+
calls: [],
|
|
232
|
+
calledBy: [],
|
|
233
|
+
resolution: "ambiguous-member",
|
|
234
|
+
confidence: "INFERRED",
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
190
238
|
if (rows.length > 0) {
|
|
191
239
|
// Prefer-self-file, then same-language-family. Only fall back to the
|
|
192
240
|
// first row when no candidate shares the center's file or family.
|
|
@@ -195,7 +243,7 @@ class GraphBuilder {
|
|
|
195
243
|
? rows.find((r) => { var _a; return (0, languages_1.languageFamilyForPath)(String((_a = r.path) !== null && _a !== void 0 ? _a : "")) ===
|
|
196
244
|
centerFamily; })
|
|
197
245
|
: undefined;
|
|
198
|
-
calleeNodes.push(this.mapRowToNode(((
|
|
246
|
+
calleeNodes.push(this.mapRowToNode(((_d = selfRow !== null && selfRow !== void 0 ? selfRow : familyRow) !== null && _d !== void 0 ? _d : rows[0]), name, "center"));
|
|
199
247
|
}
|
|
200
248
|
else if (!(0, callsites_1.isBuiltinCallee)(name)) {
|
|
201
249
|
// Unresolved + a known builtin (.map/.get/forEach) → suppress the
|
|
@@ -151,7 +151,7 @@ function formatImpactRollupHuman(rollup, opts) {
|
|
|
151
151
|
}
|
|
152
152
|
else {
|
|
153
153
|
for (const test of rollup.topTests) {
|
|
154
|
-
lines.push(` ${relativeToProject(opts.projectRoot, test.file)}:${test.line + 1} (${(0, test_hits_1.hopLabelHuman)(test.hops)}${(0, test_hits_1.formatViaHuman)(test.via)})`);
|
|
154
|
+
lines.push(` ${relativeToProject(opts.projectRoot, test.file)}:${test.line + 1} (${(0, test_hits_1.hopLabelHuman)(test.hops, test.evidence)}${(0, test_hits_1.formatViaHuman)(test.via)})`);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -191,7 +191,7 @@ function formatImpactRollupAgent(rollup, opts) {
|
|
|
191
191
|
lines.push([
|
|
192
192
|
"test",
|
|
193
193
|
`${relativeToProject(opts.projectRoot, test.file)}:${test.line + 1}`,
|
|
194
|
-
(0, test_hits_1.hopLabelAgent)(test.hops),
|
|
194
|
+
(0, test_hits_1.hopLabelAgent)(test.hops, test.evidence),
|
|
195
195
|
(0, test_hits_1.formatViaAgent)(test.via).trim(),
|
|
196
196
|
]
|
|
197
197
|
.filter(Boolean)
|
package/dist/lib/graph/impact.js
CHANGED
|
@@ -157,10 +157,20 @@ function findTests(symbols_1, vectorDb_1, projectRoot_1) {
|
|
|
157
157
|
const expanded = yield expandFileSymbols(symbols, vectorDb, projectRoot, excludePrefixes);
|
|
158
158
|
const families = yield resolveSymbolFamilies(expanded, vectorDb, projectRoot, excludePrefixes, symbolFamilies);
|
|
159
159
|
for (const symbol of expanded) {
|
|
160
|
-
|
|
160
|
+
const candidates = new Map();
|
|
161
|
+
yield walkCallers(symbol, graphBuilder, candidates, 0, depth, new Set(), (_a = families.get(symbol)) !== null && _a !== void 0 ? _a : null);
|
|
162
|
+
for (const [key, hit] of candidates) {
|
|
163
|
+
if (!symbols.includes(symbol))
|
|
164
|
+
hit.evidence = "file-candidate";
|
|
165
|
+
const previous = testHits.get(key);
|
|
166
|
+
if (!previous ||
|
|
167
|
+
(previous.evidence && !hit.evidence) ||
|
|
168
|
+
(previous.evidence === hit.evidence && hit.hops < previous.hops))
|
|
169
|
+
testHits.set(key, hit);
|
|
170
|
+
}
|
|
161
171
|
}
|
|
162
|
-
if (testHits.
|
|
163
|
-
const importFiles = yield findImportFallbackTests(
|
|
172
|
+
if (![...testHits.values()].some((hit) => !hit.evidence)) {
|
|
173
|
+
const importFiles = yield findImportFallbackTests(symbols, vectorDb, projectRoot, excludePrefixes, families);
|
|
164
174
|
for (const file of importFiles) {
|
|
165
175
|
testHits.set(`${file}:(referenced)`, {
|
|
166
176
|
file,
|
|
@@ -170,17 +180,19 @@ function findTests(symbols_1, vectorDb_1, projectRoot_1) {
|
|
|
170
180
|
});
|
|
171
181
|
}
|
|
172
182
|
}
|
|
173
|
-
return [...testHits.values()].sort((a, b) => a.
|
|
183
|
+
return [...testHits.values()].sort((a, b) => Number(!!a.evidence) - Number(!!b.evidence) ||
|
|
184
|
+
a.hops - b.hops ||
|
|
185
|
+
a.file.localeCompare(b.file));
|
|
174
186
|
});
|
|
175
187
|
}
|
|
176
|
-
function findImportFallbackTests(
|
|
188
|
+
function findImportFallbackTests(originalSymbols, vectorDb, projectRoot, excludePrefixes, symbolFamilies) {
|
|
177
189
|
return __awaiter(this, void 0, void 0, function* () {
|
|
178
190
|
var _a;
|
|
179
191
|
const files = new Set();
|
|
180
192
|
// Signal 1: referenced_symbols match (precise; works when the chunker
|
|
181
|
-
// captured call references in test bodies).
|
|
182
|
-
//
|
|
183
|
-
const dependents = yield findDependents(
|
|
193
|
+
// captured call references in test bodies). Co-defined symbols are already
|
|
194
|
+
// exposed as file candidates and must not become target coverage here.
|
|
195
|
+
const dependents = yield findDependents(originalSymbols, vectorDb, projectRoot, undefined, 50, excludePrefixes, symbolFamilies);
|
|
184
196
|
for (const d of dependents) {
|
|
185
197
|
if (isTestPath(d.file))
|
|
186
198
|
files.add(d.file);
|
|
@@ -9,20 +9,22 @@ function groupTestHitsByFile(hits) {
|
|
|
9
9
|
const byFile = new Map();
|
|
10
10
|
// Sort by hops so the first hit per file carries the best line/hops and
|
|
11
11
|
// `via` lists closest callers first ((referenced) fallback hits sort last).
|
|
12
|
-
const ordered = [...hits].sort((a, b) => (a.
|
|
13
|
-
(
|
|
12
|
+
const ordered = [...hits].sort((a, b) => Number(!!a.evidence) - Number(!!b.evidence) ||
|
|
13
|
+
(a.hops === -1 ? Number.MAX_SAFE_INTEGER : a.hops) -
|
|
14
|
+
(b.hops === -1 ? Number.MAX_SAFE_INTEGER : b.hops));
|
|
14
15
|
for (const h of ordered) {
|
|
15
16
|
let g = byFile.get(h.file);
|
|
16
17
|
if (!g) {
|
|
17
|
-
g = { file: h.file, line: h.line, hops: h.hops, via: [] };
|
|
18
|
+
g = Object.assign({ file: h.file, line: h.line, hops: h.hops, via: [] }, (h.evidence ? { evidence: h.evidence } : {}));
|
|
18
19
|
byFile.set(h.file, g);
|
|
19
20
|
}
|
|
20
21
|
if (h.symbol && h.symbol !== "(referenced)" && !g.via.includes(h.symbol)) {
|
|
21
22
|
g.via.push(h.symbol);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
|
-
return [...byFile.values()].sort((a, b) => (a.
|
|
25
|
-
(
|
|
25
|
+
return [...byFile.values()].sort((a, b) => Number(!!a.evidence) - Number(!!b.evidence) ||
|
|
26
|
+
(a.hops === -1 ? Number.MAX_SAFE_INTEGER : a.hops) -
|
|
27
|
+
(b.hops === -1 ? Number.MAX_SAFE_INTEGER : b.hops) ||
|
|
26
28
|
a.file.localeCompare(b.file));
|
|
27
29
|
}
|
|
28
30
|
const MAX_VIA = 3;
|
|
@@ -42,10 +44,14 @@ function formatViaHuman(via) {
|
|
|
42
44
|
const more = via.length > MAX_VIA ? ` (+${via.length - MAX_VIA} more)` : "";
|
|
43
45
|
return `, via ${shown}${more}`;
|
|
44
46
|
}
|
|
45
|
-
function hopLabelAgent(hops) {
|
|
47
|
+
function hopLabelAgent(hops, evidence) {
|
|
48
|
+
if (evidence)
|
|
49
|
+
return "file-candidate";
|
|
46
50
|
return hops === -1 ? "via-import" : hops === 0 ? "direct" : `${hops}-hop`;
|
|
47
51
|
}
|
|
48
|
-
function hopLabelHuman(hops) {
|
|
52
|
+
function hopLabelHuman(hops, evidence) {
|
|
53
|
+
if (evidence)
|
|
54
|
+
return "same-file candidate";
|
|
49
55
|
return hops === -1
|
|
50
56
|
? "via import"
|
|
51
57
|
: hops === 0
|
|
@@ -50,6 +50,7 @@ const vector_db_1 = require("../store/vector-db");
|
|
|
50
50
|
const cache_check_1 = require("../utils/cache-check");
|
|
51
51
|
const file_utils_1 = require("../utils/file-utils");
|
|
52
52
|
const logger_1 = require("../utils/logger");
|
|
53
|
+
const embedding_error_1 = require("../workers/embedding-error");
|
|
53
54
|
const pool_1 = require("../workers/pool");
|
|
54
55
|
const cache_coherence_1 = require("./cache-coherence");
|
|
55
56
|
const file_policy_1 = require("./file-policy");
|
|
@@ -81,6 +82,8 @@ class ProjectBatchProcessor {
|
|
|
81
82
|
this.closed = false;
|
|
82
83
|
this.currentBatchAc = null;
|
|
83
84
|
this.lastCorruptionLogMs = 0;
|
|
85
|
+
this.backendFailures = 0;
|
|
86
|
+
this.backendRetryAt = 0;
|
|
84
87
|
this.policyChangedDuringBatch = false;
|
|
85
88
|
/** Rolling window of recent batch outcomes — see `progress`. */
|
|
86
89
|
this.recentBatches = [];
|
|
@@ -201,8 +204,11 @@ class ProjectBatchProcessor {
|
|
|
201
204
|
});
|
|
202
205
|
}
|
|
203
206
|
scheduleBatch(delayMs = DEBOUNCE_MS) {
|
|
207
|
+
delayMs = Math.max(delayMs, this.backendRetryAt - Date.now());
|
|
204
208
|
const dueMs = Date.now() + Math.max(0, delayMs);
|
|
205
|
-
if (this.debounceTimer &&
|
|
209
|
+
if (this.debounceTimer &&
|
|
210
|
+
this.debounceDueMs <= dueMs &&
|
|
211
|
+
this.debounceDueMs >= this.backendRetryAt)
|
|
206
212
|
return;
|
|
207
213
|
if (this.debounceTimer)
|
|
208
214
|
clearTimeout(this.debounceTimer);
|
|
@@ -234,7 +240,10 @@ class ProjectBatchProcessor {
|
|
|
234
240
|
const run = (this.runOperation
|
|
235
241
|
? this.runOperation(execute)
|
|
236
242
|
: execute(new AbortController().signal)).catch((err) => {
|
|
237
|
-
|
|
243
|
+
if ((err === null || err === void 0 ? void 0 : err.name) === "AbortError")
|
|
244
|
+
(0, logger_1.log)(this.wtag, "Batch cancelled during shutdown");
|
|
245
|
+
else
|
|
246
|
+
console.error(`[${this.wtag}] Batch processing failed:`, err);
|
|
238
247
|
});
|
|
239
248
|
this.activeBatch = run;
|
|
240
249
|
void run.finally(() => {
|
|
@@ -303,6 +312,7 @@ class ProjectBatchProcessor {
|
|
|
303
312
|
const metaDeletes = [];
|
|
304
313
|
const completed = new Set();
|
|
305
314
|
const retryFailures = new Set();
|
|
315
|
+
let backendUnavailable = false;
|
|
306
316
|
const requeuePath = (absPath, event, failed) => {
|
|
307
317
|
var _a;
|
|
308
318
|
var _b;
|
|
@@ -434,6 +444,11 @@ class ProjectBatchProcessor {
|
|
|
434
444
|
if (batchAc.signal.aborted)
|
|
435
445
|
return;
|
|
436
446
|
const code = err === null || err === void 0 ? void 0 : err.code;
|
|
447
|
+
if ((0, embedding_error_1.isEmbeddingBackendUnavailable)(err)) {
|
|
448
|
+
backendUnavailable = true;
|
|
449
|
+
stopDispatch = true;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
437
452
|
if (code === "ENOENT") {
|
|
438
453
|
deletes.push(absPath);
|
|
439
454
|
metaDeletes.push(absPath);
|
|
@@ -479,6 +494,16 @@ class ProjectBatchProcessor {
|
|
|
479
494
|
yield schedule(() => processOne(absPath, event));
|
|
480
495
|
}
|
|
481
496
|
yield Promise.allSettled(activeTasks);
|
|
497
|
+
if (backendUnavailable) {
|
|
498
|
+
this.backendFailures++;
|
|
499
|
+
const backoffMs = Math.min(60000, 5000 * Math.pow(2, Math.min(this.backendFailures - 1, 4)));
|
|
500
|
+
this.backendRetryAt = Date.now() + backoffMs;
|
|
501
|
+
(0, logger_1.log)(this.wtag, `Embedding backend unavailable — preserving file retry budgets; retry in ${backoffMs / 1000}s`);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
this.backendFailures = 0;
|
|
505
|
+
this.backendRetryAt = 0;
|
|
506
|
+
}
|
|
482
507
|
const pureDeleteCandidates = new Set(metaDeletes);
|
|
483
508
|
const authoritativeMetaDeletes = new Set();
|
|
484
509
|
for (const absPath of pureDeleteCandidates) {
|
|
@@ -56,15 +56,24 @@ for (const lang of languages_1.LANGUAGES) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
const downloadFile = (url, dest) => __awaiter(void 0, void 0, void 0, function* () {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
60
|
+
try {
|
|
61
|
+
const response = yield fetch(url, {
|
|
62
|
+
headers: { "User-Agent": "gmax" },
|
|
63
|
+
signal: AbortSignal.timeout(30000),
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok)
|
|
66
|
+
throw new Error(`Failed to download ${url}: HTTP ${response.status}`);
|
|
67
|
+
const arrayBuffer = yield response.arrayBuffer();
|
|
68
|
+
fs.writeFileSync(dest, Buffer.from(arrayBuffer));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (attempt === 3)
|
|
73
|
+
throw error;
|
|
74
|
+
yield new Promise((resolve) => setTimeout(resolve, attempt * 500));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
68
77
|
});
|
|
69
78
|
function ensureGrammars() {
|
|
70
79
|
return __awaiter(this, arguments, void 0, function* (log = console.log, options) {
|
|
@@ -73,6 +82,7 @@ function ensureGrammars() {
|
|
|
73
82
|
fs.mkdirSync(exports.GRAMMARS_DIR, { recursive: true });
|
|
74
83
|
}
|
|
75
84
|
const silent = (_a = options === null || options === void 0 ? void 0 : options.silent) !== null && _a !== void 0 ? _a : false;
|
|
85
|
+
const failed = [];
|
|
76
86
|
for (const [lang, url] of Object.entries(GRAMMAR_URLS)) {
|
|
77
87
|
const dest = path.join(exports.GRAMMARS_DIR, `tree-sitter-${lang}.wasm`);
|
|
78
88
|
if (fs.existsSync(dest)) {
|
|
@@ -99,6 +109,7 @@ function ensureGrammars() {
|
|
|
99
109
|
}
|
|
100
110
|
}
|
|
101
111
|
catch (err) {
|
|
112
|
+
failed.push(lang);
|
|
102
113
|
if (log === console.log) {
|
|
103
114
|
console.log("Failed");
|
|
104
115
|
}
|
|
@@ -106,5 +117,7 @@ function ensureGrammars() {
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
}
|
|
120
|
+
if ((options === null || options === void 0 ? void 0 : options.strict) && failed.length)
|
|
121
|
+
throw new Error(`Required grammars could not be downloaded: ${failed.join(", ")}`);
|
|
109
122
|
});
|
|
110
123
|
}
|
|
@@ -191,7 +191,7 @@ function formatTrace(graph, options) {
|
|
|
191
191
|
lines.push(` ${style.cyan("↓")} ${style.green(callee.symbol)} ${style.dim(`(${callee.file}:${callee.line})`)}`);
|
|
192
192
|
}
|
|
193
193
|
else {
|
|
194
|
-
lines.push(` ${style.cyan("↓")} ${callee.symbol} ${style.dim("(not indexed)")}`);
|
|
194
|
+
lines.push(` ${style.cyan("↓")} ${callee.symbol} ${style.dim(callee.resolution === "ambiguous-member" ? "(receiver unresolved)" : "(not indexed)")}`);
|
|
195
195
|
}
|
|
196
196
|
});
|
|
197
197
|
}
|
|
@@ -14,17 +14,37 @@ exports.formatIndexStateFooter = formatIndexStateFooter;
|
|
|
14
14
|
* while results may actually be incomplete.
|
|
15
15
|
*/
|
|
16
16
|
function formatIndexStateFooter(state, opts) {
|
|
17
|
-
if (!
|
|
17
|
+
if (!state)
|
|
18
18
|
return null;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const issues = [];
|
|
20
|
+
if (state.failedFiles)
|
|
21
|
+
issues.push(`${state.failedFiles} files failed`);
|
|
22
|
+
if (state.degraded && !state.failedFiles)
|
|
23
|
+
issues.push("reconciliation incomplete");
|
|
24
|
+
if (state.watcherMode === "polling")
|
|
25
|
+
issues.push("polling; changes may lag up to 5min");
|
|
26
|
+
if (state.watcherMode === "recovering")
|
|
27
|
+
issues.push("watcher recovering");
|
|
28
|
+
if (state.catchupRunning)
|
|
29
|
+
issues.push("reconciling filesystem changes");
|
|
30
|
+
if (issues.length) {
|
|
31
|
+
const next = state.failedFiles || state.degraded
|
|
32
|
+
? "inspect gmax watch status; retry after recovery"
|
|
33
|
+
: "retry after reconciliation";
|
|
34
|
+
return opts.agent
|
|
35
|
+
? `[index: ${issues.join(" · ")} · results may be incomplete · ${next}]`
|
|
36
|
+
: `Index: ${issues.join("; ")} — results may be incomplete; ${next}.`;
|
|
37
|
+
}
|
|
38
|
+
if (!state.indexing)
|
|
39
|
+
return null;
|
|
40
|
+
// Cache-hit samples suggest verification, but do not prove the remaining
|
|
41
|
+
// files are unchanged. Keep that distinction without claiming full coverage.
|
|
22
42
|
if (state.verifying) {
|
|
23
43
|
const n = state.pendingFiles;
|
|
24
44
|
if (opts.agent) {
|
|
25
|
-
return `[index: verifying ~${n}
|
|
45
|
+
return `[index: verifying ~${n} files · coverage not yet verified]`;
|
|
26
46
|
}
|
|
27
|
-
return `Index verifying ${n}
|
|
47
|
+
return `Index verifying ${n} file${n === 1 ? "" : "s"} — coverage is not yet verified.`;
|
|
28
48
|
}
|
|
29
49
|
const count = state.pendingFiles > 0 ? `~${state.pendingFiles} files pending` : null;
|
|
30
50
|
if (opts.agent) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildResourceSnapshot = buildResourceSnapshot;
|
|
4
|
+
function buildResourceSnapshot(counters, memory = process.memoryUsage(), at = Date.now()) {
|
|
5
|
+
const mb = (bytes) => Math.round(bytes / 1024 / 1024);
|
|
6
|
+
return {
|
|
7
|
+
at,
|
|
8
|
+
reason: counters.reason,
|
|
9
|
+
footprintMb: counters.footprintMb,
|
|
10
|
+
rssMb: mb(memory.rss),
|
|
11
|
+
heapUsedMb: mb(memory.heapUsed),
|
|
12
|
+
externalMb: mb(memory.external),
|
|
13
|
+
arrayBuffersMb: mb(memory.arrayBuffers),
|
|
14
|
+
lanceCacheMb: counters.lanceCacheBytes === null ? null : mb(counters.lanceCacheBytes),
|
|
15
|
+
workers: counters.workers,
|
|
16
|
+
pendingFiles: counters.pendingFiles,
|
|
17
|
+
operations: counters.operations,
|
|
18
|
+
maintenance: counters.maintenance,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -15,6 +15,7 @@ exports.fetchTestsForFooter = fetchTestsForFooter;
|
|
|
15
15
|
exports.renderTestsFooterAgent = renderTestsFooterAgent;
|
|
16
16
|
exports.renderTestsFooterHuman = renderTestsFooterHuman;
|
|
17
17
|
const impact_1 = require("../graph/impact");
|
|
18
|
+
const test_hits_1 = require("../graph/test-hits");
|
|
18
19
|
/**
|
|
19
20
|
* The footer is a courtesy line under a body the user asked for; it must never
|
|
20
21
|
* make the command feel slow. Past this budget the lookup is abandoned and the
|
|
@@ -57,24 +58,10 @@ function fetchTestsForFooter(symbol, vectorDb, pathPrefix, excludePrefixes) {
|
|
|
57
58
|
function relPath(p, projectRoot) {
|
|
58
59
|
return p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
|
|
59
60
|
}
|
|
60
|
-
function hopLabelAgent(hops) {
|
|
61
|
-
if (hops === -1)
|
|
62
|
-
return "via-import";
|
|
63
|
-
if (hops === 0)
|
|
64
|
-
return "direct";
|
|
65
|
-
return `${hops}-hop`;
|
|
66
|
-
}
|
|
67
|
-
function hopLabelHuman(hops) {
|
|
68
|
-
if (hops === -1)
|
|
69
|
-
return "via import";
|
|
70
|
-
if (hops === 0)
|
|
71
|
-
return "direct";
|
|
72
|
-
return `${hops} hop${hops > 1 ? "s" : ""}`;
|
|
73
|
-
}
|
|
74
61
|
function renderTestsFooterAgent(tests, projectRoot) {
|
|
75
62
|
const lines = [];
|
|
76
63
|
for (const t of tests.slice(0, MAX_SHOWN)) {
|
|
77
|
-
lines.push(`t: ${relPath(t.file, projectRoot)}:${t.line + 1}\t${t.symbol}\t${hopLabelAgent(t.hops)}`);
|
|
64
|
+
lines.push(`t: ${relPath(t.file, projectRoot)}:${t.line + 1}\t${t.symbol}\t${(0, test_hits_1.hopLabelAgent)(t.hops, t.evidence)}`);
|
|
78
65
|
}
|
|
79
66
|
if (tests.length > MAX_SHOWN) {
|
|
80
67
|
lines.push(`t: ... ${tests.length - MAX_SHOWN} more`);
|
|
@@ -86,7 +73,12 @@ function renderTestsFooterHuman(tests, projectRoot) {
|
|
|
86
73
|
lines.push("");
|
|
87
74
|
lines.push(`tests (${tests.length}):`);
|
|
88
75
|
for (const t of tests.slice(0, MAX_SHOWN)) {
|
|
89
|
-
|
|
76
|
+
const label = t.evidence
|
|
77
|
+
? "same-file candidate"
|
|
78
|
+
: t.hops === -1
|
|
79
|
+
? "via import"
|
|
80
|
+
: (0, test_hits_1.hopLabelAgent)(t.hops);
|
|
81
|
+
lines.push(` ${t.symbol.padEnd(25)} ${dim(`${relPath(t.file, projectRoot)}:${t.line + 1}`)} ${dim(`(${label})`)}`);
|
|
90
82
|
}
|
|
91
83
|
if (tests.length > MAX_SHOWN) {
|
|
92
84
|
lines.push(dim(` ... and ${tests.length - MAX_SHOWN} more`));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EmbeddingBackendUnavailableError = void 0;
|
|
4
|
+
exports.isEmbeddingBackendUnavailable = isEmbeddingBackendUnavailable;
|
|
5
|
+
/** Shared backend outages are retryable; malformed inputs/protocols are not. */
|
|
6
|
+
class EmbeddingBackendUnavailableError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = "EMBED_BACKEND_UNAVAILABLE";
|
|
10
|
+
this.name = "EmbeddingBackendUnavailableError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.EmbeddingBackendUnavailableError = EmbeddingBackendUnavailableError;
|
|
14
|
+
function isEmbeddingBackendUnavailable(error) {
|
|
15
|
+
return ((error === null || error === void 0 ? void 0 : error.code) === "EMBED_BACKEND_UNAVAILABLE");
|
|
16
|
+
}
|