grepmax 0.23.0 → 0.25.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.
Files changed (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +25 -6
  3. package/dist/commands/add.js +38 -31
  4. package/dist/commands/config.js +16 -15
  5. package/dist/commands/context.js +38 -13
  6. package/dist/commands/doctor.js +76 -83
  7. package/dist/commands/extract.js +12 -1
  8. package/dist/commands/impact.js +33 -1
  9. package/dist/commands/index.js +23 -24
  10. package/dist/commands/list.js +23 -14
  11. package/dist/commands/mcp.js +494 -162
  12. package/dist/commands/peek.js +16 -5
  13. package/dist/commands/repair.js +54 -120
  14. package/dist/commands/search-output.js +30 -9
  15. package/dist/commands/search-run.js +75 -47
  16. package/dist/commands/search-skeletons.js +28 -18
  17. package/dist/commands/search.js +45 -49
  18. package/dist/commands/serve.js +415 -373
  19. package/dist/commands/setup.js +2 -2
  20. package/dist/commands/similar.js +5 -5
  21. package/dist/commands/skeleton.js +67 -41
  22. package/dist/commands/status.js +5 -2
  23. package/dist/commands/summarize.js +6 -0
  24. package/dist/commands/surprises.js +150 -0
  25. package/dist/commands/watch.js +102 -38
  26. package/dist/config.js +3 -0
  27. package/dist/eval-surprising-connections.js +191 -0
  28. package/dist/index.js +2 -0
  29. package/dist/lib/analysis/surprising-connections.js +600 -0
  30. package/dist/lib/daemon/daemon.js +1101 -379
  31. package/dist/lib/daemon/ipc-handler.js +122 -13
  32. package/dist/lib/daemon/mlx-server-manager.js +268 -125
  33. package/dist/lib/daemon/process-manager.js +7 -4
  34. package/dist/lib/daemon/search-handler.js +23 -9
  35. package/dist/lib/daemon/watcher-manager.js +353 -110
  36. package/dist/lib/graph/impact-rollup.js +202 -0
  37. package/dist/lib/graph/impact.js +15 -1
  38. package/dist/lib/help/agent-cheatsheet.js +1 -0
  39. package/dist/lib/index/batch-processor.js +231 -67
  40. package/dist/lib/index/embedding-generation.js +109 -0
  41. package/dist/lib/index/embedding-status.js +23 -0
  42. package/dist/lib/index/file-policy.js +273 -0
  43. package/dist/lib/index/ignore-patterns.js +4 -0
  44. package/dist/lib/index/index-config.js +18 -4
  45. package/dist/lib/index/syncer.js +256 -79
  46. package/dist/lib/index/walker.js +66 -86
  47. package/dist/lib/index/watcher-batch.js +9 -0
  48. package/dist/lib/index/watcher.js +129 -3
  49. package/dist/lib/llm/server.js +6 -0
  50. package/dist/lib/search/searcher.js +30 -19
  51. package/dist/lib/skeleton/skeletonizer.js +118 -0
  52. package/dist/lib/skeleton/summary-formatter.js +8 -1
  53. package/dist/lib/store/store-lease.js +473 -0
  54. package/dist/lib/store/vector-db.js +302 -57
  55. package/dist/lib/utils/blocked-roots.js +63 -0
  56. package/dist/lib/utils/cross-project.js +7 -3
  57. package/dist/lib/utils/daemon-client.js +24 -1
  58. package/dist/lib/utils/daemon-launcher.js +38 -13
  59. package/dist/lib/utils/doctor-status.js +76 -0
  60. package/dist/lib/utils/file-utils.js +74 -4
  61. package/dist/lib/utils/keyed-mutex.js +101 -0
  62. package/dist/lib/utils/logger.js +57 -1
  63. package/dist/lib/utils/mlx-hf-cache.js +114 -0
  64. package/dist/lib/utils/operation-coordinator.js +146 -0
  65. package/dist/lib/utils/path-containment.js +106 -0
  66. package/dist/lib/utils/process.js +44 -0
  67. package/dist/lib/utils/project-registry.js +351 -3
  68. package/dist/lib/utils/scope-filter.js +3 -9
  69. package/dist/lib/utils/stale-hint.js +12 -19
  70. package/dist/lib/utils/watcher-launcher.js +5 -1
  71. package/dist/lib/utils/watcher-store.js +2 -2
  72. package/dist/lib/workers/colbert-math.js +15 -12
  73. package/dist/lib/workers/embeddings/colbert.js +3 -2
  74. package/dist/lib/workers/embeddings/granite.js +4 -3
  75. package/dist/lib/workers/embeddings/mlx-client.js +59 -16
  76. package/dist/lib/workers/orchestrator.js +39 -8
  77. package/dist/lib/workers/pool.js +150 -83
  78. package/dist/lib/workers/process-child.js +11 -2
  79. package/dist/lib/workers/serialized-handler.js +10 -0
  80. package/dist/lib/workers/worker.js +13 -4
  81. package/mlx-embed-server/server.py +21 -3
  82. package/mlx-embed-server/summarizer.py +8 -0
  83. package/package.json +6 -3
  84. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  85. package/plugins/grepmax/hooks/start.js +3 -170
@@ -142,10 +142,10 @@ exports.setup = new commander_1.Command("setup")
142
142
  const tierChanged = existingConfig.modelTier !== modelTier;
143
143
  const modeChanged = existingConfig.embedMode !== embedMode;
144
144
  if (tierChanged) {
145
- p.log.warn(`Model tier changed (${(_d = existingConfig.vectorDim) !== null && _d !== void 0 ? _d : 384}d → ${selectedTier.vectorDim}d). Existing indexes will be rebuilt on next use.`);
145
+ p.log.warn(`Model tier changed (${(_d = existingConfig.vectorDim) !== null && _d !== void 0 ? _d : 384}d → ${selectedTier.vectorDim}d). Existing indexes are preserved and search is blocked until you run 'gmax repair --rebuild'.`);
146
146
  }
147
147
  else if (modeChanged) {
148
- p.log.warn("Embedding mode changed. Run `gmax serve` to apply the new settings.");
148
+ p.log.warn("Embedding mode changed. Restart the gmax daemon to apply the new settings.");
149
149
  }
150
150
  }
151
151
  // Step 8: Install plugins for detected clients
@@ -43,13 +43,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.similar = void 0;
46
- const path = __importStar(require("node:path"));
47
46
  const commander_1 = require("commander");
48
47
  const vector_db_1 = require("../lib/store/vector-db");
49
48
  const agent_errors_1 = require("../lib/utils/agent-errors");
50
49
  const arrow_1 = require("../lib/utils/arrow");
51
50
  const exit_1 = require("../lib/utils/exit");
52
51
  const filter_builder_1 = require("../lib/utils/filter-builder");
52
+ const path_containment_1 = require("../lib/utils/path-containment");
53
53
  const project_registry_1 = require("../lib/utils/project-registry");
54
54
  const project_root_1 = require("../lib/utils/project-root");
55
55
  const stale_hint_1 = require("../lib/utils/stale-hint");
@@ -81,9 +81,9 @@ exports.similar = new commander_1.Command("similar")
81
81
  // Look up the source chunk's vector
82
82
  let sourceRows;
83
83
  if (isFile) {
84
- const absPath = target.startsWith("/")
85
- ? target
86
- : path.resolve(projectRoot, target);
84
+ const absPath = (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
85
+ verifyExistingTarget: true,
86
+ });
87
87
  sourceRows = yield table
88
88
  .query()
89
89
  .select(["vector", "path", "defined_symbols", "start_line"])
@@ -95,7 +95,7 @@ exports.similar = new commander_1.Command("similar")
95
95
  sourceRows = yield table
96
96
  .query()
97
97
  .select(["vector", "path", "defined_symbols", "start_line"])
98
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(target)}')`)
98
+ .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(target)}') AND ${(0, filter_builder_1.pathStartsWith)(`${projectRoot}/`)}`)
99
99
  .limit(1)
100
100
  .toArray();
101
101
  }
@@ -52,8 +52,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
53
  exports.skeleton = void 0;
54
54
  const fs = __importStar(require("node:fs"));
55
- const path = __importStar(require("node:path"));
56
55
  const commander_1 = require("commander");
56
+ const config_1 = require("../config");
57
57
  const sync_helpers_1 = require("../lib/index/sync-helpers");
58
58
  const syncer_1 = require("../lib/index/syncer");
59
59
  const searcher_1 = require("../lib/search/searcher");
@@ -62,20 +62,11 @@ const retriever_1 = require("../lib/skeleton/retriever");
62
62
  const skeletonizer_1 = require("../lib/skeleton/skeletonizer");
63
63
  const vector_db_1 = require("../lib/store/vector-db");
64
64
  const exit_1 = require("../lib/utils/exit");
65
+ const file_utils_1 = require("../lib/utils/file-utils");
66
+ const filter_builder_1 = require("../lib/utils/filter-builder");
67
+ const path_containment_1 = require("../lib/utils/path-containment");
65
68
  const project_registry_1 = require("../lib/utils/project-registry");
66
69
  const project_root_1 = require("../lib/utils/project-root");
67
- /**
68
- * Resolve a relative path across all indexed projects.
69
- * Returns the first match found, or null.
70
- */
71
- function resolveAcrossProjects(relativePath) {
72
- for (const project of (0, project_registry_1.listProjects)()) {
73
- const candidate = path.join(project.root, relativePath);
74
- if (fs.existsSync(candidate))
75
- return candidate;
76
- }
77
- return null;
78
- }
79
70
  /**
80
71
  * Check if target looks like a file path.
81
72
  */
@@ -94,12 +85,16 @@ function isSymbolLike(target) {
94
85
  /**
95
86
  * Find a file by symbol name in the index.
96
87
  */
97
- function findFileBySymbol(symbol, db) {
88
+ function findFileBySymbol(symbol, db, projectRoot) {
98
89
  return __awaiter(this, void 0, void 0, function* () {
99
90
  try {
100
91
  const table = yield db.ensureTable();
101
92
  // Search for files that define this symbol
102
- const results = yield table.search(symbol).limit(10).toArray();
93
+ const results = yield table
94
+ .search(symbol)
95
+ .where((0, filter_builder_1.pathStartsWith)(`${projectRoot}/`))
96
+ .limit(10)
97
+ .toArray();
103
98
  // Find a result where this symbol is defined
104
99
  for (const result of results) {
105
100
  const defined = result.defined_symbols;
@@ -144,8 +139,26 @@ Examples:
144
139
  // Sync if requested
145
140
  if (options.sync) {
146
141
  const { spinner, onProgress } = (0, sync_helpers_1.createIndexingSpinner)(projectRoot, "Syncing...", { verbose: false });
147
- yield (0, syncer_1.initialSync)({ projectRoot, onProgress });
148
- spinner.succeed("Sync complete");
142
+ const result = yield (0, syncer_1.initialSync)({ projectRoot, onProgress });
143
+ if (result.degraded) {
144
+ spinner.warn(`Sync incomplete: ${result.scanErrors} scan error(s), ${result.failedFiles} file failure(s)`);
145
+ }
146
+ else {
147
+ const prefix = projectRoot.endsWith("/")
148
+ ? projectRoot
149
+ : `${projectRoot}/`;
150
+ const chunkCount = yield vectorDb.countRowsForPath(prefix);
151
+ (0, project_registry_1.stampProjectFullSync)({
152
+ root: projectRoot,
153
+ generation: result.generation,
154
+ embedMode: result.embedMode,
155
+ chunkCount,
156
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
157
+ expectedFingerprint: result.registryExpectation.embeddingFingerprint,
158
+ expectedRebuildId: result.registryExpectation.rebuildId,
159
+ });
160
+ spinner.succeed("Sync complete");
161
+ }
149
162
  }
150
163
  // Initialize skeletonizer
151
164
  const skeletonizer = new skeletonizer_1.Skeletonizer();
@@ -154,7 +167,9 @@ Examples:
154
167
  includeSummary: !options.noSummary,
155
168
  };
156
169
  // Determine mode based on target
157
- const resolvedTarget = path.resolve(target);
170
+ const resolvedTarget = (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
171
+ verifyExistingTarget: true,
172
+ });
158
173
  // Directory mode is unsupported. Auto-picking files from a directory
159
174
  // was confusingly magical (and on '.' it fell through to the resolver
160
175
  // path and skeletonized .gitignore). Refuse and point at the file form.
@@ -176,12 +191,14 @@ Examples:
176
191
  .map((t) => t.trim())
177
192
  .filter(Boolean);
178
193
  for (const t of targets) {
179
- const filePath = path.resolve(t);
194
+ const filePath = (0, path_containment_1.resolveContainedPath)(projectRoot, t, {
195
+ verifyExistingTarget: true,
196
+ });
180
197
  if (!fs.existsSync(filePath)) {
181
198
  console.error(`Not found: ${t}`);
182
199
  continue;
183
200
  }
184
- const content = fs.readFileSync(filePath, "utf-8");
201
+ const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, filePath);
185
202
  const result = yield skeletonizer.skeletonizeFile(filePath, content, skeletonOpts);
186
203
  outputResult(result, options);
187
204
  }
@@ -189,16 +206,13 @@ Examples:
189
206
  }
190
207
  if (isFilePath(target)) {
191
208
  // === FILE MODE ===
192
- let filePath = path.resolve(target);
209
+ const filePath = (0, path_containment_1.resolveContainedPath)(projectRoot, target, {
210
+ verifyExistingTarget: true,
211
+ });
193
212
  if (!fs.existsSync(filePath)) {
194
- // Try resolving across indexed projects
195
- const found = resolveAcrossProjects(target);
196
- if (!found) {
197
- console.error(`File not found: ${filePath}`);
198
- process.exitCode = 1;
199
- return;
200
- }
201
- filePath = found;
213
+ console.error(`File not found: ${filePath}`);
214
+ process.exitCode = 1;
215
+ return;
202
216
  }
203
217
  if (vectorDb) {
204
218
  // Use absolute path for DB lookup (centralized index stores absolute paths)
@@ -212,13 +226,13 @@ Examples:
212
226
  return;
213
227
  }
214
228
  }
215
- const content = fs.readFileSync(filePath, "utf-8");
229
+ const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, filePath);
216
230
  const result = yield skeletonizer.skeletonizeFile(filePath, content, skeletonOpts);
217
231
  outputResult(result, options);
218
232
  }
219
233
  else if (isSymbolLike(target) && !target.includes(" ")) {
220
234
  // === SYMBOL MODE ===
221
- const filePath = yield findFileBySymbol(target, vectorDb);
235
+ const filePath = yield findFileBySymbol(target, vectorDb, projectRoot);
222
236
  if (!filePath) {
223
237
  console.error(`Symbol not found in index: ${target}`);
224
238
  console.error("Try running 'gmax index' first or use a search query.");
@@ -226,9 +240,9 @@ Examples:
226
240
  return;
227
241
  }
228
242
  // filePath from DB is absolute (centralized index)
229
- const absolutePath = path.isAbsolute(filePath)
230
- ? filePath
231
- : path.resolve(projectRoot, filePath);
243
+ const absolutePath = (0, path_containment_1.resolveContainedPath)(projectRoot, filePath, {
244
+ verifyExistingTarget: true,
245
+ });
232
246
  if (!fs.existsSync(absolutePath)) {
233
247
  console.error(`File not found: ${absolutePath}`);
234
248
  process.exitCode = 1;
@@ -243,7 +257,7 @@ Examples:
243
257
  }, options);
244
258
  return;
245
259
  }
246
- const content = fs.readFileSync(absolutePath, "utf-8");
260
+ const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, absolutePath);
247
261
  const result = yield skeletonizer.skeletonizeFile(absolutePath, content, skeletonOpts);
248
262
  outputResult(result, options);
249
263
  }
@@ -251,7 +265,7 @@ Examples:
251
265
  // === QUERY MODE ===
252
266
  const searcher = new searcher_1.Searcher(vectorDb);
253
267
  const limit = Math.min(Number.parseInt(options.limit, 10) || 3, 10);
254
- const searchResults = yield searcher.search(target, limit);
268
+ const searchResults = yield searcher.search(target, limit, {}, {}, `${projectRoot}/`);
255
269
  if (!searchResults.data || searchResults.data.length === 0) {
256
270
  console.error(`No results found for: ${target}`);
257
271
  process.exitCode = 1;
@@ -273,9 +287,21 @@ Examples:
273
287
  const results = [];
274
288
  for (const filePath of filePaths) {
275
289
  // Paths from search results are absolute (centralized index)
276
- const absolutePath = path.isAbsolute(filePath)
277
- ? filePath
278
- : path.resolve(projectRoot, filePath);
290
+ let absolutePath;
291
+ try {
292
+ absolutePath = (0, path_containment_1.resolveContainedPath)(projectRoot, filePath, {
293
+ verifyExistingTarget: true,
294
+ });
295
+ }
296
+ catch (_c) {
297
+ results.push({
298
+ file: filePath,
299
+ skeleton: `// File outside selected project: ${filePath}`,
300
+ tokens: 0,
301
+ error: "File outside selected project",
302
+ });
303
+ continue;
304
+ }
279
305
  if (!fs.existsSync(absolutePath)) {
280
306
  results.push({
281
307
  file: filePath,
@@ -295,7 +321,7 @@ Examples:
295
321
  });
296
322
  continue;
297
323
  }
298
- const content = fs.readFileSync(absolutePath, "utf-8");
324
+ const content = (0, file_utils_1.readContainedTextFileSync)(projectRoot, absolutePath);
299
325
  const result = yield skeletonizer.skeletonizeFile(absolutePath, content, skeletonOpts);
300
326
  results.push({
301
327
  file: filePath,
@@ -326,7 +352,7 @@ Examples:
326
352
  try {
327
353
  yield vectorDb.close();
328
354
  }
329
- catch (_c) {
355
+ catch (_d) {
330
356
  // Ignore close errors
331
357
  }
332
358
  }
@@ -46,6 +46,7 @@ exports.status = void 0;
46
46
  const os = __importStar(require("node:os"));
47
47
  const commander_1 = require("commander");
48
48
  const config_1 = require("../config");
49
+ const embedding_status_1 = require("../lib/index/embedding-status");
49
50
  const index_config_1 = require("../lib/index/index-config");
50
51
  const exit_1 = require("../lib/utils/exit");
51
52
  const filter_builder_1 = require("../lib/utils/filter-builder");
@@ -157,7 +158,8 @@ Examples:
157
158
  st = "idle";
158
159
  const isCurrent = project.root === currentRoot;
159
160
  const count = (_b = chunkCounts.get(project.root)) !== null && _b !== void 0 ? _b : project.chunkCount;
160
- console.log(`${project.name}\t${formatChunks(count)}\t${formatAge(project.lastIndexed)}\t${st}${isCurrent ? "\tcurrent" : ""}`);
161
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig);
162
+ console.log(`${project.name}\t${formatChunks(count)}\t${formatAge(project.lastIndexed)}\t${st}\tembedding=${identity.state}${isCurrent ? "\tcurrent" : ""}`);
161
163
  }
162
164
  yield (0, exit_1.gracefulExit)();
163
165
  return;
@@ -191,10 +193,11 @@ Examples:
191
193
  const chunks = `${formatChunks(count)} chunks`;
192
194
  // Age column
193
195
  const age = formatAge(project.lastIndexed);
196
+ const embedding = (0, embedding_status_1.projectEmbeddingStatus)(project, globalConfig).state;
194
197
  // Current marker
195
198
  const marker = isCurrent ? style.cyan(" ←") : "";
196
199
  const name = project.name.padEnd(nameWidth);
197
- console.log(` ${name} ${chunks.padEnd(12)} ${age.padEnd(10)} ${statusStr}${marker}`);
200
+ console.log(` ${name} ${chunks.padEnd(12)} ${age.padEnd(10)} ${statusStr} embedding:${embedding}${marker}`);
198
201
  }
199
202
  if (currentRoot) {
200
203
  console.log(`\n${style.dim("Current")}: ${shortenPath(currentRoot)}`);
@@ -50,6 +50,12 @@ const syncer_1 = require("../lib/index/syncer");
50
50
  const vector_db_1 = require("../lib/store/vector-db");
51
51
  const exit_1 = require("../lib/utils/exit");
52
52
  const project_root_1 = require("../lib/utils/project-root");
53
+ // ⛔ HARD STOP FOR AI AGENTS: The summarizer is decommissioned on purpose.
54
+ // `generateSummaries` (src/lib/index/syncer.ts) is a no-op stub, so this
55
+ // command intentionally does nothing. DO NOT re-enable it, and DO NOT start
56
+ // the summarizer server (mlx-embed-server/summarizer.py, port 8101) or load its
57
+ // ~16GB model to "measure"/"benchmark" — doing so has crashed the machine.
58
+ // Stop and get the user's explicit authorization first. See CLAUDE.md.
53
59
  exports.summarize = new commander_1.Command("summarize")
54
60
  .description("Generate LLM summaries for indexed chunks without re-indexing")
55
61
  .option("-p, --path <dir>", "Only summarize chunks under this directory")
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.surprises = void 0;
13
+ const commander_1 = require("commander");
14
+ const surprising_connections_1 = require("../lib/analysis/surprising-connections");
15
+ const vector_db_1 = require("../lib/store/vector-db");
16
+ const exit_1 = require("../lib/utils/exit");
17
+ const project_registry_1 = require("../lib/utils/project-registry");
18
+ const project_root_1 = require("../lib/utils/project-root");
19
+ const stale_hint_1 = require("../lib/utils/stale-hint");
20
+ const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
21
+ const style = {
22
+ bold: (s) => (useColors ? `\x1b[1m${s}\x1b[22m` : s),
23
+ dim: (s) => (useColors ? `\x1b[2m${s}\x1b[39m` : s),
24
+ cyan: (s) => (useColors ? `\x1b[36m${s}\x1b[39m` : s),
25
+ };
26
+ function parseIntOption(value, fallback, max = 10000) {
27
+ const parsed = Number.parseInt(String(value !== null && value !== void 0 ? value : ""), 10);
28
+ if (!Number.isFinite(parsed) || parsed <= 0)
29
+ return fallback;
30
+ return Math.min(parsed, max);
31
+ }
32
+ function parseFloatOption(value, fallback) {
33
+ const parsed = Number.parseFloat(String(value !== null && value !== void 0 ? value : ""));
34
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
35
+ }
36
+ function formatAgent(result, top) {
37
+ const lines = [];
38
+ const { summary, findings } = result;
39
+ lines.push(`summary\t${summary.sampledAnchors}\t${summary.codeRows}\t${summary.acceptedPairs}\t${summary.acceptedFilePairs}`);
40
+ for (const finding of findings.slice(0, top)) {
41
+ const pair = finding.representative;
42
+ lines.push([
43
+ "surprise",
44
+ finding.score.toFixed(3),
45
+ finding.maxSimilarity.toFixed(3),
46
+ String(finding.pairCount),
47
+ finding.fileA,
48
+ finding.fileB,
49
+ (0, surprising_connections_1.lineLabel)(pair.source),
50
+ (0, surprising_connections_1.lineLabel)(pair.target),
51
+ finding.reasons.join(","),
52
+ `buckets=${(0, surprising_connections_1.findingBucketLabel)(finding, summary.options.dirDepth)}`,
53
+ `top_sims=${finding.topSimilarities.join(",")}`,
54
+ `penalties=${(0, surprising_connections_1.formatPenaltySummary)(pair.scoreParts)}`,
55
+ `next=${(0, surprising_connections_1.skeletonHint)(finding)}`,
56
+ ].join("\t"));
57
+ }
58
+ return lines.join("\n");
59
+ }
60
+ function formatHuman(result, top) {
61
+ const { summary, findings } = result;
62
+ const out = [];
63
+ out.push(`${style.bold("Surprising connections")} ${style.dim("(experimental, embedding-similar but graph-disconnected file pairs)")}`);
64
+ out.push(style.dim(` sampled ${summary.sampledAnchors}/${summary.codeRows} code chunks; accepted ${summary.acceptedPairs} chunk pairs across ${summary.acceptedFilePairs} file pairs`));
65
+ out.push(style.dim(` score p50/p90/max ${summary.actionabilityScore.p50}/${summary.actionabilityScore.p90}/${summary.actionabilityScore.max}; graph file edges filtered ${summary.filters.graphEdge}`));
66
+ out.push("");
67
+ if (findings.length === 0) {
68
+ out.push(style.dim(" none"));
69
+ return out.join("\n");
70
+ }
71
+ for (const finding of findings.slice(0, top)) {
72
+ const pair = finding.representative;
73
+ out.push(` ${style.cyan(`score=${finding.score.toFixed(3)}`)} sim=${finding.maxSimilarity.toFixed(3)} pairs=${finding.pairCount} ${finding.fileA}`);
74
+ out.push(` <-> ${finding.fileB}`);
75
+ out.push(` best: ${(0, surprising_connections_1.lineLabel)(pair.source)} <-> ${(0, surprising_connections_1.lineLabel)(pair.target)}`);
76
+ out.push(` detail: no static file edge; buckets=${(0, surprising_connections_1.findingBucketLabel)(finding, summary.options.dirDepth)}; top_sims=${finding.topSimilarities.join(",")}`);
77
+ out.push(` reasons: ${finding.reasons.join(", ") || "none"}; penalties=${(0, surprising_connections_1.formatPenaltySummary)(pair.scoreParts)}`);
78
+ const examples = (0, surprising_connections_1.findingExamples)(finding, 2);
79
+ if (examples.length > 1) {
80
+ out.push(` examples: ${examples
81
+ .map((example) => `${(0, surprising_connections_1.lineLabel)(example.source)} <-> ${(0, surprising_connections_1.lineLabel)(example.target)}`)
82
+ .join("; ")}`);
83
+ }
84
+ out.push(` next: ${(0, surprising_connections_1.skeletonHint)(finding)}`);
85
+ }
86
+ return out.join("\n");
87
+ }
88
+ exports.surprises = new commander_1.Command("surprises")
89
+ .description("Experimental: find embedding-similar cross-directory file pairs not already connected by the static graph")
90
+ .option("--experimental", "Required acknowledgement for this experimental signal", false)
91
+ .option("--root <dir>", "Project root directory")
92
+ .option("--sample <n>", "How many indexed code chunks to sample", String(surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.sample))
93
+ .option("--neighbors <n>", "Nearest neighbors to inspect per sampled chunk", String(surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.neighbors))
94
+ .option("--top <n>", "How many grouped findings to show", "20")
95
+ .option("--dir-depth <n>", "Directory bucket depth considered unsurprising", String(surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.dirDepth))
96
+ .option("--min-sim <n>", "Minimum similarity 0-1", String(surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.minSimilarity))
97
+ .option("--max-rows <n>", `Maximum indexed rows to scan (capped at ${surprising_connections_1.MAX_SURPRISE_ROWS})`, String(surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.maxRows))
98
+ .option("--include-tests", "Include test files", false)
99
+ .option("--include-eval", "Include eval/experiment/script files", false)
100
+ .option("--in <subpath>", "Restrict to a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
101
+ .option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
102
+ .option("--agent", "Compact TSV output for AI agents", false)
103
+ .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
104
+ var _a;
105
+ if (!opts.experimental) {
106
+ const msg = "`gmax surprises` is experimental; rerun with --experimental.";
107
+ console.error(opts.agent ? `error\texperimental_required\t${msg}` : msg);
108
+ process.exitCode = 1;
109
+ return;
110
+ }
111
+ let vectorDb = null;
112
+ try {
113
+ const root = (0, project_registry_1.resolveRootOrExit)(opts.root);
114
+ if (root === null)
115
+ return;
116
+ const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
117
+ (0, stale_hint_1.maybeWarnStaleChunker)(projectRoot, { agent: opts.agent });
118
+ (0, stale_hint_1.maybeWarnStaleEmbedding)(projectRoot, { agent: opts.agent });
119
+ const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
120
+ vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
121
+ const table = yield vectorDb.ensureTable();
122
+ const top = parseIntOption(opts.top, 20, 100);
123
+ const result = yield (0, surprising_connections_1.analyzeSurprisingConnections)(table, projectRoot, {
124
+ sample: parseIntOption(opts.sample, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.sample),
125
+ neighbors: parseIntOption(opts.neighbors, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.neighbors, 200),
126
+ dirDepth: parseIntOption(opts.dirDepth, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.dirDepth, 20),
127
+ minSimilarity: parseFloatOption(opts.minSim, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.minSimilarity),
128
+ maxRows: parseIntOption(opts.maxRows, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.maxRows, surprising_connections_1.MAX_SURPRISE_ROWS),
129
+ includeTests: Boolean(opts.includeTests),
130
+ includeEval: Boolean(opts.includeEval),
131
+ in: opts.in,
132
+ exclude: opts.exclude,
133
+ });
134
+ console.log(opts.agent ? formatAgent(result, top) : formatHuman(result, top));
135
+ }
136
+ catch (error) {
137
+ const msg = error instanceof Error ? error.message : "Unknown error";
138
+ console.error("Surprises failed:", msg);
139
+ process.exitCode = 1;
140
+ }
141
+ finally {
142
+ if (vectorDb) {
143
+ try {
144
+ yield vectorDb.close();
145
+ }
146
+ catch (_b) { }
147
+ }
148
+ yield (0, exit_1.gracefulExit)();
149
+ }
150
+ }));