token-goat 2.8.4 → 2.9.1

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.
@@ -23,10 +23,13 @@ import {
23
23
  extractPdfMeta,
24
24
  extractPdfOutline,
25
25
  extractPdfText,
26
+ fenceUntrusted,
26
27
  fenceUntrustedContent,
27
28
  formatCsvProfile,
28
29
  formatCsvTable,
30
+ getEmbeddingCoverage,
29
31
  getFileEntry,
32
+ getTrackedFiles,
30
33
  indexFileSync,
31
34
  isAvailable,
32
35
  isBlobStale,
@@ -49,7 +52,7 @@ import {
49
52
  queryRefs,
50
53
  queryRefsByContext,
51
54
  querySymbols,
52
- scanForInjectionPatterns,
55
+ scanAndRecord,
53
56
  searchEvidenceSemantically,
54
57
  searchSemantic,
55
58
  searchSymbolsFts,
@@ -64,7 +67,7 @@ import {
64
67
  walkProject,
65
68
  yamlLineClosesQuote,
66
69
  yamlOpenQuoteAfter
67
- } from "./token-goat-chunk-4OTIB7SB.mjs";
70
+ } from "./token-goat-chunk-FDXINYK4.mjs";
68
71
  import {
69
72
  Database,
70
73
  PER_FILE_COUNTERFACTUAL_CEILING,
@@ -118,36 +121,11 @@ import {
118
121
  unsupportedLanguageName,
119
122
  windowsCmdQuoteArg,
120
123
  withExtension
121
- } from "./token-goat-chunk-E76UNTVK.mjs";
124
+ } from "./token-goat-chunk-4ZDRTNGV.mjs";
122
125
  import {
123
126
  registerReset
124
127
  } from "./token-goat-chunk-AO2QD2AG.mjs";
125
128
 
126
- // src/repomap.ts
127
- import * as fs from "fs";
128
- import * as path from "path";
129
- function getTrackedFiles(cwd = process.cwd()) {
130
- try {
131
- let dir = cwd;
132
- let onlyFile = "";
133
- try {
134
- if (fs.statSync(cwd).isFile()) {
135
- dir = path.dirname(cwd);
136
- onlyFile = path.basename(cwd);
137
- }
138
- } catch {
139
- }
140
- const args = onlyFile === "" ? ["ls-files"] : ["ls-files", "--error-unmatch", "--", onlyFile];
141
- const result = runGit(args, { cwd: dir });
142
- if (result.exitCode !== 0 || !result.stdout) {
143
- return [];
144
- }
145
- return result.stdout.split("\n").filter((line) => line.trim().length > 0).map((rel) => path.join(dir, rel));
146
- } catch {
147
- return [];
148
- }
149
- }
150
-
151
129
  // src/section_reader.ts
152
130
  import { readFileSync } from "node:fs";
153
131
  function parseHeadingSpec(spec, headers) {
@@ -475,8 +453,8 @@ import { execFileSync, spawnSync as spawnSync2 } from "node:child_process";
475
453
  import { randomUUID } from "node:crypto";
476
454
 
477
455
  // src/read_commands.ts
478
- import * as fs5 from "node:fs";
479
- import * as path5 from "node:path";
456
+ import * as fs4 from "node:fs";
457
+ import * as path4 from "node:path";
480
458
 
481
459
  // src/json_query.ts
482
460
  function jsonType(value) {
@@ -3442,11 +3420,11 @@ var CHOMPING_STRIP = CHOMPING_MODE.STRIP;
3442
3420
  var CHOMPING_KEEP = CHOMPING_MODE.KEEP;
3443
3421
 
3444
3422
  // src/openapi_query.ts
3445
- import * as path2 from "node:path";
3423
+ import * as path from "node:path";
3446
3424
  var HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
3447
3425
  function parseOpenApiSpec(text, filePath) {
3448
3426
  text = stripBom(text);
3449
- const ext = path2.extname(filePath).toLowerCase();
3427
+ const ext = path.extname(filePath).toLowerCase();
3450
3428
  if (ext === ".yaml" || ext === ".yml") return load(text);
3451
3429
  if (ext === ".json") return JSON.parse(text);
3452
3430
  try {
@@ -3800,24 +3778,24 @@ ${c.body}`).join("\n\n");
3800
3778
  }
3801
3779
 
3802
3780
  // src/sqlite_query.ts
3803
- import * as fs2 from "node:fs";
3781
+ import * as fs from "node:fs";
3804
3782
  var SQLITE_QUERY_ROW_CAP = 5e3;
3805
3783
  var SQLITE_MAGIC = Buffer.from([83, 81, 76, 105, 116, 101, 32, 102, 111, 114, 109, 97, 116, 32, 51, 0]);
3806
3784
  function readMagicBytes(filePath) {
3807
3785
  let fd;
3808
3786
  try {
3809
- fd = fs2.openSync(filePath, "r");
3787
+ fd = fs.openSync(filePath, "r");
3810
3788
  } catch {
3811
3789
  return null;
3812
3790
  }
3813
3791
  try {
3814
3792
  const buf = Buffer.alloc(SQLITE_MAGIC.length);
3815
- const bytesRead = fs2.readSync(fd, buf, 0, buf.length, 0);
3793
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
3816
3794
  return bytesRead === buf.length ? buf : null;
3817
3795
  } catch {
3818
3796
  return null;
3819
3797
  } finally {
3820
- fs2.closeSync(fd);
3798
+ fs.closeSync(fd);
3821
3799
  }
3822
3800
  }
3823
3801
  function isSqliteFile(filePath) {
@@ -3825,7 +3803,7 @@ function isSqliteFile(filePath) {
3825
3803
  return magic !== null && magic.equals(SQLITE_MAGIC);
3826
3804
  }
3827
3805
  function openReadonlySqlite(filePath) {
3828
- if (!fs2.existsSync(filePath)) {
3806
+ if (!fs.existsSync(filePath)) {
3829
3807
  throw new Error(`file not found: ${filePath}`);
3830
3808
  }
3831
3809
  if (!isSqliteFile(filePath)) {
@@ -4528,8 +4506,8 @@ function formatConflictSummaries(summaries) {
4528
4506
 
4529
4507
  // src/screenshot.ts
4530
4508
  import dns from "node:dns/promises";
4531
- import fs3 from "node:fs";
4532
- import path3 from "node:path";
4509
+ import fs2 from "node:fs";
4510
+ import path2 from "node:path";
4533
4511
  var loadPuppeteer = createLazyModuleLoader(
4534
4512
  async () => await import("puppeteer-core"),
4535
4513
  "screenshot disabled (puppeteer-core unavailable)"
@@ -4538,7 +4516,7 @@ var PLAYWRIGHT_CHROME_SUBDIRS = ["chrome-win64", "chrome-win"];
4538
4516
  function findPlaywrightChromium(msPlaywrightDir) {
4539
4517
  let entries;
4540
4518
  try {
4541
- entries = fs3.readdirSync(msPlaywrightDir);
4519
+ entries = fs2.readdirSync(msPlaywrightDir);
4542
4520
  } catch {
4543
4521
  return [];
4544
4522
  }
@@ -4546,7 +4524,7 @@ function findPlaywrightChromium(msPlaywrightDir) {
4546
4524
  const candidates = [];
4547
4525
  for (const m of versioned) {
4548
4526
  for (const sub of PLAYWRIGHT_CHROME_SUBDIRS) {
4549
- candidates.push(path3.join(msPlaywrightDir, m[0], sub, "chrome.exe"));
4527
+ candidates.push(path2.join(msPlaywrightDir, m[0], sub, "chrome.exe"));
4550
4528
  }
4551
4529
  }
4552
4530
  return candidates;
@@ -4557,33 +4535,33 @@ function platformCandidatePaths() {
4557
4535
  const programFiles = process.env["PROGRAMFILES"];
4558
4536
  const programFilesX86 = process.env["PROGRAMFILES(X86)"];
4559
4537
  const localAppData = process.env["LOCALAPPDATA"];
4560
- if (programFiles) candidates2.push(path3.join(programFiles, "Google", "Chrome", "Application", "chrome.exe"));
4561
- if (programFilesX86) candidates2.push(path3.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"));
4538
+ if (programFiles) candidates2.push(path2.join(programFiles, "Google", "Chrome", "Application", "chrome.exe"));
4539
+ if (programFilesX86) candidates2.push(path2.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"));
4562
4540
  if (localAppData) {
4563
- candidates2.push(path3.join(localAppData, "Google", "Chrome", "Application", "chrome.exe"));
4564
- candidates2.push(...findPlaywrightChromium(path3.join(localAppData, "ms-playwright")));
4541
+ candidates2.push(path2.join(localAppData, "Google", "Chrome", "Application", "chrome.exe"));
4542
+ candidates2.push(...findPlaywrightChromium(path2.join(localAppData, "ms-playwright")));
4565
4543
  }
4566
4544
  return candidates2;
4567
4545
  }
4568
4546
  if (process.platform === "darwin") {
4569
4547
  const home2 = process.env["HOME"];
4570
4548
  const candidates2 = ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
4571
- if (home2) candidates2.push(...findPlaywrightChromium(path3.join(home2, "Library", "Caches", "ms-playwright")));
4549
+ if (home2) candidates2.push(...findPlaywrightChromium(path2.join(home2, "Library", "Caches", "ms-playwright")));
4572
4550
  return candidates2;
4573
4551
  }
4574
4552
  const home = process.env["HOME"];
4575
4553
  const candidates = ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/usr/bin/chromium-browser", "/usr/bin/chromium"];
4576
- if (home) candidates.push(...findPlaywrightChromium(path3.join(home, ".cache", "ms-playwright")));
4554
+ if (home) candidates.push(...findPlaywrightChromium(path2.join(home, ".cache", "ms-playwright")));
4577
4555
  return candidates;
4578
4556
  }
4579
4557
  function resolveBrowserExecutablePath(explicit) {
4580
- if (explicit && fs3.existsSync(explicit)) return explicit;
4558
+ if (explicit && fs2.existsSync(explicit)) return explicit;
4581
4559
  const cfgPath = loadConfig().screenshot.chrome_path;
4582
- if (cfgPath && fs3.existsSync(cfgPath)) return cfgPath;
4560
+ if (cfgPath && fs2.existsSync(cfgPath)) return cfgPath;
4583
4561
  const envPath = process.env["TOKEN_GOAT_CHROME_PATH"];
4584
- if (envPath && fs3.existsSync(envPath)) return envPath;
4562
+ if (envPath && fs2.existsSync(envPath)) return envPath;
4585
4563
  for (const candidate of platformCandidatePaths()) {
4586
- if (fs3.existsSync(candidate)) return candidate;
4564
+ if (fs2.existsSync(candidate)) return candidate;
4587
4565
  }
4588
4566
  return null;
4589
4567
  }
@@ -4890,7 +4868,7 @@ async function takeScreenshot(url, destPath, opts) {
4890
4868
  }
4891
4869
 
4892
4870
  // src/notes.ts
4893
- import * as fs4 from "node:fs";
4871
+ import * as fs3 from "node:fs";
4894
4872
  var WHOLE_FILE_NOTE_SYMBOL = "";
4895
4873
  function toNoteRow(row) {
4896
4874
  return {
@@ -4915,7 +4893,7 @@ function resolveSymbolMatch(filePath, symbolName, dbPath = globalDbPath()) {
4915
4893
  }
4916
4894
  function bodyFromSource(entry) {
4917
4895
  try {
4918
- return fs4.readFileSync(entry.filePath, "utf8").split(/\r?\n/).slice(Math.max(0, entry.lineStart - 1), entry.lineEnd).join("\n");
4896
+ return fs3.readFileSync(entry.filePath, "utf8").split(/\r?\n/).slice(Math.max(0, entry.lineStart - 1), entry.lineEnd).join("\n");
4919
4897
  } catch {
4920
4898
  return "";
4921
4899
  }
@@ -4966,7 +4944,7 @@ function isNoteStale(note, dbPath = globalDbPath()) {
4966
4944
 
4967
4945
  // src/ts_refs.ts
4968
4946
  import { createRequire } from "node:module";
4969
- import * as path4 from "node:path";
4947
+ import * as path3 from "node:path";
4970
4948
  var _require = createRequire(import.meta.url);
4971
4949
  var MAX_CANDIDATE_FILES = 50;
4972
4950
  var _ts = null;
@@ -4997,7 +4975,7 @@ registerReset(() => {
4997
4975
  var TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
4998
4976
  var TS_JS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
4999
4977
  function isTsPath(filePath) {
5000
- return TS_EXTENSIONS.has(path4.extname(filePath).toLowerCase());
4978
+ return TS_EXTENSIONS.has(path3.extname(filePath).toLowerCase());
5001
4979
  }
5002
4980
  function resolveTypedRefs(input) {
5003
4981
  const ts = loadTs();
@@ -5027,7 +5005,7 @@ function resolveTypedRefs(input) {
5027
5005
  if (defSymbol === null) return null;
5028
5006
  const out = [];
5029
5007
  for (const ref of input.candidates) {
5030
- if (!TS_JS_EXTENSIONS.has(path4.extname(ref.filePath).toLowerCase())) {
5008
+ if (!TS_JS_EXTENSIONS.has(path3.extname(ref.filePath).toLowerCase())) {
5031
5009
  out.push(ref);
5032
5010
  continue;
5033
5011
  }
@@ -5053,12 +5031,12 @@ function buildScopedProgram(ts, rootNames, searchFrom) {
5053
5031
  resolveJsonModule: true,
5054
5032
  esModuleInterop: true
5055
5033
  };
5056
- const configPath = ts.findConfigFile(path4.dirname(searchFrom), ts.sys.fileExists, "tsconfig.json");
5034
+ const configPath = ts.findConfigFile(path3.dirname(searchFrom), ts.sys.fileExists, "tsconfig.json");
5057
5035
  if (configPath !== void 0) {
5058
5036
  try {
5059
5037
  const raw = ts.readConfigFile(configPath, ts.sys.readFile);
5060
5038
  if (raw.error === void 0) {
5061
- const parsed = ts.parseJsonConfigFileContent(raw.config, ts.sys, path4.dirname(configPath));
5039
+ const parsed = ts.parseJsonConfigFileContent(raw.config, ts.sys, path3.dirname(configPath));
5062
5040
  Object.assign(options, parsed.options);
5063
5041
  }
5064
5042
  } catch {
@@ -5162,7 +5140,7 @@ var STRUCTURED_MISS_MAX_NODES = 2e4;
5162
5140
  var REFS_TOP_SCAN_LIMIT = 2e4;
5163
5141
  function fileExists(p) {
5164
5142
  try {
5165
- fs5.statSync(p);
5143
+ fs4.statSync(p);
5166
5144
  return true;
5167
5145
  } catch {
5168
5146
  return false;
@@ -5193,30 +5171,30 @@ function withPinnedReads(pins, fn) {
5193
5171
  }
5194
5172
  }
5195
5173
  function readPinnedBytes(p, pinned) {
5196
- const fd = fs5.openSync(p, fs5.constants.O_RDONLY);
5174
+ const fd = fs4.openSync(p, fs4.constants.O_RDONLY);
5197
5175
  try {
5198
- const actual = fileIdentity(fs5.fstatSync(fd, { bigint: true }));
5176
+ const actual = fileIdentity(fs4.fstatSync(fd, { bigint: true }));
5199
5177
  if (actual !== pinned) {
5200
5178
  throw new ConfinementIdentityError(
5201
5179
  `refused: "${p}" changed identity between validation and read (validated ${pinned}, opened ${actual}). The file was replaced or redirected after the confinement check, so the read was not performed.`
5202
5180
  );
5203
5181
  }
5204
- return fs5.readFileSync(fd);
5182
+ return fs4.readFileSync(fd);
5205
5183
  } finally {
5206
- fs5.closeSync(fd);
5184
+ fs4.closeSync(fd);
5207
5185
  }
5208
5186
  }
5209
5187
  function verifyPinnedIdentity(p, pinned) {
5210
- const fd = fs5.openSync(p, fs5.constants.O_RDONLY);
5188
+ const fd = fs4.openSync(p, fs4.constants.O_RDONLY);
5211
5189
  try {
5212
- const actual = fileIdentity(fs5.fstatSync(fd, { bigint: true }));
5190
+ const actual = fileIdentity(fs4.fstatSync(fd, { bigint: true }));
5213
5191
  if (actual !== pinned) {
5214
5192
  throw new ConfinementIdentityError(
5215
5193
  `refused: "${p}" changed identity between validation and read (validated ${pinned}, opened ${actual}). The file was replaced or redirected after the confinement check, so the read was not performed.`
5216
5194
  );
5217
5195
  }
5218
5196
  } finally {
5219
- fs5.closeSync(fd);
5197
+ fs4.closeSync(fd);
5220
5198
  }
5221
5199
  }
5222
5200
  function verifyStillAbsent(p) {
@@ -5234,7 +5212,7 @@ function verifyPin(p, pinned) {
5234
5212
  verifyPinnedIdentity(p, pinned);
5235
5213
  }
5236
5214
  function indexFileSyncPinned(resolvedPath, dbPath) {
5237
- const pinned = activePins?.get(pinKey(path5.resolve(resolvedPath)));
5215
+ const pinned = activePins?.get(pinKey(path4.resolve(resolvedPath)));
5238
5216
  if (pinned === void 0) {
5239
5217
  indexFileSync(resolvedPath, dbPath);
5240
5218
  return;
@@ -5256,21 +5234,21 @@ function indexFileSyncPinned(resolvedPath, dbPath) {
5256
5234
  indexFileSync(resolvedPath, dbPath, bytes);
5257
5235
  }
5258
5236
  function readFileText(p) {
5259
- const pinned = activePins?.get(pinKey(path5.resolve(p)));
5237
+ const pinned = activePins?.get(pinKey(path4.resolve(p)));
5260
5238
  try {
5261
5239
  if (pinned === ABSENT_PIN) {
5262
5240
  verifyStillAbsent(p);
5263
5241
  return null;
5264
5242
  }
5265
5243
  if (pinned !== void 0) return redactIfDotenv(p, decodeSource(readPinnedBytes(p, pinned)));
5266
- return redactIfDotenv(p, decodeSource(fs5.readFileSync(p)));
5244
+ return redactIfDotenv(p, decodeSource(fs4.readFileSync(p)));
5267
5245
  } catch (err) {
5268
5246
  if (err instanceof ConfinementIdentityError) throw err;
5269
5247
  return null;
5270
5248
  }
5271
5249
  }
5272
5250
  function readFileBytes(p) {
5273
- const pinned = activePins?.get(pinKey(path5.resolve(p)));
5251
+ const pinned = activePins?.get(pinKey(path4.resolve(p)));
5274
5252
  try {
5275
5253
  if (pinned === ABSENT_PIN) {
5276
5254
  verifyStillAbsent(p);
@@ -5281,9 +5259,9 @@ function readFileBytes(p) {
5281
5259
  if (bytes.length > MAX_ZIP_INPUT_BYTES) throw new ZipInputTooLargeError(p, bytes.length, MAX_ZIP_INPUT_BYTES);
5282
5260
  return bytes;
5283
5261
  }
5284
- const stat = fs5.statSync(p);
5262
+ const stat = fs4.statSync(p);
5285
5263
  if (stat.size > MAX_ZIP_INPUT_BYTES) throw new ZipInputTooLargeError(p, stat.size, MAX_ZIP_INPUT_BYTES);
5286
- return fs5.readFileSync(p);
5264
+ return fs4.readFileSync(p);
5287
5265
  } catch (err) {
5288
5266
  if (err instanceof ConfinementIdentityError || err instanceof ZipInputTooLargeError) throw err;
5289
5267
  return null;
@@ -5302,7 +5280,7 @@ var STALE_WARNING = "\u26A0 STALE: index is older than the file on disk (worker
5302
5280
  var DELETED_WARNING = "\u26A0 DELETED: this file is no longer on disk \u2014 what follows is what the index last saw of it";
5303
5281
  var DELETED_TAG = "\u26A0 DELETED: file no longer on disk";
5304
5282
  function fileIsGone(absPath) {
5305
- if (!path5.isAbsolute(absPath)) return false;
5283
+ if (!path4.isAbsolute(absPath)) return false;
5306
5284
  return fileIsAbsent(absPath);
5307
5285
  }
5308
5286
  function staleWarning(resolvedPath) {
@@ -5353,15 +5331,12 @@ function guardText(text, command) {
5353
5331
  const cfg = loadConfig();
5354
5332
  return cfg.overflow_guard.enabled ? trimToBudget(text, cfg.overflow_guard.max_tokens, command) : text;
5355
5333
  }
5356
- function fenceGithubTextIfMatched(text) {
5357
- let matches = [];
5358
- try {
5359
- if (loadConfig().injection.enabled) matches = scanForInjectionPatterns(text);
5360
- } catch {
5361
- matches = [];
5362
- }
5334
+ function fenceGithubText(text) {
5335
+ return fenceUntrusted(text, UNTRUSTED_GITHUB_TAG);
5336
+ }
5337
+ function fenceGithubFieldIfMatched(text) {
5338
+ const matches = scanAndRecord(text);
5363
5339
  if (matches.length === 0) return text;
5364
- recordStat("injection_detected", 0, 0, void 0, matches.join(","));
5365
5340
  return fenceUntrustedContent(text, matches, UNTRUSTED_GITHUB_TAG);
5366
5341
  }
5367
5342
  function guardJsonRows(items) {
@@ -5373,12 +5348,18 @@ function sumFileSizes(filePaths) {
5373
5348
  let total = 0;
5374
5349
  for (const fp of new Set(filePaths)) {
5375
5350
  try {
5376
- total += Math.min(fs5.statSync(fp).size, PER_FILE_COUNTERFACTUAL_CEILING);
5351
+ total += Math.min(fs4.statSync(fp).size, PER_FILE_COUNTERFACTUAL_CEILING);
5377
5352
  } catch {
5378
5353
  }
5379
5354
  }
5380
5355
  return total;
5381
5356
  }
5357
+ function refsSearchBaselineBytes(rows) {
5358
+ let total = 0;
5359
+ for (const ref of rows) total += Buffer.byteLength(`${refsDisplayPath(ref.filePath)}:${ref.line}: ${ref.context}
5360
+ `, "utf8");
5361
+ return total;
5362
+ }
5382
5363
  function recordReadStat(kind, fullSourceBytes, emittedText, detail) {
5383
5364
  const emittedBytes = Buffer.byteLength(emittedText, "utf8");
5384
5365
  const bytesSaved = Math.max(1, fullSourceBytes - emittedBytes);
@@ -5551,7 +5532,7 @@ function findStructuredKeyPath(name, filePaths) {
5551
5532
  if (!isYaml && !lower.endsWith(".json")) continue;
5552
5533
  let size;
5553
5534
  try {
5554
- size = fs5.statSync(filePath).size;
5535
+ size = fs4.statSync(filePath).size;
5555
5536
  } catch {
5556
5537
  continue;
5557
5538
  }
@@ -6089,7 +6070,7 @@ ${sub.text}`);
6089
6070
  return { text, code: 1 };
6090
6071
  }
6091
6072
  function resolveAgainstProjectRoot(file, projectRoot) {
6092
- return projectRoot !== void 0 && !path5.isAbsolute(file) ? path5.resolve(projectRoot, file) : file;
6073
+ return projectRoot !== void 0 && !path4.isAbsolute(file) ? path4.resolve(projectRoot, file) : file;
6093
6074
  }
6094
6075
  function literalHeadingExists(filePath, heading) {
6095
6076
  const ordinalMatch = /^([^#\r\n]+)#(\d+)$/.exec(heading);
@@ -6113,7 +6094,7 @@ function runSection(opts) {
6113
6094
  }
6114
6095
  const result = readSection(filePath, heading, readFileText);
6115
6096
  if (result === null) {
6116
- if (!fs5.existsSync(filePath)) {
6097
+ if (!fs4.existsSync(filePath)) {
6117
6098
  return { text: `File not found: '${filePath}'`, code: 1 };
6118
6099
  }
6119
6100
  const ordSpec = /^(.*?)#(\d+)$/.exec(heading);
@@ -6200,7 +6181,7 @@ function runSectionCrossFile(pairs, opts) {
6200
6181
  textBlocks.push(`${key}:
6201
6182
  ${sub.text}`);
6202
6183
  }
6203
- const resolvePath = (f) => opts.projectRoot !== void 0 && !path5.isAbsolute(f) ? path5.resolve(opts.projectRoot, f) : f;
6184
+ const resolvePath = (f) => opts.projectRoot !== void 0 && !path4.isAbsolute(f) ? path4.resolve(opts.projectRoot, f) : f;
6204
6185
  const text = opts.json === true ? JSON.stringify(jsonOut, null, 2) : textBlocks.join("\n\n");
6205
6186
  if (anyFound) {
6206
6187
  const fullSourceBytes = sumFileSizes(Array.from(distinctFiles, resolvePath));
@@ -6273,7 +6254,7 @@ function renderRefsTargets(targets, opts, {
6273
6254
  const jsonOut = {};
6274
6255
  let anyFound = false;
6275
6256
  const lines = [];
6276
- const refFilePaths = [];
6257
+ const refRows = [];
6277
6258
  for (const { file, symbol, key } of targets) {
6278
6259
  const queryOpts = { name: symbol };
6279
6260
  if (opts.excludeTests === true || opts.grep !== void 0) queryOpts.limit = REFS_TOP_SCAN_LIMIT;
@@ -6299,7 +6280,7 @@ function renderRefsTargets(targets, opts, {
6299
6280
  results = results.slice(0, opts.limit ?? 100);
6300
6281
  }
6301
6282
  if (results.length > 0) anyFound = true;
6302
- refFilePaths.push(...results.map((r) => r.filePath));
6283
+ refRows.push(...results);
6303
6284
  if (opts.json === true) {
6304
6285
  const hiddenByGrep = matchesGrep !== void 0 ? preGrepCount - (filteredTotal ?? results.length) : 0;
6305
6286
  const withHidden = (payload) => ({ ...payload, ...annotateHiddenByGrep && hiddenByGrep > 0 ? { hiddenByGrep } : {} });
@@ -6341,7 +6322,7 @@ function renderRefsTargets(targets, opts, {
6341
6322
  if (notice !== null) lines.push(` token-goat: ${notice}`);
6342
6323
  }
6343
6324
  }
6344
- const fullSourceBytes = sumFileSizes(refFilePaths);
6325
+ const fullSourceBytes = refsSearchBaselineBytes(refRows);
6345
6326
  if (opts.json === true) {
6346
6327
  const text2 = JSON.stringify(jsonOut, null, 2);
6347
6328
  emit(text2);
@@ -6450,7 +6431,7 @@ function runRefsSingle(opts) {
6450
6431
  }
6451
6432
  return 1;
6452
6433
  }
6453
- const fullSourceBytes = sumFileSizes(results.map((r) => r.filePath));
6434
+ const fullSourceBytes = refsSearchBaselineBytes(results);
6454
6435
  if (opts.json === true) {
6455
6436
  let payload;
6456
6437
  if (opts.top !== void 0) {
@@ -6523,7 +6504,7 @@ function renderCallerGroups(refs, contextLines = 0) {
6523
6504
  return lines;
6524
6505
  }
6525
6506
  function noSymbolsMessage(displayPath, resolvedPath) {
6526
- if (!fs5.existsSync(resolvedPath)) {
6507
+ if (!fs4.existsSync(resolvedPath)) {
6527
6508
  return `Could not read: ${displayPath}`;
6528
6509
  }
6529
6510
  const lang = unsupportedLanguageName(resolvedPath);
@@ -7217,11 +7198,11 @@ function runPrSlice(opts) {
7217
7198
  }
7218
7199
  const fullSourceBytes = Buffer.byteLength(diffText, "utf8");
7219
7200
  if (opts.json === true) {
7220
- const jsonText = JSON.stringify({ path: parsed.path, diff: fenceGithubTextIfMatched(fileDiff) });
7201
+ const jsonText = JSON.stringify({ path: parsed.path, diff: fenceGithubFieldIfMatched(fileDiff) });
7221
7202
  emit(jsonText);
7222
7203
  recordReadStat("pr_slice", fullSourceBytes, jsonText, `${repo}#${opts.pr} diff:${parsed.path}`);
7223
7204
  } else {
7224
- emitGuarded(fenceGithubTextIfMatched(fileDiff), "pr-slice");
7205
+ emitGuarded(fenceGithubText(fileDiff), "pr-slice");
7225
7206
  recordReadStat("pr_slice", fullSourceBytes, fileDiff, `${repo}#${opts.pr} diff:${parsed.path}`);
7226
7207
  }
7227
7208
  return 0;
@@ -7230,14 +7211,14 @@ function runPrSlice(opts) {
7230
7211
  const comments = fetchPrComments(opts.pr, repo);
7231
7212
  const fullSourceBytes = Buffer.byteLength(JSON.stringify(comments), "utf8");
7232
7213
  if (opts.json === true) {
7233
- const fencedComments = comments.map((c) => ({ ...c, body: fenceGithubTextIfMatched(c.body) }));
7214
+ const fencedComments = comments.map((c) => ({ ...c, body: fenceGithubFieldIfMatched(c.body) }));
7234
7215
  const capped = guardJsonRows(fencedComments);
7235
7216
  const jsonText = JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount });
7236
7217
  emit(jsonText);
7237
7218
  recordReadStat("pr_slice", fullSourceBytes, jsonText, `${repo}#${opts.pr} comments`);
7238
7219
  } else {
7239
7220
  const text = formatCommentsSlice(comments);
7240
- emitGuarded(fenceGithubTextIfMatched(text), "pr-slice");
7221
+ emitGuarded(fenceGithubText(text), "pr-slice");
7241
7222
  recordReadStat("pr_slice", fullSourceBytes, text, `${repo}#${opts.pr} comments`);
7242
7223
  }
7243
7224
  return 0;
@@ -7248,15 +7229,15 @@ function runPrSlice(opts) {
7248
7229
  if (opts.json === true) {
7249
7230
  const fencedDesc = {
7250
7231
  ...desc,
7251
- title: fenceGithubTextIfMatched(desc.title),
7252
- body: desc.body !== null ? fenceGithubTextIfMatched(desc.body) : null
7232
+ title: fenceGithubFieldIfMatched(desc.title),
7233
+ body: desc.body !== null ? fenceGithubFieldIfMatched(desc.body) : null
7253
7234
  };
7254
7235
  const jsonText = JSON.stringify(fencedDesc);
7255
7236
  emit(jsonText);
7256
7237
  recordReadStat("pr_slice", fullSourceBytes, jsonText, `${repo}#${opts.pr} description`);
7257
7238
  } else {
7258
7239
  const text = formatDescriptionSlice(desc);
7259
- emitGuarded(fenceGithubTextIfMatched(text), "pr-slice");
7240
+ emitGuarded(fenceGithubText(text), "pr-slice");
7260
7241
  recordReadStat("pr_slice", fullSourceBytes, text, `${repo}#${opts.pr} description`);
7261
7242
  }
7262
7243
  return 0;
@@ -7362,10 +7343,10 @@ function runConflicts(opts) {
7362
7343
  if (opts.path === void 0) {
7363
7344
  files = walkProject(process.cwd()).files;
7364
7345
  } else {
7365
- const abs = path5.resolve(opts.path);
7346
+ const abs = path4.resolve(opts.path);
7366
7347
  let stat;
7367
7348
  try {
7368
- stat = fs5.statSync(abs);
7349
+ stat = fs4.statSync(abs);
7369
7350
  } catch {
7370
7351
  emitErr(`Could not read: ${opts.path}`);
7371
7352
  return 1;
@@ -7400,7 +7381,7 @@ async function runPdfExtractText(file, pagesSpec, layout = false) {
7400
7381
  if (!fileExists(file)) {
7401
7382
  throw new Error(`Could not read: ${file}`);
7402
7383
  }
7403
- const data = fs5.readFileSync(file);
7384
+ const data = fs4.readFileSync(file);
7404
7385
  const result = await extractPdfText(new Uint8Array(data), pagesSpec, layout);
7405
7386
  return result.text;
7406
7387
  }
@@ -7408,21 +7389,21 @@ async function runPdfOutline(file) {
7408
7389
  if (!fileExists(file)) {
7409
7390
  throw new Error(`Could not read: ${file}`);
7410
7391
  }
7411
- const data = fs5.readFileSync(file);
7392
+ const data = fs4.readFileSync(file);
7412
7393
  return extractPdfOutline(new Uint8Array(data));
7413
7394
  }
7414
7395
  async function runPdfMeta(file) {
7415
7396
  if (!fileExists(file)) {
7416
7397
  throw new Error(`Could not read: ${file}`);
7417
7398
  }
7418
- const data = fs5.readFileSync(file);
7399
+ const data = fs4.readFileSync(file);
7419
7400
  return extractPdfMeta(new Uint8Array(data));
7420
7401
  }
7421
7402
  async function runPdfLocate(file, pattern, opts) {
7422
7403
  if (!fileExists(file)) {
7423
7404
  throw new Error(`Could not read: ${file}`);
7424
7405
  }
7425
- const data = fs5.readFileSync(file);
7406
+ const data = fs4.readFileSync(file);
7426
7407
  return locatePdfPages(new Uint8Array(data), pattern, opts);
7427
7408
  }
7428
7409
  async function runImageMeta(file) {
@@ -7432,7 +7413,7 @@ async function runImageMeta(file) {
7432
7413
  if (!isImagePath(file)) {
7433
7414
  throw new Error(`Not an image file: ${file}`);
7434
7415
  }
7435
- const data = fs5.readFileSync(file);
7416
+ const data = fs4.readFileSync(file);
7436
7417
  const bytes = data.length;
7437
7418
  let probe;
7438
7419
  try {
@@ -7464,7 +7445,7 @@ async function runImageText(file) {
7464
7445
  if (!isImagePath(file)) {
7465
7446
  throw new Error(`Not an image file: ${file}`);
7466
7447
  }
7467
- const data = fs5.readFileSync(file);
7448
+ const data = fs4.readFileSync(file);
7468
7449
  const ocr = await ocrImage(data);
7469
7450
  if (ocr === null) {
7470
7451
  if (ocrIntegrityFailed()) {
@@ -7673,7 +7654,7 @@ function runFind(opts) {
7673
7654
  }
7674
7655
  if (opts.json === true) {
7675
7656
  const fuzzyPayload = fuzzyNames.length > 0 ? { fuzzy: true, matchedNames: fuzzyNames } : {};
7676
- emit(JSON.stringify({ files, truncated, ...fuzzyPayload }, null, 2));
7657
+ emit(JSON.stringify({ files, truncated, totalCount: allFiles.length, ...fuzzyPayload }, null, 2));
7677
7658
  return 0;
7678
7659
  }
7679
7660
  if (fuzzyNames.length > 0) {
@@ -8168,7 +8149,7 @@ function runGrep(opts) {
8168
8149
  if (activePins !== null) {
8169
8150
  let realDir;
8170
8151
  try {
8171
- realDir = foldRealpath(fs5.realpathSync(dir));
8152
+ realDir = foldRealpath(fs4.realpathSync(dir));
8172
8153
  } catch {
8173
8154
  return;
8174
8155
  }
@@ -8177,12 +8158,12 @@ function runGrep(opts) {
8177
8158
  visitedRealDirs.add(realDir);
8178
8159
  }
8179
8160
  try {
8180
- for (const entry of fs5.readdirSync(dir)) {
8161
+ for (const entry of fs4.readdirSync(dir)) {
8181
8162
  if (entry.startsWith(".")) continue;
8182
- const full = path5.join(dir, entry);
8163
+ const full = path4.join(dir, entry);
8183
8164
  let lst;
8184
8165
  try {
8185
- lst = fs5.lstatSync(full);
8166
+ lst = fs4.lstatSync(full);
8186
8167
  } catch {
8187
8168
  continue;
8188
8169
  }
@@ -8190,7 +8171,7 @@ function runGrep(opts) {
8190
8171
  if (lst.isSymbolicLink()) {
8191
8172
  let real;
8192
8173
  try {
8193
- real = fs5.realpathSync(full);
8174
+ real = fs4.realpathSync(full);
8194
8175
  } catch {
8195
8176
  continue;
8196
8177
  }
@@ -8199,7 +8180,7 @@ function runGrep(opts) {
8199
8180
  }
8200
8181
  let stat;
8201
8182
  try {
8202
- stat = fs5.statSync(target);
8183
+ stat = fs4.statSync(target);
8203
8184
  } catch {
8204
8185
  continue;
8205
8186
  }
@@ -8219,16 +8200,16 @@ function runGrep(opts) {
8219
8200
  emitErr(`Path not found: ${searchPath}`);
8220
8201
  return 1;
8221
8202
  }
8222
- const stat = fs5.statSync(searchPath);
8203
+ const stat = fs4.statSync(searchPath);
8223
8204
  if (stat.isDirectory()) {
8224
- const pinned = activePins?.get(pinKey(path5.resolve(searchPath)));
8205
+ const pinned = activePins?.get(pinKey(path4.resolve(searchPath)));
8225
8206
  if (pinned !== void 0) verifyPin(searchPath, pinned);
8226
8207
  let boundaryReal;
8227
8208
  try {
8228
- boundaryReal = fs5.realpathSync(searchPath);
8209
+ boundaryReal = fs4.realpathSync(searchPath);
8229
8210
  } catch (err) {
8230
8211
  if (pinned === void 0) {
8231
- boundaryReal = path5.resolve(searchPath);
8212
+ boundaryReal = path4.resolve(searchPath);
8232
8213
  } else {
8233
8214
  throw new ConfinementIdentityError(
8234
8215
  `refused: "${searchPath}" could not be resolved after validation (${String(err)}). The path may have been replaced or redirected after the confinement check, so the search was not performed.`
@@ -8359,7 +8340,7 @@ function runConfigGet(opts) {
8359
8340
  emit(value);
8360
8341
  return 0;
8361
8342
  }
8362
- const ext = path5.extname(opts.file).toLowerCase();
8343
+ const ext = path4.extname(opts.file).toLowerCase();
8363
8344
  if (ext === ".json") {
8364
8345
  try {
8365
8346
  let obj = JSON.parse(text);
@@ -8482,7 +8463,7 @@ function runExports(opts) {
8482
8463
  names.push(s.name);
8483
8464
  }
8484
8465
  }
8485
- const ext = path5.extname(opts.file).toLowerCase();
8466
+ const ext = path4.extname(opts.file).toLowerCase();
8486
8467
  const text = readFileText(diskPath);
8487
8468
  if (text === null && symbols.length === 0) {
8488
8469
  emitErr(`Could not read: ${opts.file}`);
@@ -8819,9 +8800,9 @@ function extractImports(text, ext) {
8819
8800
  return found;
8820
8801
  }
8821
8802
  function importsExtensionFor(filePath) {
8822
- const base = path5.basename(filePath).toLowerCase();
8803
+ const base = path4.basename(filePath).toLowerCase();
8823
8804
  if (base === "makefile" || base === "gnumakefile" || base === "bsdmakefile") return ".mk";
8824
- return path5.extname(filePath);
8805
+ return path4.extname(filePath);
8825
8806
  }
8826
8807
  function runImports(opts) {
8827
8808
  const multiFiles = parseMultiFileSpec(opts.file);
@@ -8913,7 +8894,7 @@ async function runSemantic(query, opts) {
8913
8894
  }
8914
8895
  const n = opts.limit !== void 0 && Number.isFinite(opts.limit) ? opts.limit : 20;
8915
8896
  if (opts.projectRoot !== void 0) {
8916
- if (!path5.isAbsolute(opts.projectRoot) || !fs5.existsSync(opts.projectRoot) || !fs5.statSync(opts.projectRoot).isDirectory()) {
8897
+ if (!path4.isAbsolute(opts.projectRoot) || !fs4.existsSync(opts.projectRoot) || !fs4.statSync(opts.projectRoot).isDirectory()) {
8917
8898
  const message = `token-goat: projectRoot must be an absolute, existing directory, got '${opts.projectRoot}'`;
8918
8899
  if (opts.json === true) {
8919
8900
  return { text: JSON.stringify({ error: message }, null, 2), code: 1 };
@@ -8943,6 +8924,18 @@ async function runSemantic(query, opts) {
8943
8924
  `Matching on meaning is off (${extractErrorMessage(e)}); these results come from keyword search alone.`
8944
8925
  );
8945
8926
  }
8927
+ if (rawHits.length === 0 && isAvailable()) {
8928
+ try {
8929
+ const enabled = loadConfig().indexing.embeddings_enabled;
8930
+ const { indexedFiles, embeddedFiles } = enabled ? getEmbeddingCoverage(globalDbPath(), rootDir) : { indexedFiles: 0, embeddedFiles: 0 };
8931
+ if (indexedFiles > 0 && embeddedFiles < indexedFiles) {
8932
+ console.warn(
8933
+ `Matching on meaning found nothing, and only ${embeddedFiles} of ${indexedFiles} indexed file(s) in this project have embeddings \u2014 these results come from keyword search alone. Run 'token-goat doctor' for why.`
8934
+ );
8935
+ }
8936
+ } catch {
8937
+ }
8938
+ }
8946
8939
  const mergedHits = mergeNearbyHits(rawHits);
8947
8940
  const overFetchFts = Math.min(MAX_OVER_FETCH, n * OVER_FETCH_FACTOR);
8948
8941
  const ftsRows = searchSymbolsFts(query, overFetchFts, void 0, rootDir);
@@ -8989,7 +8982,9 @@ async function runSemantic(query, opts) {
8989
8982
  const anyFilter = matchesGrep !== void 0 || opts.excludeTests === true;
8990
8983
  const filteredHits = anyFilter ? fusedHits.filter(keepHit) : fusedHits;
8991
8984
  const suppressedTotal = opts.excludeTests === true ? fusedHits.filter((h) => (matchesGrep === void 0 || matchesGrep(toDisplayPath(rootDir, h.filePath))) && isTestFile(h.filePath)).length : 0;
8985
+ const eligibleCount = filteredHits.length;
8992
8986
  const hits = filteredHits.slice(0, n);
8987
+ const candidatesClipped = rawHits.length >= overFetchForMerge || ftsRows.length >= overFetchFts;
8993
8988
  const hadDense = mergedHits.length > 0;
8994
8989
  const hadFts = ftsRows.length > 0;
8995
8990
  const source = hadDense && hadFts ? "hybrid" : hadDense ? "embeddings" : "fts";
@@ -9005,7 +9000,8 @@ async function runSemantic(query, opts) {
9005
9000
  preview: previewLines(h.previewText, 3)
9006
9001
  }));
9007
9002
  const capped = guardJsonRows(items);
9008
- const text3 = JSON.stringify({ source, ...capped }, null, 2);
9003
+ const limitTruncated = hits.length < eligibleCount;
9004
+ const text3 = JSON.stringify({ source, ...capped, truncated: capped.truncated || limitTruncated, totalCount: eligibleCount, ...candidatesClipped ? { totalCountIsFloor: true } : {} }, null, 2);
9009
9005
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text3, query);
9010
9006
  return { text: text3, code: 0 };
9011
9007
  }
@@ -9019,6 +9015,9 @@ ${previewLines(h.previewText, 3)}`;
9019
9015
  ${previewLines(h.previewText, 3)}`;
9020
9016
  });
9021
9017
  const text2 = guardText(blocks.join("\n\n"), "semantic");
9018
+ if (hits.length < eligibleCount) {
9019
+ emitErr(`Showing ${hits.length} of ${candidatesClipped ? "at least " : ""}${countNoun(eligibleCount, "match", "matches")} (raise --limit to see the rest).`);
9020
+ }
9022
9021
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text2, query);
9023
9022
  return { text: text2, code: 0 };
9024
9023
  }
@@ -9133,6 +9132,318 @@ function runNoteList(opts = {}) {
9133
9132
  return { text: lines.join("\n"), code: 0 };
9134
9133
  }
9135
9134
 
9135
+ // src/import_graph.ts
9136
+ import * as fs5 from "node:fs";
9137
+ import * as path5 from "node:path";
9138
+ var DIRECT_PROBE_EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cjs", ".cts", ".py"];
9139
+ var INDEX_PROBE_EXTENSIONS = [".ts", ".js", ".tsx", ".jsx", ".mts", ".cts"];
9140
+ function buildImportGraph(cwd) {
9141
+ const files = getTrackedFiles(cwd);
9142
+ const filesByFoldedPath = /* @__PURE__ */ new Map();
9143
+ for (const f of files) filesByFoldedPath.set(foldPath(f), f);
9144
+ const resolve5 = (fromFile, spec) => {
9145
+ if (!spec.startsWith(".")) return null;
9146
+ const dir = path5.dirname(fromFile);
9147
+ const strippedSpec = spec.replace(/\.(m?js|cjs)$/, "");
9148
+ const base = path5.resolve(dir, strippedSpec);
9149
+ for (const ext of DIRECT_PROBE_EXTENSIONS) {
9150
+ const match = filesByFoldedPath.get(foldPath(base + ext));
9151
+ if (match !== void 0) return match;
9152
+ }
9153
+ const idx = path5.join(base, "index");
9154
+ for (const ext of INDEX_PROBE_EXTENSIONS) {
9155
+ const match = filesByFoldedPath.get(foldPath(idx + ext));
9156
+ if (match !== void 0) return match;
9157
+ }
9158
+ return null;
9159
+ };
9160
+ const graph = /* @__PURE__ */ new Map();
9161
+ const importedBy = /* @__PURE__ */ new Map();
9162
+ for (const file of files) {
9163
+ let text;
9164
+ try {
9165
+ text = fs5.readFileSync(file, "utf8");
9166
+ } catch {
9167
+ continue;
9168
+ }
9169
+ const internal = [];
9170
+ for (const spec of extractImports(text, importsExtensionFor(file))) {
9171
+ const resolved = resolve5(file, spec);
9172
+ if (resolved === null) continue;
9173
+ internal.push(resolved);
9174
+ let importers = importedBy.get(resolved);
9175
+ if (importers === void 0) {
9176
+ importers = /* @__PURE__ */ new Set();
9177
+ importedBy.set(resolved, importers);
9178
+ }
9179
+ importers.add(file);
9180
+ }
9181
+ graph.set(file, internal);
9182
+ }
9183
+ return { files, graph, importedBy, resolve: resolve5 };
9184
+ }
9185
+
9186
+ // src/modules.ts
9187
+ var WEAK_MODULARITY = 0.3;
9188
+ var MAX_PASSES = 32;
9189
+ var GAIN_EPSILON = 1e-12;
9190
+ function emptyGraph(n) {
9191
+ return { n, adj: Array.from({ length: n }, () => /* @__PURE__ */ new Map()), degree: new Array(n).fill(0), m: 0 };
9192
+ }
9193
+ function addEdge(g, a, b, w) {
9194
+ if (a === b) {
9195
+ g.adj[a].set(a, (g.adj[a].get(a) ?? 0) + w);
9196
+ g.degree[a] += 2 * w;
9197
+ } else {
9198
+ g.adj[a].set(b, (g.adj[a].get(b) ?? 0) + w);
9199
+ g.adj[b].set(a, (g.adj[b].get(a) ?? 0) + w);
9200
+ g.degree[a] += w;
9201
+ g.degree[b] += w;
9202
+ }
9203
+ g.m += w;
9204
+ }
9205
+ function localMoving(g) {
9206
+ const community = Array.from({ length: g.n }, (_, i) => i);
9207
+ const tot = g.degree.slice();
9208
+ const twoM = 2 * g.m;
9209
+ let movedEver = false;
9210
+ if (twoM === 0) return { community, moved: false };
9211
+ for (let pass = 0; pass < MAX_PASSES; pass++) {
9212
+ let improved = false;
9213
+ for (let i = 0; i < g.n; i++) {
9214
+ const ki = g.degree[i];
9215
+ const ci = community[i];
9216
+ const links = /* @__PURE__ */ new Map();
9217
+ for (const [j, w] of g.adj[i]) {
9218
+ if (j === i) continue;
9219
+ const cj = community[j];
9220
+ links.set(cj, (links.get(cj) ?? 0) + w);
9221
+ }
9222
+ tot[ci] = tot[ci] - ki;
9223
+ let bestC = ci;
9224
+ let bestGain = (links.get(ci) ?? 0) - tot[ci] * ki / twoM;
9225
+ for (const c of [...links.keys()].sort((a, b) => a - b)) {
9226
+ if (c === ci) continue;
9227
+ const gain = links.get(c) - tot[c] * ki / twoM;
9228
+ if (gain > bestGain + GAIN_EPSILON) {
9229
+ bestGain = gain;
9230
+ bestC = c;
9231
+ }
9232
+ }
9233
+ tot[bestC] = tot[bestC] + ki;
9234
+ community[i] = bestC;
9235
+ if (bestC !== ci) {
9236
+ improved = true;
9237
+ movedEver = true;
9238
+ }
9239
+ }
9240
+ if (!improved) break;
9241
+ }
9242
+ return { community, moved: movedEver };
9243
+ }
9244
+ function densify(community) {
9245
+ const firstMember = /* @__PURE__ */ new Map();
9246
+ for (let i = 0; i < community.length; i++) {
9247
+ const c = community[i];
9248
+ if (!firstMember.has(c)) firstMember.set(c, i);
9249
+ }
9250
+ const ordered = [...firstMember.entries()].sort((a, b) => a[1] - b[1]).map(([c]) => c);
9251
+ const remap = /* @__PURE__ */ new Map();
9252
+ ordered.forEach((c, idx) => remap.set(c, idx));
9253
+ return { dense: community.map((c) => remap.get(c)), count: ordered.length };
9254
+ }
9255
+ function modularityOf(g, partition) {
9256
+ const twoM = 2 * g.m;
9257
+ if (twoM === 0) return null;
9258
+ const internal = /* @__PURE__ */ new Map();
9259
+ const tot = /* @__PURE__ */ new Map();
9260
+ for (let i = 0; i < g.n; i++) {
9261
+ const ci = partition[i];
9262
+ tot.set(ci, (tot.get(ci) ?? 0) + g.degree[i]);
9263
+ for (const [j, w] of g.adj[i]) {
9264
+ if (j < i) continue;
9265
+ if (partition[j] === ci) internal.set(ci, (internal.get(ci) ?? 0) + w);
9266
+ }
9267
+ }
9268
+ let q = 0;
9269
+ for (const [c, t] of tot) {
9270
+ q += 2 * (internal.get(c) ?? 0) / twoM - (t / twoM) ** 2;
9271
+ }
9272
+ return q;
9273
+ }
9274
+ function louvain(g) {
9275
+ let current = g;
9276
+ let mapping = Array.from({ length: g.n }, (_, i) => i);
9277
+ for (let level = 0; level < MAX_PASSES; level++) {
9278
+ const { community, moved } = localMoving(current);
9279
+ const { dense, count } = densify(community);
9280
+ mapping = mapping.map((c) => dense[c]);
9281
+ if (!moved || count === current.n) break;
9282
+ const next = emptyGraph(count);
9283
+ for (let i = 0; i < current.n; i++) {
9284
+ for (const [j, w] of current.adj[i]) {
9285
+ if (j < i) continue;
9286
+ addEdge(next, dense[i], dense[j], w);
9287
+ }
9288
+ }
9289
+ current = next;
9290
+ }
9291
+ return mapping;
9292
+ }
9293
+ function commonDirLabel(files) {
9294
+ const dirs = new Set(files.map(dirOf));
9295
+ if (dirs.size === 1) {
9296
+ const only = [...dirs][0];
9297
+ return only === "." ? "." : `${only}/`;
9298
+ }
9299
+ const segmentLists = files.map((f) => {
9300
+ const parts = f.split("/");
9301
+ parts.pop();
9302
+ return parts;
9303
+ });
9304
+ const first = segmentLists[0];
9305
+ if (first === void 0) return null;
9306
+ const prefix = [];
9307
+ for (let i = 0; i < first.length; i++) {
9308
+ const seg = first[i];
9309
+ if (!segmentLists.every((s) => s[i] === seg)) break;
9310
+ prefix.push(seg);
9311
+ }
9312
+ return prefix.length === 0 ? null : `${prefix.join("/")}/`;
9313
+ }
9314
+ function dirOf(file) {
9315
+ const idx = file.lastIndexOf("/");
9316
+ return idx === -1 ? "." : file.slice(0, idx);
9317
+ }
9318
+ function detectModules(imports, cwd, top) {
9319
+ const { files, graph } = imports;
9320
+ const root = getDisplayRoot(cwd);
9321
+ const degreeOf = /* @__PURE__ */ new Map();
9322
+ const bump = (f) => {
9323
+ degreeOf.set(f, (degreeOf.get(f) ?? 0) + 1);
9324
+ };
9325
+ for (const [from, targets] of graph) {
9326
+ for (const to of targets) {
9327
+ if (from === to) continue;
9328
+ bump(from);
9329
+ bump(to);
9330
+ }
9331
+ }
9332
+ const connected = files.filter((f) => (degreeOf.get(f) ?? 0) > 0);
9333
+ const isolatedCount = files.length - connected.length;
9334
+ if (connected.length === 0) {
9335
+ return { modules: [], modulesTotal: 0, modularity: null, isolatedCount, crossImports: [], crossImportsTotal: 0, noEdges: true };
9336
+ }
9337
+ const ordered = [...connected].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
9338
+ const idOf = /* @__PURE__ */ new Map();
9339
+ ordered.forEach((f, i) => idOf.set(f, i));
9340
+ const g = emptyGraph(ordered.length);
9341
+ const pairWeight = /* @__PURE__ */ new Map();
9342
+ for (const [from, targets] of graph) {
9343
+ const a = idOf.get(from);
9344
+ if (a === void 0) continue;
9345
+ for (const to of targets) {
9346
+ const b = idOf.get(to);
9347
+ if (b === void 0 || a === b) continue;
9348
+ const key = a < b ? `${a}:${b}` : `${b}:${a}`;
9349
+ pairWeight.set(key, (pairWeight.get(key) ?? 0) + 1);
9350
+ }
9351
+ }
9352
+ for (const key of [...pairWeight.keys()].sort()) {
9353
+ const [a, b] = key.split(":").map(Number);
9354
+ addEdge(g, a, b, pairWeight.get(key));
9355
+ }
9356
+ const partition = louvain(g);
9357
+ const modularity = modularityOf(g, partition);
9358
+ const membersOf = /* @__PURE__ */ new Map();
9359
+ for (let i = 0; i < ordered.length; i++) {
9360
+ const c = partition[i];
9361
+ const list = membersOf.get(c);
9362
+ if (list === void 0) membersOf.set(c, [ordered[i]]);
9363
+ else list.push(ordered[i]);
9364
+ }
9365
+ const inModuleDegree = /* @__PURE__ */ new Map();
9366
+ for (const [from, targets] of graph) {
9367
+ const a = idOf.get(from);
9368
+ if (a === void 0) continue;
9369
+ for (const to of targets) {
9370
+ const b = idOf.get(to);
9371
+ if (b === void 0 || a === b || partition[a] !== partition[b]) continue;
9372
+ inModuleDegree.set(from, (inModuleDegree.get(from) ?? 0) + 1);
9373
+ inModuleDegree.set(to, (inModuleDegree.get(to) ?? 0) + 1);
9374
+ }
9375
+ }
9376
+ const built = [...membersOf.entries()].map(([c, absFiles]) => {
9377
+ const display = absFiles.map((f) => toDisplayPath(root, f)).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
9378
+ const coreAbs = [...absFiles].sort((a, b) => (inModuleDegree.get(b) ?? 0) - (inModuleDegree.get(a) ?? 0) || (a < b ? -1 : a > b ? 1 : 0))[0];
9379
+ return { community: c, core: toDisplayPath(root, coreAbs), commonDir: commonDirLabel(display), files: display, directories: new Set(display.map(dirOf)).size };
9380
+ }).sort((a, b) => b.files.length - a.files.length || (a.core < b.core ? -1 : a.core > b.core ? 1 : 0) || (a.files[0] < b.files[0] ? -1 : 1));
9381
+ const indexOfCommunity = /* @__PURE__ */ new Map();
9382
+ built.forEach((m, i) => indexOfCommunity.set(m.community, i + 1));
9383
+ const coreOfIndex = /* @__PURE__ */ new Map();
9384
+ built.forEach((m, i) => coreOfIndex.set(i + 1, m.core));
9385
+ const crossCount = /* @__PURE__ */ new Map();
9386
+ for (const [from, targets] of graph) {
9387
+ const a = idOf.get(from);
9388
+ if (a === void 0) continue;
9389
+ const ca = indexOfCommunity.get(partition[a]);
9390
+ if (ca === void 0) continue;
9391
+ for (const to of targets) {
9392
+ const b = idOf.get(to);
9393
+ if (b === void 0) continue;
9394
+ const cb = indexOfCommunity.get(partition[b]);
9395
+ if (cb === void 0 || ca === cb) continue;
9396
+ const key = `${ca}:${cb}`;
9397
+ crossCount.set(key, (crossCount.get(key) ?? 0) + 1);
9398
+ }
9399
+ }
9400
+ const allCross = [...crossCount.entries()].map(([key, imports2]) => {
9401
+ const [fromIndex, toIndex] = key.split(":").map(Number);
9402
+ return { fromIndex, toIndex, fromCore: coreOfIndex.get(fromIndex), toCore: coreOfIndex.get(toIndex), imports: imports2 };
9403
+ }).sort((a, b) => b.imports - a.imports || a.fromIndex - b.fromIndex || a.toIndex - b.toIndex);
9404
+ const modules = built.slice(0, top).map((m, i) => ({ index: i + 1, core: m.core, commonDir: m.commonDir, files: m.files, size: m.files.length, directories: m.directories }));
9405
+ return {
9406
+ modules,
9407
+ modulesTotal: built.length,
9408
+ modularity,
9409
+ isolatedCount,
9410
+ crossImports: allCross.slice(0, top),
9411
+ crossImportsTotal: allCross.length,
9412
+ noEdges: false
9413
+ };
9414
+ }
9415
+ function renderModules(result, top) {
9416
+ const lines = [];
9417
+ if (result.noEdges) {
9418
+ lines.push(`modules: none. No file in this project imports another by a relative path, so there is nothing to group (${countNoun(result.isolatedCount, "file")} checked).`);
9419
+ return lines;
9420
+ }
9421
+ const q = result.modularity;
9422
+ const strength = q === null ? "" : q < WEAK_MODULARITY ? `, modularity ${q.toFixed(2)} -- weak, so treat this grouping as a hint rather than a finding` : `, modularity ${q.toFixed(2)}`;
9423
+ lines.push(result.modules.length < result.modulesTotal ? `modules (top ${result.modules.length} of ${result.modulesTotal}${strength}):` : `modules (${result.modulesTotal} found${strength}):`);
9424
+ for (const m of result.modules) {
9425
+ const where = m.commonDir === null ? `across ${m.directories} directories` : m.commonDir === "." ? "all in the repository root" : `all in ${m.commonDir}`;
9426
+ lines.push(` #${m.index} ${m.size} files ${m.core} (${where})`);
9427
+ }
9428
+ if (result.isolatedCount > 0) {
9429
+ lines.push(` ${result.isolatedCount} files are in no module (they neither import nor are imported within the project).`);
9430
+ }
9431
+ if (result.crossImportsTotal === 0) {
9432
+ lines.push("cross-module imports: none. Every import stays inside its own module.");
9433
+ } else {
9434
+ lines.push(
9435
+ result.crossImports.length < result.crossImportsTotal ? `cross-module imports (top ${result.crossImports.length} of ${result.crossImportsTotal} pairs):` : `cross-module imports (${result.crossImportsTotal} pairs):`
9436
+ );
9437
+ for (const c of result.crossImports) {
9438
+ lines.push(` ${c.imports} imports #${c.fromIndex} ${c.fromCore} -> #${c.toIndex} ${c.toCore}`);
9439
+ }
9440
+ }
9441
+ if (result.modules.length < result.modulesTotal && result.crossImports.some((c) => c.fromIndex > top || c.toIndex > top)) {
9442
+ lines.push(` (a #N above ${top} refers to a module the --top cap left out; raise --top to see it.)`);
9443
+ }
9444
+ return lines;
9445
+ }
9446
+
9136
9447
  // src/graph_commands.ts
9137
9448
  var DEFAULT_REF_QUERY_LIMIT = 500;
9138
9449
  var ALL_SYMBOLS_IN_FILE_LIMIT = 1e4;
@@ -9819,7 +10130,7 @@ var TYPE_KINDS = [
9819
10130
  var CORE_SYMBOL_KINDS = [
9820
10131
  .../* @__PURE__ */ new Set([...GENERIC_SYMBOL_KINDS, ...TYPE_KINDS, ...OTHER_ADAPTER_SYMBOL_KINDS])
9821
10132
  ];
9822
- var TYPES_EXCLUDE_TESTS_SCAN_LIMIT = 5e3;
10133
+ var TYPES_SCAN_LIMIT = 5e3;
9823
10134
  function runTypes(opts) {
9824
10135
  if (opts.limit !== void 0 && opts.limit <= 0) {
9825
10136
  emitErr2(`--limit must be a positive number, got: ${opts.limit}`);
@@ -9832,34 +10143,25 @@ function runTypes(opts) {
9832
10143
  const rootDir = resolveProjectRoot({ project: process.cwd() });
9833
10144
  const results = [];
9834
10145
  let suppressed = 0;
9835
- const queryLimit = excludeTests ? TYPES_EXCLUDE_TESTS_SCAN_LIMIT : limit;
9836
10146
  for (const k of TYPE_KINDS) {
9837
- const syms = querySymbols({ kind: k, ...fpOpt, limit: queryLimit, rootDir });
9838
- if (!excludeTests) {
9839
- results.push(...syms);
9840
- continue;
9841
- }
9842
- const kept = [];
10147
+ const syms = querySymbols({ kind: k, ...fpOpt, limit: TYPES_SCAN_LIMIT, rootDir });
9843
10148
  for (const sym of syms) {
9844
- if (isTestFile(sym.filePath)) {
10149
+ if (excludeTests && isTestFile(sym.filePath)) {
9845
10150
  suppressed += 1;
9846
10151
  continue;
9847
10152
  }
9848
- kept.push(sym);
10153
+ results.push(sym);
9849
10154
  }
9850
- results.push(...kept.slice(0, limit));
9851
10155
  }
9852
- const classes = querySymbols({ kind: "class", ...fpOpt, limit: queryLimit, rootDir });
9853
- const typeClasses = [];
10156
+ const classes = querySymbols({ kind: "class", ...fpOpt, limit: TYPES_SCAN_LIMIT, rootDir });
9854
10157
  for (const cls of classes) {
9855
10158
  if (!looksLikeTypeClass(cls.body)) continue;
9856
10159
  if (excludeTests && isTestFile(cls.filePath)) {
9857
10160
  suppressed += 1;
9858
10161
  continue;
9859
10162
  }
9860
- typeClasses.push(cls);
10163
+ results.push(cls);
9861
10164
  }
9862
- results.push(...excludeTests ? typeClasses.slice(0, limit) : typeClasses);
9863
10165
  results.sort(
9864
10166
  // Pin the locale: an unlocaled localeCompare() sorts differently across
9865
10167
  // Node's small-icu vs full-icu builds and different system default
@@ -9886,7 +10188,16 @@ function runTypes(opts) {
9886
10188
  }
9887
10189
  const preFilterCount = results.length;
9888
10190
  const matchesGrep = opts.grep !== void 0 ? compileGrepMatcher(opts.grep) : void 0;
9889
- const filtered = matchesGrep !== void 0 ? results.filter((r) => matchesGrep(r.name)) : results;
10191
+ const matched = matchesGrep !== void 0 ? results.filter((r) => matchesGrep(r.name)) : results;
10192
+ const eligibleCount = matched.length;
10193
+ const perKindShown = /* @__PURE__ */ new Map();
10194
+ const filtered = matched.filter((r) => {
10195
+ const seen = perKindShown.get(r.kind) ?? 0;
10196
+ if (seen >= limit) return false;
10197
+ perKindShown.set(r.kind, seen + 1);
10198
+ return true;
10199
+ });
10200
+ const cappedOut = eligibleCount - filtered.length;
9890
10201
  if (filtered.length === 0) {
9891
10202
  if (opts.json === true) {
9892
10203
  emit2(JSON.stringify({ items: [], truncated: false, totalCount: 0, hiddenByGrep: preFilterCount }, null, 2));
@@ -9897,8 +10208,8 @@ function runTypes(opts) {
9897
10208
  }
9898
10209
  if (opts.json === true) {
9899
10210
  const capped = guardJsonRows(filtered.map((r) => ({ ...r, filePath: toDisplayPath(rootDir, r.filePath) })));
9900
- const hiddenByGrep = preFilterCount - filtered.length;
9901
- emit2(JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...excludeTests && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {} }, null, 2));
10211
+ const hiddenByGrep = preFilterCount - eligibleCount;
10212
+ emit2(JSON.stringify({ items: capped.items, truncated: capped.truncated || cappedOut > 0, totalCount: eligibleCount, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...excludeTests && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {} }, null, 2));
9902
10213
  return 0;
9903
10214
  }
9904
10215
  if (excludeTests && suppressed > 0) {
@@ -9907,6 +10218,9 @@ function runTypes(opts) {
9907
10218
  for (const r of filtered) {
9908
10219
  emit2(`${r.name} ${r.kind} ${toDisplayPath(rootDir, r.filePath)}:${r.lineStart}`);
9909
10220
  }
10221
+ if (cappedOut > 0) {
10222
+ emitErr2(`Showing ${filtered.length} of ${eligibleCount} type declarations (--limit is per kind; raise it to see the rest).`);
10223
+ }
9910
10224
  return 0;
9911
10225
  }
9912
10226
  function runScope(opts) {
@@ -9922,7 +10236,7 @@ function runScope(opts) {
9922
10236
  return 1;
9923
10237
  }
9924
10238
  const line = Number.parseInt(lineStr, 10);
9925
- if (!Number.isFinite(line) || line < 1) {
10239
+ if (!Number.isSafeInteger(line) || line < 1) {
9926
10240
  emitErr2(`Invalid line number: ${lineStr}`);
9927
10241
  return 1;
9928
10242
  }
@@ -9932,25 +10246,32 @@ function runScope(opts) {
9932
10246
  return 1;
9933
10247
  }
9934
10248
  const filePath = resolveIndexPath(file);
9935
- const syms = querySymbols({ filePath, limit: ALL_SYMBOLS_IN_FILE_LIMIT });
9936
- const enclosing = syms.filter((s) => s.lineStart <= line && line <= s.lineEnd).sort((a, b) => b.lineStart - a.lineStart || a.lineEnd - b.lineEnd || a.name.localeCompare(b.name, "en"));
9937
- if (syms.length === 0) {
9938
- if (!fs6.existsSync(filePath)) {
9939
- emitErr2(`Could not read: ${file}`);
10249
+ const enclosing = querySymbols({ filePath, enclosingLine: line, limit: ALL_SYMBOLS_IN_FILE_LIMIT }).sort((a, b) => b.lineStart - a.lineStart || a.lineEnd - b.lineEnd || a.name.localeCompare(b.name, "en"));
10250
+ if (enclosing.length === 0) {
10251
+ if (querySymbols({ filePath, limit: 1 }).length === 0) {
10252
+ if (!fs6.existsSync(filePath)) {
10253
+ emitErr2(`Could not read: ${file}`);
10254
+ return 1;
10255
+ }
10256
+ emitErr2(
10257
+ `No indexed symbols in '${file}' \u2014 the file exists but nothing is indexed for it, so every line looks empty`
10258
+ );
9940
10259
  return 1;
9941
10260
  }
9942
- emitErr2(`No indexed symbols in '${file}' \u2014 the file exists but nothing is indexed for it, so every line looks empty`);
9943
- return 1;
9944
- }
9945
- if (enclosing.length === 0) {
9946
10261
  emitErr2(`No symbols enclosing line ${line} in '${file}'`);
9947
10262
  return 1;
9948
10263
  }
10264
+ const scopeDisplayRoot = getDisplayRoot();
9949
10265
  if (opts.json === true) {
9950
- emit2(JSON.stringify(enclosing, null, 2));
10266
+ emit2(
10267
+ JSON.stringify(
10268
+ enclosing.map((s) => ({ ...s, filePath: toDisplayPath(scopeDisplayRoot, s.filePath) })),
10269
+ null,
10270
+ 2
10271
+ )
10272
+ );
9951
10273
  return 0;
9952
10274
  }
9953
- const scopeDisplayRoot = getDisplayRoot();
9954
10275
  for (const s of enclosing) {
9955
10276
  emit2(`${s.name} ${s.kind} ${toDisplayPath(scopeDisplayRoot, s.filePath)}:${s.lineStart}-${s.lineEnd}`);
9956
10277
  }
@@ -10075,8 +10396,12 @@ function runSimilar(opts) {
10075
10396
  const words = [anchor.name, ...(anchor.docstring ?? "").split(/\s+/).filter((w) => w.length > 4)];
10076
10397
  const query = words.slice(0, 8).join(" ");
10077
10398
  const rootDir = resolveProjectRoot({ project: process.cwd() });
10078
- const hits = searchSymbolsFts(query, top + 1, void 0, rootDir);
10079
- const results = hits.filter((h) => !(h.filePath === anchor.filePath && h.name === anchor.name)).slice(0, top);
10399
+ const hits = searchSymbolsFts(query, UNBOUNDED_REF_LIMIT, void 0, rootDir);
10400
+ const matches = hits.filter((h) => !(h.filePath === anchor.filePath && h.name === anchor.name));
10401
+ const results = matches.slice(0, top);
10402
+ if (matches.length > results.length) {
10403
+ emitErr2(`Showing top ${results.length} of ${matches.length} similar symbols (raise --top to see the rest).`);
10404
+ }
10080
10405
  if (opts.json === true) {
10081
10406
  emit2(JSON.stringify(results.map((h) => ({ name: h.name, kind: h.kind, file: h.filePath, line: h.lineStart })), null, 2));
10082
10407
  return 0;
@@ -10092,7 +10417,9 @@ function runContextFor(opts) {
10092
10417
  const top = opts.top ?? 12;
10093
10418
  const budget = opts.budget;
10094
10419
  const rootDir = resolveProjectRoot({ project: process.cwd() });
10095
- const hits = searchSymbolsFts(opts.task, top, void 0, rootDir);
10420
+ const fetched = searchSymbolsFts(opts.task, top + 1, void 0, rootDir);
10421
+ const moreBeyondTop = fetched.length > top;
10422
+ const hits = fetched.slice(0, top);
10096
10423
  if (hits.length === 0) {
10097
10424
  emitErr2(`No matches found for '${opts.task}'`);
10098
10425
  return 1;
@@ -10105,6 +10432,15 @@ function runContextFor(opts) {
10105
10432
  tokensSoFar += bodyTokens;
10106
10433
  entries.push({ file: h.filePath, symbol: h.name, kind: h.kind, line: h.lineStart, readCmd: `token-goat read "${h.filePath}::${h.name}@${h.lineStart}"` });
10107
10434
  }
10435
+ const skippedByBudget = hits.length - entries.length;
10436
+ const notices = [];
10437
+ if (skippedByBudget > 0) {
10438
+ notices.push(
10439
+ entries.length === 0 ? `All ${countNoun(skippedByBudget, "matching symbol")} were larger than --budget ${String(opts.budget)} tokens, so none are shown. Raise --budget.` : `Showing ${entries.length} of ${countNoun(hits.length, "matching symbol")}; ${skippedByBudget} did not fit --budget ${String(opts.budget)} tokens.`
10440
+ );
10441
+ }
10442
+ if (moreBeyondTop) notices.push(`More matches exist beyond --top ${top} (raise it to see them).`);
10443
+ for (const n of notices) emitErr2(n);
10108
10444
  if (opts.json === true) {
10109
10445
  emit2(JSON.stringify(entries, null, 2));
10110
10446
  return 0;
@@ -10195,66 +10531,31 @@ function runArch(opts) {
10195
10531
  }
10196
10532
  const cwd = opts.cwd ?? process.cwd();
10197
10533
  const top = opts.top ?? 10;
10198
- const files = getTrackedFiles(cwd);
10534
+ const { files, graph, importedBy, resolve: resolve5 } = buildImportGraph(cwd);
10199
10535
  if (files.length === 0 && opts.json !== true) {
10200
10536
  emit2(`no tracked files found under '${toDisplayPath(getDisplayRoot(cwd), cwd)}' (is it a git repo?). Nothing to analyse.`);
10201
10537
  return 0;
10202
10538
  }
10203
- const filesByFoldedPath = /* @__PURE__ */ new Map();
10204
- for (const f of files) filesByFoldedPath.set(foldPath(f), f);
10205
- const graph = /* @__PURE__ */ new Map();
10206
- const importedBy = /* @__PURE__ */ new Map();
10207
- const resolveRelImport = (fromFile, spec) => {
10208
- if (!spec.startsWith(".")) return null;
10209
- const dir = path6.dirname(fromFile);
10210
- const strippedSpec = spec.replace(/\.(m?js|cjs)$/, "");
10211
- const base = path6.resolve(dir, strippedSpec);
10212
- for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cjs", ".cts", ".py"]) {
10213
- const candidate = base + ext;
10214
- const match = filesByFoldedPath.get(foldPath(candidate));
10215
- if (match !== void 0) return match;
10216
- }
10217
- const idx = path6.join(base, "index");
10218
- for (const ext of [".ts", ".js", ".tsx", ".jsx", ".mts", ".cts"]) {
10219
- const candidate = idx + ext;
10220
- const match = filesByFoldedPath.get(foldPath(candidate));
10221
- if (match !== void 0) return match;
10222
- }
10223
- return null;
10224
- };
10225
- for (const file of files) {
10226
- let text;
10227
- try {
10228
- text = fs6.readFileSync(file, "utf8");
10229
- } catch {
10230
- continue;
10231
- }
10232
- const ext = importsExtensionFor(file);
10233
- const rawImports = extractImports(text, ext);
10234
- const internal = [];
10235
- for (const spec of rawImports) {
10236
- const resolved = resolveRelImport(file, spec);
10237
- if (resolved !== null) {
10238
- internal.push(resolved);
10239
- if (!importedBy.has(resolved)) importedBy.set(resolved, /* @__PURE__ */ new Set());
10240
- importedBy.get(resolved).add(file);
10241
- }
10242
- }
10243
- graph.set(file, internal);
10244
- }
10245
- const hubs = [...importedBy.entries()].sort((a, b) => b[1].size - a[1].size).slice(0, top).map(([f, importers]) => ({ file: f, importedBy: importers.size }));
10246
- const entryPoints = files.filter((f) => !importedBy.has(f) && (graph.get(f) ?? []).length > 0).slice(0, top).map((f) => ({ file: f }));
10539
+ const allHubs = [...importedBy.entries()].sort((a, b) => b[1].size - a[1].size).map(([f, importers]) => ({ file: f, importedBy: importers.size }));
10540
+ const hubsTotal = allHubs.length;
10541
+ const hubs = allHubs.slice(0, top);
10542
+ const allEntryPoints = files.filter((f) => !importedBy.has(f) && (graph.get(f) ?? []).length > 0).map((f) => ({ file: f }));
10543
+ const entryPointsTotal = allEntryPoints.length;
10544
+ const entryPoints = allEntryPoints.slice(0, top);
10247
10545
  const { cycles, truncated: cyclesTruncated } = findCyclesCapped(graph);
10546
+ const moduleResult = opts.modules === true ? detectModules({ files, graph, importedBy, resolve: resolve5 }, cwd, top) : null;
10248
10547
  if (opts.json === true) {
10249
- emit2(JSON.stringify({ hubs, entryPoints, cycles, ...cyclesTruncated ? { cyclesTruncated: true } : {} }, null, 2));
10548
+ const modulePayload = moduleResult === null ? {} : { modules: moduleResult.modules, modulesTotal: moduleResult.modulesTotal, modulesTruncated: moduleResult.modules.length < moduleResult.modulesTotal, modularity: moduleResult.modularity, isolatedCount: moduleResult.isolatedCount, crossImports: moduleResult.crossImports, crossImportsTotal: moduleResult.crossImportsTotal, crossImportsTruncated: moduleResult.crossImports.length < moduleResult.crossImportsTotal, noImportEdges: moduleResult.noEdges };
10549
+ emit2(JSON.stringify({ hubs, hubsTotal, hubsTruncated: hubs.length < hubsTotal, entryPoints, entryPointsTotal, entryPointsTruncated: entryPoints.length < entryPointsTotal, cycles, ...cyclesTruncated ? { cyclesTruncated: true } : {}, ...modulePayload }, null, 2));
10250
10550
  return 0;
10251
10551
  }
10252
- emit2(`hubs (top ${top} most-imported):`);
10552
+ emit2(hubs.length < hubsTotal ? `hubs (top ${hubs.length} of ${hubsTotal} most-imported):` : `hubs (${hubsTotal} most-imported):`);
10253
10553
  for (const h of hubs) emit2(` ${h.importedBy} importers ${toDisplayPath(getDisplayRoot(opts.cwd), h.file)}`);
10254
- emit2(`entry points (imported by nobody, top ${top}):`);
10554
+ emit2(entryPoints.length < entryPointsTotal ? `entry points (imported by nobody, top ${entryPoints.length} of ${entryPointsTotal}):` : `entry points (imported by nobody, ${entryPointsTotal} found):`);
10255
10555
  for (const e of entryPoints) emit2(` ${toDisplayPath(getDisplayRoot(opts.cwd), e.file)}`);
10256
10556
  emit2(cyclesTruncated ? `cycles (first ${cycles.length}, truncated at the ${MAX_CYCLES}-cycle enumeration limit: more cycles exist):` : `cycles (${cycles.length} found):`);
10257
10557
  for (const c of cycles) emit2(` ${c.map((f) => toDisplayPath(getDisplayRoot(opts.cwd), f)).join(" -> ")}`);
10558
+ if (moduleResult !== null) for (const line of renderModules(moduleResult, top)) emit2(line);
10258
10559
  return 0;
10259
10560
  }
10260
10561
  function runBlame(opts) {
@@ -10301,7 +10602,12 @@ function runAsk(opts) {
10301
10602
  }
10302
10603
  const top = opts.top ?? 8;
10303
10604
  const rootDir = resolveProjectRoot({ project: process.cwd() });
10304
- const hits = searchSymbolsFts(opts.question, top, void 0, rootDir);
10605
+ const fetched = searchSymbolsFts(opts.question, top + 1, void 0, rootDir);
10606
+ const moreBeyondTop = fetched.length > top;
10607
+ const hits = fetched.slice(0, top);
10608
+ if (moreBeyondTop) {
10609
+ emitErr2(`Answer is grounded in the top ${countNoun(top, "match", "matches")}; more matched (raise --top to widen the evidence).`);
10610
+ }
10305
10611
  const BACKEND_ENV = "TOKEN_GOAT_ASK_BACKEND";
10306
10612
  const backendLabel = process.env[BACKEND_ENV] ?? "";
10307
10613
  const entries = hits.map((h) => ({ file: h.filePath, symbol: h.name, kind: h.kind, line: h.lineStart, readCmd: `token-goat read "${h.filePath}::${h.name}@${h.lineStart}"` }));
@@ -10505,10 +10811,10 @@ function resolveHandoff(name, opts = {}) {
10505
10811
  }
10506
10812
 
10507
10813
  export {
10508
- getTrackedFiles,
10509
10814
  extractSection,
10510
10815
  readSection,
10511
10816
  listSections,
10817
+ buildImportGraph,
10512
10818
  ALL_SYMBOLS_IN_FILE_LIMIT,
10513
10819
  enclosingSymbol,
10514
10820
  runCallers,