token-goat 2.8.3 → 2.8.6

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,9 +23,11 @@ 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,
30
32
  indexFileSync,
31
33
  isAvailable,
@@ -39,6 +41,7 @@ import {
39
41
  locatePdfPages,
40
42
  mergeNearbyHits,
41
43
  ocrImage,
44
+ ocrIntegrityFailed,
42
45
  parseWhereSpecs,
43
46
  pathEqClause,
44
47
  probeImageMeta,
@@ -48,13 +51,14 @@ import {
48
51
  queryRefs,
49
52
  queryRefsByContext,
50
53
  querySymbols,
51
- scanForInjectionPatterns,
54
+ scanAndRecord,
52
55
  searchEvidenceSemantically,
53
56
  searchSemantic,
54
57
  searchSymbolsFts,
55
58
  shrinkImage,
56
59
  storeBlob,
57
60
  stripLeadingAttributes,
61
+ stripTomlComment,
58
62
  tomlBracketDelta,
59
63
  trimToBudget,
60
64
  unzipBounded,
@@ -62,7 +66,7 @@ import {
62
66
  walkProject,
63
67
  yamlLineClosesQuote,
64
68
  yamlOpenQuoteAfter
65
- } from "./token-goat-chunk-PWVXXPCC.mjs";
69
+ } from "./token-goat-chunk-DK4VLLYB.mjs";
66
70
  import {
67
71
  Database,
68
72
  PER_FILE_COUNTERFACTUAL_CEILING,
@@ -82,6 +86,7 @@ import {
82
86
  excludeTestsHiddenNote,
83
87
  extractErrorMessage,
84
88
  fileIsAbsent,
89
+ filtersFilteredToEmptyNotice,
85
90
  findHtmlHeadingMatches,
86
91
  findProject,
87
92
  fingerprintContent,
@@ -107,6 +112,7 @@ import {
107
112
  resolveIndexPath,
108
113
  resolveProjectRoot,
109
114
  runGit,
115
+ savedTokensFromBytes,
110
116
  shortFingerprint,
111
117
  stripAnsi,
112
118
  stripBom,
@@ -114,7 +120,7 @@ import {
114
120
  unsupportedLanguageName,
115
121
  windowsCmdQuoteArg,
116
122
  withExtension
117
- } from "./token-goat-chunk-6ODZ6PZK.mjs";
123
+ } from "./token-goat-chunk-2JZ66BBE.mjs";
118
124
  import {
119
125
  registerReset
120
126
  } from "./token-goat-chunk-AO2QD2AG.mjs";
@@ -198,18 +204,19 @@ function findTableHeaders(lines, isToml) {
198
204
  const restStart = closeIdx + openDelim.length;
199
205
  const m2 = TABLE_HEADER_RE.exec(line.slice(restStart));
200
206
  if (m2 !== null && m2[1] !== void 0) headers.push({ heading: m2[1].trim(), level: 1, index: i });
201
- openDelim = lineOpenDelimiterAfter(line, restStart);
207
+ openDelim = lineOpenDelimiterAfter(stripTomlComment(line.slice(restStart)), 0);
202
208
  continue;
203
209
  }
204
210
  if (isToml && arrayDepth > 0) {
205
- arrayDepth = Math.max(0, arrayDepth + tomlBracketDelta(line));
211
+ arrayDepth = Math.max(0, arrayDepth + tomlBracketDelta(stripTomlComment(line)));
206
212
  continue;
207
213
  }
208
214
  const m = TABLE_HEADER_RE.exec(line);
209
215
  if (m !== null && m[1] !== void 0) headers.push({ heading: m[1].trim(), level: 1, index: i });
210
216
  if (isToml) {
211
- openDelim = lineOpenDelimiterAfter(line, 0);
212
- if (openDelim === null) arrayDepth = Math.max(0, tomlBracketDelta(line));
217
+ const code = stripTomlComment(line);
218
+ openDelim = lineOpenDelimiterAfter(code, 0);
219
+ if (openDelim === null) arrayDepth = Math.max(0, tomlBracketDelta(code));
213
220
  }
214
221
  }
215
222
  return headers;
@@ -255,10 +262,11 @@ function findKeyValueHeaders(lines, language) {
255
262
  if (closed) openQuote = null;
256
263
  continue;
257
264
  }
258
- const m = (isEnv ? ENV_KEYVALUE_HEADER_RE : KEYVALUE_HEADER_RE).exec(line);
265
+ const scanned = isEnv ? line.replace(/^[ \t]+/, "") : line;
266
+ const m = (isEnv ? ENV_KEYVALUE_HEADER_RE : KEYVALUE_HEADER_RE).exec(scanned);
259
267
  if (m === null || m[1] === void 0) continue;
260
268
  headers.push({ heading: m[1], level: 1, index: i });
261
- openQuote = isEnv ? _detectOpenQuote(line.slice(m[0].length)) : yamlOpenQuoteAfter(line, m[0].length);
269
+ openQuote = isEnv ? _detectOpenQuote(scanned.slice(m[0].length)) : yamlOpenQuoteAfter(line, m[0].length);
262
270
  }
263
271
  return headers;
264
272
  }
@@ -5347,15 +5355,12 @@ function guardText(text, command) {
5347
5355
  const cfg = loadConfig();
5348
5356
  return cfg.overflow_guard.enabled ? trimToBudget(text, cfg.overflow_guard.max_tokens, command) : text;
5349
5357
  }
5350
- function fenceGithubTextIfMatched(text) {
5351
- let matches = [];
5352
- try {
5353
- if (loadConfig().injection.enabled) matches = scanForInjectionPatterns(text);
5354
- } catch {
5355
- matches = [];
5356
- }
5358
+ function fenceGithubText(text) {
5359
+ return fenceUntrusted(text, UNTRUSTED_GITHUB_TAG);
5360
+ }
5361
+ function fenceGithubFieldIfMatched(text) {
5362
+ const matches = scanAndRecord(text);
5357
5363
  if (matches.length === 0) return text;
5358
- recordStat("injection_detected", 0, 0, void 0, matches.join(","));
5359
5364
  return fenceUntrustedContent(text, matches, UNTRUSTED_GITHUB_TAG);
5360
5365
  }
5361
5366
  function guardJsonRows(items) {
@@ -5376,7 +5381,7 @@ function sumFileSizes(filePaths) {
5376
5381
  function recordReadStat(kind, fullSourceBytes, emittedText, detail) {
5377
5382
  const emittedBytes = Buffer.byteLength(emittedText, "utf8");
5378
5383
  const bytesSaved = Math.max(1, fullSourceBytes - emittedBytes);
5379
- recordStat(kind, bytesSaved, Math.round(bytesSaved / 4), void 0, detail);
5384
+ recordStat(kind, bytesSaved, savedTokensFromBytes(bytesSaved), void 0, detail);
5380
5385
  }
5381
5386
  function findSpecSeparator(spec) {
5382
5387
  return spec.lastIndexOf("::");
@@ -5659,10 +5664,21 @@ function runSymbol(opts) {
5659
5664
  if (opts.name !== void 0 && emptyIndexRoot !== null) {
5660
5665
  const rootDir = emptyIndexRoot;
5661
5666
  const rawSymbols = querySymbols({ limit: FIND_SCAN_LIMIT, rootDir });
5662
- const candidates = rankSimilarNames(rawSymbols.map((s) => s.name), opts.name);
5663
- text2 += candidates.length > 0 ? `
5667
+ const exactMatches = rawSymbols.filter((s) => s.name === opts.name);
5668
+ if (exactMatches.length > 0) {
5669
+ const shown = exactMatches.slice(0, DIDYOUMEAN_LIMIT);
5670
+ const where = shown.map((s) => `${s.kind} at ${toDisplayPath(rootDir, s.filePath)}:${s.lineStart}`).join("; ");
5671
+ const more = exactMatches.length > shown.length ? ` (+${exactMatches.length - shown.length} more)` : "";
5672
+ const flags = [opts.kind !== void 0 ? "--kind" : null, opts.file !== void 0 ? "--file" : null].filter((f) => f !== null);
5673
+ const widen = flags.length > 0 ? `drop ${flags.join("/")} to see it` : "widen the search scope to see it";
5674
+ text2 += `
5675
+ '${opts.name}' IS indexed (${where}${more}) -- ${widen}`;
5676
+ } else {
5677
+ const candidates = rankSimilarNames(rawSymbols.map((s) => s.name), opts.name);
5678
+ text2 += candidates.length > 0 ? `
5664
5679
  ${didYouMean(candidates)}` : indexEmpty ? "" : `
5665
5680
  Try: token-goat semantic "${opts.name}"`;
5681
+ }
5666
5682
  const structuredFiles = [...new Set(rawSymbols.map((s) => s.filePath))].sort();
5667
5683
  const hit = findStructuredKeyPath(opts.name, structuredFiles);
5668
5684
  if (hit !== null) {
@@ -5936,9 +5952,11 @@ function resolveSymbolSpec(spec, forceRefresh, projectRoot) {
5936
5952
  let candidates = querySymbols({ name: lookupName, filePath: resolved, limit: 10 });
5937
5953
  if (candidates.length === 0) {
5938
5954
  const foldedFile = foldPath(file);
5955
+ const baseName = file.slice(Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\")) + 1);
5939
5956
  candidates = querySymbols({
5940
5957
  name: lookupName,
5941
5958
  limit: 50,
5959
+ ...baseName !== "" ? { fileBaseName: baseName } : {},
5942
5960
  ...projectRoot !== void 0 ? { rootDir: projectRoot } : {}
5943
5961
  }).filter((s) => {
5944
5962
  const foldedFilePath = foldPath(s.filePath);
@@ -6072,6 +6090,12 @@ ${sub.text}`);
6072
6090
  function resolveAgainstProjectRoot(file, projectRoot) {
6073
6091
  return projectRoot !== void 0 && !path5.isAbsolute(file) ? path5.resolve(projectRoot, file) : file;
6074
6092
  }
6093
+ function literalHeadingExists(filePath, heading) {
6094
+ const ordinalMatch = /^([^#\r\n]+)#(\d+)$/.exec(heading);
6095
+ const base = (ordinalMatch?.[1] ?? heading).trim().toLowerCase();
6096
+ if (base.length === 0) return false;
6097
+ return listSections(filePath, readFileText).some((h) => h.trim().toLowerCase() === base);
6098
+ }
6075
6099
  function runSection(opts) {
6076
6100
  const crossFilePairs = parseCrossFileMultiSpec(opts.spec);
6077
6101
  if (crossFilePairs !== null) return runSectionCrossFile(crossFilePairs, opts);
@@ -6082,7 +6106,7 @@ function runSection(opts) {
6082
6106
  const specFilePath = opts.spec.slice(0, colonIdx);
6083
6107
  const filePath = resolveAgainstProjectRoot(specFilePath, opts.projectRoot);
6084
6108
  const heading = opts.spec.slice(colonIdx + 2);
6085
- if (heading.includes(",")) {
6109
+ if (heading.includes(",") && !literalHeadingExists(filePath, heading)) {
6086
6110
  const multiHeadings = heading.split(",").map((h) => h.trim()).filter((h) => h.length > 0);
6087
6111
  if (multiHeadings.length > 1) return runSectionMulti(specFilePath, filePath, multiHeadings, opts);
6088
6112
  }
@@ -6516,14 +6540,10 @@ function formatStatsSuffix(refCounts, sym) {
6516
6540
  return refCounts !== void 0 ? ` [${countNoun(refCounts.get(sym.name) ?? 0, "ref")}, ${hasRealDocstring(sym.docstring) ? "documented" : "undocumented"}]` : "";
6517
6541
  }
6518
6542
  function filteredToEmptyNotice(preFilterCount, minLines, grep) {
6519
- const plural = preFilterCount === 1 ? "symbol" : "symbols";
6520
6543
  const parts = [];
6521
6544
  if (minLines !== void 0) parts.push(`--min-lines ${minLines}`);
6522
6545
  if (grep !== void 0) parts.push(`--grep ${grep}`);
6523
- const cause = parts.length === 0 ? "the active filter" : parts.join(" + ");
6524
- const knob = parts.length > 1 ? "filters" : "filter";
6525
- const verb = preFilterCount === 1 ? "was" : "were";
6526
- return ` (all ${preFilterCount} indexed ${plural} ${verb} filtered out by ${cause}; the file is indexed -- widen or drop the ${knob} to see them)`;
6546
+ return filtersFilteredToEmptyNotice(preFilterCount, parts, "indexed symbol", "indexed symbols", "the file is indexed");
6527
6547
  }
6528
6548
  function prepareSymbolListing(file, opts) {
6529
6549
  const resolved = resolveIndexPath(file, opts.projectRoot ?? process.cwd());
@@ -6632,6 +6652,12 @@ function runSkeleton(opts) {
6632
6652
  recordReadStat("stub_view", fullSourceBytes, text, opts.file);
6633
6653
  return { text, code: 0 };
6634
6654
  }
6655
+ var DOC_SUMMARY_MAX_CHARS = 140;
6656
+ function clipDocSummary(firstLine) {
6657
+ if (firstLine.length <= DOC_SUMMARY_MAX_CHARS) return firstLine;
6658
+ const cut = firstLine.lastIndexOf(" ", DOC_SUMMARY_MAX_CHARS);
6659
+ return `${firstLine.slice(0, cut > 40 ? cut : DOC_SUMMARY_MAX_CHARS).trimEnd()}\u2026`;
6660
+ }
6635
6661
  function runOutline(opts) {
6636
6662
  const multiFiles = parseMultiFileSpec(opts.file);
6637
6663
  if (multiFiles !== null) return runPerFileListing(multiFiles, (file) => runOutline({ ...opts, file, includeFilePath: true }), opts.json === true);
@@ -6669,7 +6695,7 @@ function runOutline(opts) {
6669
6695
  const rangeStr = `${sym.lineStart.toString().padStart(4)}-${sym.lineEnd.toString().padEnd(6)}`;
6670
6696
  const kindStr = sym.kind.padEnd(14);
6671
6697
  const bodyLen = sym.lineEnd - sym.lineStart + 1;
6672
- const docFirst = hasRealDocstring(sym.docstring) ? ` # ${sym.docstring.split("\n")[0] ?? ""}` : "";
6698
+ const docFirst = hasRealDocstring(sym.docstring) ? ` # ${clipDocSummary(sym.docstring.split("\n")[0] ?? "")}` : "";
6673
6699
  const statsStr = formatStatsSuffix(refCounts, sym);
6674
6700
  lines.push(` ${rangeStr} ${kindStr} ${sym.name} (${bodyLen}\u2113)${docFirst}${statsStr}`);
6675
6701
  }
@@ -6709,11 +6735,11 @@ function runCsvQuery(opts) {
6709
6735
  const rowsJson = result.rows.map((r) => Object.fromEntries(result.header.map((h, i) => [h, r[i]])));
6710
6736
  const headTruncated = result.rows.length < result.totalRows;
6711
6737
  const capped = guardJsonRows(rowsJson);
6712
- const jsonText = JSON.stringify({ items: capped.items, truncated: capped.truncated || headTruncated, totalCount: result.totalRows });
6738
+ const jsonText = JSON.stringify({ items: capped.items, truncated: capped.truncated || headTruncated, totalCount: result.totalRows, ...result.totalRows === 0 && result.preFilterRows > 0 ? { filteredFromRows: result.preFilterRows } : {} });
6713
6739
  emit(jsonText);
6714
6740
  recordReadStat("csv_query", fullSourceBytes, jsonText, opts.file);
6715
6741
  } else {
6716
- const tableText = formatCsvTable(result);
6742
+ const tableText = formatCsvTable(result, (opts.where ?? []).map((w) => `--where ${w}`));
6717
6743
  emit(tableText);
6718
6744
  recordReadStat("csv_query", fullSourceBytes, tableText, opts.file);
6719
6745
  }
@@ -6856,6 +6882,7 @@ function resolveYamlMergeKeys(node) {
6856
6882
  }
6857
6883
  function parseYamlDocument(text) {
6858
6884
  const docs = loadAll(text).map(resolveYamlMergeKeys);
6885
+ if (docs.length === 0) return null;
6859
6886
  return docs.length === 1 ? docs[0] : docs;
6860
6887
  }
6861
6888
  function runYamlOutline(opts) {
@@ -7189,11 +7216,11 @@ function runPrSlice(opts) {
7189
7216
  }
7190
7217
  const fullSourceBytes = Buffer.byteLength(diffText, "utf8");
7191
7218
  if (opts.json === true) {
7192
- const jsonText = JSON.stringify({ path: parsed.path, diff: fenceGithubTextIfMatched(fileDiff) });
7219
+ const jsonText = JSON.stringify({ path: parsed.path, diff: fenceGithubFieldIfMatched(fileDiff) });
7193
7220
  emit(jsonText);
7194
7221
  recordReadStat("pr_slice", fullSourceBytes, jsonText, `${repo}#${opts.pr} diff:${parsed.path}`);
7195
7222
  } else {
7196
- emitGuarded(fenceGithubTextIfMatched(fileDiff), "pr-slice");
7223
+ emitGuarded(fenceGithubText(fileDiff), "pr-slice");
7197
7224
  recordReadStat("pr_slice", fullSourceBytes, fileDiff, `${repo}#${opts.pr} diff:${parsed.path}`);
7198
7225
  }
7199
7226
  return 0;
@@ -7202,14 +7229,14 @@ function runPrSlice(opts) {
7202
7229
  const comments = fetchPrComments(opts.pr, repo);
7203
7230
  const fullSourceBytes = Buffer.byteLength(JSON.stringify(comments), "utf8");
7204
7231
  if (opts.json === true) {
7205
- const fencedComments = comments.map((c) => ({ ...c, body: fenceGithubTextIfMatched(c.body) }));
7232
+ const fencedComments = comments.map((c) => ({ ...c, body: fenceGithubFieldIfMatched(c.body) }));
7206
7233
  const capped = guardJsonRows(fencedComments);
7207
7234
  const jsonText = JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount });
7208
7235
  emit(jsonText);
7209
7236
  recordReadStat("pr_slice", fullSourceBytes, jsonText, `${repo}#${opts.pr} comments`);
7210
7237
  } else {
7211
7238
  const text = formatCommentsSlice(comments);
7212
- emitGuarded(fenceGithubTextIfMatched(text), "pr-slice");
7239
+ emitGuarded(fenceGithubText(text), "pr-slice");
7213
7240
  recordReadStat("pr_slice", fullSourceBytes, text, `${repo}#${opts.pr} comments`);
7214
7241
  }
7215
7242
  return 0;
@@ -7220,15 +7247,15 @@ function runPrSlice(opts) {
7220
7247
  if (opts.json === true) {
7221
7248
  const fencedDesc = {
7222
7249
  ...desc,
7223
- title: fenceGithubTextIfMatched(desc.title),
7224
- body: desc.body !== null ? fenceGithubTextIfMatched(desc.body) : null
7250
+ title: fenceGithubFieldIfMatched(desc.title),
7251
+ body: desc.body !== null ? fenceGithubFieldIfMatched(desc.body) : null
7225
7252
  };
7226
7253
  const jsonText = JSON.stringify(fencedDesc);
7227
7254
  emit(jsonText);
7228
7255
  recordReadStat("pr_slice", fullSourceBytes, jsonText, `${repo}#${opts.pr} description`);
7229
7256
  } else {
7230
7257
  const text = formatDescriptionSlice(desc);
7231
- emitGuarded(fenceGithubTextIfMatched(text), "pr-slice");
7258
+ emitGuarded(fenceGithubText(text), "pr-slice");
7232
7259
  recordReadStat("pr_slice", fullSourceBytes, text, `${repo}#${opts.pr} description`);
7233
7260
  }
7234
7261
  return 0;
@@ -7439,6 +7466,9 @@ async function runImageText(file) {
7439
7466
  const data = fs5.readFileSync(file);
7440
7467
  const ocr = await ocrImage(data);
7441
7468
  if (ocr === null) {
7469
+ if (ocrIntegrityFailed()) {
7470
+ throw new Error(`${file} was not OCRed: the cached language model failed its checksum and was discarded, so the next run re-downloads it`);
7471
+ }
7442
7472
  if (isOcrEngineAvailable()) {
7443
7473
  throw new Error(`${file} could not be processed by OCR (unreadable image, timeout, or offline model fetch)`);
7444
7474
  }
@@ -7489,15 +7519,14 @@ ${emptyIndexMessage(rootDir2)}`, code: 1 };
7489
7519
  const match = resolution.entry;
7490
7520
  const rootDir = resolveProjectRoot({ project: opts.projectRoot ?? process.cwd() });
7491
7521
  const excludeTests = opts.excludeTests === true;
7492
- const unboundedQuery = excludeTests || opts.grep !== void 0;
7493
- const allCallers = resolveCallers(match.name, void 0, match.filePath, rootDir, unboundedQuery);
7522
+ const allCallers = resolveCallers(match.name, void 0, match.filePath, rootDir, true);
7494
7523
  const testFiltered = excludeTests ? allCallers.filter((c) => !isTestFile(c.file)) : allCallers;
7495
7524
  const hiddenByExcludeTests = excludeTests ? allCallers.length - testFiltered.length : 0;
7496
7525
  const preGrepCount = testFiltered.length;
7497
7526
  const matchesGrep = opts.grep !== void 0 ? compileGrepMatcher(opts.grep) : void 0;
7498
7527
  const callers = matchesGrep !== void 0 ? testFiltered.filter((c) => matchesGrep(c.caller)) : testFiltered;
7499
7528
  const hiddenByGrep = matchesGrep !== void 0 ? preGrepCount - callers.length : 0;
7500
- const totalCallers = unboundedQuery ? callers.length : queryRefCounts([match.name], globalDbPath(), rootDir).get(match.name) ?? callers.length;
7529
+ const totalCallers = callers.length;
7501
7530
  const section = findContainingSection(match.filePath, match.lineStart, match.lineEnd, readFileText);
7502
7531
  const limit = opts.limit ?? 20;
7503
7532
  const shown = callers.slice(0, limit);
@@ -7643,7 +7672,7 @@ function runFind(opts) {
7643
7672
  }
7644
7673
  if (opts.json === true) {
7645
7674
  const fuzzyPayload = fuzzyNames.length > 0 ? { fuzzy: true, matchedNames: fuzzyNames } : {};
7646
- emit(JSON.stringify({ files, truncated, ...fuzzyPayload }, null, 2));
7675
+ emit(JSON.stringify({ files, truncated, totalCount: allFiles.length, ...fuzzyPayload }, null, 2));
7647
7676
  return 0;
7648
7677
  }
7649
7678
  if (fuzzyNames.length > 0) {
@@ -8913,6 +8942,18 @@ async function runSemantic(query, opts) {
8913
8942
  `Matching on meaning is off (${extractErrorMessage(e)}); these results come from keyword search alone.`
8914
8943
  );
8915
8944
  }
8945
+ if (rawHits.length === 0 && isAvailable()) {
8946
+ try {
8947
+ const enabled = loadConfig().indexing.embeddings_enabled;
8948
+ const { indexedFiles, embeddedFiles } = enabled ? getEmbeddingCoverage(globalDbPath(), rootDir) : { indexedFiles: 0, embeddedFiles: 0 };
8949
+ if (indexedFiles > 0 && embeddedFiles < indexedFiles) {
8950
+ console.warn(
8951
+ `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.`
8952
+ );
8953
+ }
8954
+ } catch {
8955
+ }
8956
+ }
8916
8957
  const mergedHits = mergeNearbyHits(rawHits);
8917
8958
  const overFetchFts = Math.min(MAX_OVER_FETCH, n * OVER_FETCH_FACTOR);
8918
8959
  const ftsRows = searchSymbolsFts(query, overFetchFts, void 0, rootDir);
@@ -8959,7 +9000,9 @@ async function runSemantic(query, opts) {
8959
9000
  const anyFilter = matchesGrep !== void 0 || opts.excludeTests === true;
8960
9001
  const filteredHits = anyFilter ? fusedHits.filter(keepHit) : fusedHits;
8961
9002
  const suppressedTotal = opts.excludeTests === true ? fusedHits.filter((h) => (matchesGrep === void 0 || matchesGrep(toDisplayPath(rootDir, h.filePath))) && isTestFile(h.filePath)).length : 0;
9003
+ const eligibleCount = filteredHits.length;
8962
9004
  const hits = filteredHits.slice(0, n);
9005
+ const candidatesClipped = rawHits.length >= overFetchForMerge || ftsRows.length >= overFetchFts;
8963
9006
  const hadDense = mergedHits.length > 0;
8964
9007
  const hadFts = ftsRows.length > 0;
8965
9008
  const source = hadDense && hadFts ? "hybrid" : hadDense ? "embeddings" : "fts";
@@ -8975,7 +9018,8 @@ async function runSemantic(query, opts) {
8975
9018
  preview: previewLines(h.previewText, 3)
8976
9019
  }));
8977
9020
  const capped = guardJsonRows(items);
8978
- const text3 = JSON.stringify({ source, ...capped }, null, 2);
9021
+ const limitTruncated = hits.length < eligibleCount;
9022
+ const text3 = JSON.stringify({ source, ...capped, truncated: capped.truncated || limitTruncated, totalCount: eligibleCount, ...candidatesClipped ? { totalCountIsFloor: true } : {} }, null, 2);
8979
9023
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text3, query);
8980
9024
  return { text: text3, code: 0 };
8981
9025
  }
@@ -8989,6 +9033,9 @@ ${previewLines(h.previewText, 3)}`;
8989
9033
  ${previewLines(h.previewText, 3)}`;
8990
9034
  });
8991
9035
  const text2 = guardText(blocks.join("\n\n"), "semantic");
9036
+ if (hits.length < eligibleCount) {
9037
+ emitErr(`Showing ${hits.length} of ${candidatesClipped ? "at least " : ""}${countNoun(eligibleCount, "match", "matches")} (raise --limit to see the rest).`);
9038
+ }
8992
9039
  recordReadStat("semantic_search", sumFileSizes(hits.map((h) => h.filePath)), text2, query);
8993
9040
  return { text: text2, code: 0 };
8994
9041
  }
@@ -9146,10 +9193,14 @@ function bfsCallChains(start, callersOf, maxDepth) {
9146
9193
  const tip = chain[chain.length - 1];
9147
9194
  if (tip === void 0) continue;
9148
9195
  const callers = callersOf(tip);
9149
- if (callers.length === 0 || chain.length > maxDepth) {
9196
+ if (callers.length === 0) {
9150
9197
  complete.push(chain);
9151
9198
  continue;
9152
9199
  }
9200
+ if (chain.length > maxDepth) {
9201
+ complete.push([...chain, "(depth-limit)"]);
9202
+ continue;
9203
+ }
9153
9204
  let expanded = false;
9154
9205
  for (const caller of callers) {
9155
9206
  if (chain.includes(caller)) {
@@ -9235,8 +9286,11 @@ function runCallers(opts) {
9235
9286
  emitErr2(`Symbol '${name}' not found in '${file}'`);
9236
9287
  return 1;
9237
9288
  }
9238
- const unbounded = opts.excludeTests === true || opts.grep !== void 0;
9239
- const resolved = resolveCallers(name, opts.limit, fileHint, rootDir, unbounded);
9289
+ const unbounded = opts.excludeTests === true || opts.grep !== void 0 || fileHint !== void 0;
9290
+ const requestedLimit = opts.limit ?? DEFAULT_REF_QUERY_LIMIT;
9291
+ const probed = resolveCallers(name, unbounded ? opts.limit : requestedLimit + 1, fileHint, rootDir, unbounded);
9292
+ const sqlTruncated = !unbounded && probed.length > requestedLimit;
9293
+ const resolved = sqlTruncated ? probed.slice(0, requestedLimit) : probed;
9240
9294
  const suppressed = opts.excludeTests === true ? resolved.filter((e) => isTestFile(e.file)).length : 0;
9241
9295
  let filtered = opts.excludeTests === true ? resolved.filter((e) => !isTestFile(e.file)) : resolved;
9242
9296
  const preGrepCount = filtered.length;
@@ -9277,7 +9331,7 @@ function runCallers(opts) {
9277
9331
  return { ...e, file: displayPath, filePath: displayPath };
9278
9332
  });
9279
9333
  const capped = guardJsonRows(rows);
9280
- const limitTruncated = entries.length < filtered.length;
9334
+ const limitTruncated = entries.length < filtered.length || sqlTruncated;
9281
9335
  const hiddenByGrep = preGrepCount - filtered.length;
9282
9336
  emit2(JSON.stringify({ items: capped.items, truncated: capped.truncated || limitTruncated, totalCount: filtered.length, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...opts.excludeTests === true && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {} }, null, 2));
9283
9337
  return 0;
@@ -9316,7 +9370,7 @@ function runCallChain(opts) {
9316
9370
  const getSyms = buildFileSymCache();
9317
9371
  let suppressedCount = 0;
9318
9372
  const callersOf = (n) => {
9319
- const refs = queryRefs({ name: n, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
9373
+ const refs = queryRefs({ name: n, limit: UNBOUNDED_REF_LIMIT, rootDir });
9320
9374
  if (refs.length === 0) return [];
9321
9375
  const scoped = fileHint !== void 0 && n === name ? filterRefsForSymbol(refs, n, fileHint, getSyms) : refs;
9322
9376
  const names = /* @__PURE__ */ new Set();
@@ -9379,12 +9433,16 @@ function runImpact(opts) {
9379
9433
  const hops = /* @__PURE__ */ new Map([[rootName, 0]]);
9380
9434
  const queue = [[rootName, 0]];
9381
9435
  let suppressedCount = 0;
9436
+ let depthCapped = 0;
9382
9437
  while (queue.length > 0) {
9383
9438
  const item = queue.shift();
9384
9439
  if (item === void 0) break;
9385
9440
  const [name, depth] = item;
9386
- if (depth >= DEPTH_CAP) continue;
9387
- const refs = queryRefs({ name, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
9441
+ if (depth >= DEPTH_CAP) {
9442
+ depthCapped += 1;
9443
+ continue;
9444
+ }
9445
+ const refs = queryRefs({ name, limit: UNBOUNDED_REF_LIMIT, rootDir });
9388
9446
  const scoped = fileHint !== void 0 && name === rootName ? filterRefsForSymbol(refs, name, fileHint, getSyms) : refs;
9389
9447
  for (const ref of scoped) {
9390
9448
  if (opts.excludeTests === true && isTestFile(ref.filePath)) {
@@ -9412,6 +9470,12 @@ function runImpact(opts) {
9412
9470
  const matchesGrep = opts.grep !== void 0 ? compileGrepMatcher(opts.grep) : void 0;
9413
9471
  const grepped = matchesGrep !== void 0 ? allSorted.filter(([symbol]) => matchesGrep(symbol)) : allSorted;
9414
9472
  const sorted = grepped.slice(0, top);
9473
+ if (depthCapped > 0) {
9474
+ emitErr2(`Impact truncated at the ${DEPTH_CAP}-hop depth limit: ${depthCapped} symbol${depthCapped === 1 ? "" : "s"} at that depth ${depthCapped === 1 ? "was" : "were"} not expanded, so callers reachable only beyond ${DEPTH_CAP} hops are missing.`);
9475
+ }
9476
+ if (grepped.length > sorted.length) {
9477
+ emitErr2(`Showing top ${sorted.length} of ${grepped.length} impacted symbols (raise --top to see the rest).`);
9478
+ }
9415
9479
  if (matchesGrep !== void 0 && sorted.length === 0 && allSorted.length > 0) {
9416
9480
  if (opts.json === true) {
9417
9481
  emitErr2(grepFilteredToEmptyNotice(allSorted.length, opts.grep, "impacted symbol", "impacted symbols"));
@@ -9541,14 +9605,17 @@ function runDead(opts) {
9541
9605
  }
9542
9606
  return 1;
9543
9607
  }
9544
- const syms = kinds.flatMap((k) => querySymbols({ kind: k, limit: 5e3, rootDir }));
9608
+ const syms = kinds.flatMap((k) => querySymbols({ kind: k, limit: UNBOUNDED_REF_LIMIT, rootDir }));
9545
9609
  const getSyms = buildFileSymCache();
9546
9610
  const results = [];
9547
9611
  let suppressed = 0;
9548
9612
  for (const sym of syms) {
9549
9613
  if (opts.includePrivate !== true && sym.name.startsWith("_")) continue;
9550
9614
  const refs = queryRefs({ name: sym.name, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
9551
- const scoped = filterRefsForSymbol(refs, sym.name, sym.filePath, getSyms);
9615
+ let scoped = filterRefsForSymbol(refs, sym.name, sym.filePath, getSyms);
9616
+ if (scoped.length === 0 && refs.length >= DEFAULT_REF_QUERY_LIMIT) {
9617
+ scoped = filterRefsForSymbol(queryRefs({ name: sym.name, limit: UNBOUNDED_REF_LIMIT, rootDir }), sym.name, sym.filePath, getSyms);
9618
+ }
9552
9619
  if (!isDeadSymbol(sym.name, scoped.length)) continue;
9553
9620
  if (sym.kind === "method") {
9554
9621
  const ownScope = enclosingNamedScope(getSyms(sym.filePath), sym.lineStart);
@@ -9769,7 +9836,7 @@ var TYPE_KINDS = [
9769
9836
  var CORE_SYMBOL_KINDS = [
9770
9837
  .../* @__PURE__ */ new Set([...GENERIC_SYMBOL_KINDS, ...TYPE_KINDS, ...OTHER_ADAPTER_SYMBOL_KINDS])
9771
9838
  ];
9772
- var TYPES_EXCLUDE_TESTS_SCAN_LIMIT = 5e3;
9839
+ var TYPES_SCAN_LIMIT = 5e3;
9773
9840
  function runTypes(opts) {
9774
9841
  if (opts.limit !== void 0 && opts.limit <= 0) {
9775
9842
  emitErr2(`--limit must be a positive number, got: ${opts.limit}`);
@@ -9782,34 +9849,25 @@ function runTypes(opts) {
9782
9849
  const rootDir = resolveProjectRoot({ project: process.cwd() });
9783
9850
  const results = [];
9784
9851
  let suppressed = 0;
9785
- const queryLimit = excludeTests ? TYPES_EXCLUDE_TESTS_SCAN_LIMIT : limit;
9786
9852
  for (const k of TYPE_KINDS) {
9787
- const syms = querySymbols({ kind: k, ...fpOpt, limit: queryLimit, rootDir });
9788
- if (!excludeTests) {
9789
- results.push(...syms);
9790
- continue;
9791
- }
9792
- const kept = [];
9853
+ const syms = querySymbols({ kind: k, ...fpOpt, limit: TYPES_SCAN_LIMIT, rootDir });
9793
9854
  for (const sym of syms) {
9794
- if (isTestFile(sym.filePath)) {
9855
+ if (excludeTests && isTestFile(sym.filePath)) {
9795
9856
  suppressed += 1;
9796
9857
  continue;
9797
9858
  }
9798
- kept.push(sym);
9859
+ results.push(sym);
9799
9860
  }
9800
- results.push(...kept.slice(0, limit));
9801
9861
  }
9802
- const classes = querySymbols({ kind: "class", ...fpOpt, limit: queryLimit, rootDir });
9803
- const typeClasses = [];
9862
+ const classes = querySymbols({ kind: "class", ...fpOpt, limit: TYPES_SCAN_LIMIT, rootDir });
9804
9863
  for (const cls of classes) {
9805
9864
  if (!looksLikeTypeClass(cls.body)) continue;
9806
9865
  if (excludeTests && isTestFile(cls.filePath)) {
9807
9866
  suppressed += 1;
9808
9867
  continue;
9809
9868
  }
9810
- typeClasses.push(cls);
9869
+ results.push(cls);
9811
9870
  }
9812
- results.push(...excludeTests ? typeClasses.slice(0, limit) : typeClasses);
9813
9871
  results.sort(
9814
9872
  // Pin the locale: an unlocaled localeCompare() sorts differently across
9815
9873
  // Node's small-icu vs full-icu builds and different system default
@@ -9836,7 +9894,16 @@ function runTypes(opts) {
9836
9894
  }
9837
9895
  const preFilterCount = results.length;
9838
9896
  const matchesGrep = opts.grep !== void 0 ? compileGrepMatcher(opts.grep) : void 0;
9839
- const filtered = matchesGrep !== void 0 ? results.filter((r) => matchesGrep(r.name)) : results;
9897
+ const matched = matchesGrep !== void 0 ? results.filter((r) => matchesGrep(r.name)) : results;
9898
+ const eligibleCount = matched.length;
9899
+ const perKindShown = /* @__PURE__ */ new Map();
9900
+ const filtered = matched.filter((r) => {
9901
+ const seen = perKindShown.get(r.kind) ?? 0;
9902
+ if (seen >= limit) return false;
9903
+ perKindShown.set(r.kind, seen + 1);
9904
+ return true;
9905
+ });
9906
+ const cappedOut = eligibleCount - filtered.length;
9840
9907
  if (filtered.length === 0) {
9841
9908
  if (opts.json === true) {
9842
9909
  emit2(JSON.stringify({ items: [], truncated: false, totalCount: 0, hiddenByGrep: preFilterCount }, null, 2));
@@ -9847,8 +9914,8 @@ function runTypes(opts) {
9847
9914
  }
9848
9915
  if (opts.json === true) {
9849
9916
  const capped = guardJsonRows(filtered.map((r) => ({ ...r, filePath: toDisplayPath(rootDir, r.filePath) })));
9850
- const hiddenByGrep = preFilterCount - filtered.length;
9851
- emit2(JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...excludeTests && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {} }, null, 2));
9917
+ const hiddenByGrep = preFilterCount - eligibleCount;
9918
+ emit2(JSON.stringify({ items: capped.items, truncated: capped.truncated || cappedOut > 0, totalCount: eligibleCount, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...excludeTests && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {} }, null, 2));
9852
9919
  return 0;
9853
9920
  }
9854
9921
  if (excludeTests && suppressed > 0) {
@@ -9857,6 +9924,9 @@ function runTypes(opts) {
9857
9924
  for (const r of filtered) {
9858
9925
  emit2(`${r.name} ${r.kind} ${toDisplayPath(rootDir, r.filePath)}:${r.lineStart}`);
9859
9926
  }
9927
+ if (cappedOut > 0) {
9928
+ emitErr2(`Showing ${filtered.length} of ${eligibleCount} type declarations (--limit is per kind; raise it to see the rest).`);
9929
+ }
9860
9930
  return 0;
9861
9931
  }
9862
9932
  function runScope(opts) {
@@ -9872,7 +9942,7 @@ function runScope(opts) {
9872
9942
  return 1;
9873
9943
  }
9874
9944
  const line = Number.parseInt(lineStr, 10);
9875
- if (!Number.isFinite(line) || line < 1) {
9945
+ if (!Number.isSafeInteger(line) || line < 1) {
9876
9946
  emitErr2(`Invalid line number: ${lineStr}`);
9877
9947
  return 1;
9878
9948
  }
@@ -9882,25 +9952,32 @@ function runScope(opts) {
9882
9952
  return 1;
9883
9953
  }
9884
9954
  const filePath = resolveIndexPath(file);
9885
- const syms = querySymbols({ filePath, limit: ALL_SYMBOLS_IN_FILE_LIMIT });
9886
- const enclosing = syms.filter((s) => s.lineStart <= line && line <= s.lineEnd).sort((a, b) => b.lineStart - a.lineStart);
9887
- if (syms.length === 0) {
9888
- if (!fs6.existsSync(filePath)) {
9889
- emitErr2(`Could not read: ${file}`);
9955
+ 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"));
9956
+ if (enclosing.length === 0) {
9957
+ if (querySymbols({ filePath, limit: 1 }).length === 0) {
9958
+ if (!fs6.existsSync(filePath)) {
9959
+ emitErr2(`Could not read: ${file}`);
9960
+ return 1;
9961
+ }
9962
+ emitErr2(
9963
+ `No indexed symbols in '${file}' \u2014 the file exists but nothing is indexed for it, so every line looks empty`
9964
+ );
9890
9965
  return 1;
9891
9966
  }
9892
- emitErr2(`No indexed symbols in '${file}' \u2014 the file exists but nothing is indexed for it, so every line looks empty`);
9893
- return 1;
9894
- }
9895
- if (enclosing.length === 0) {
9896
9967
  emitErr2(`No symbols enclosing line ${line} in '${file}'`);
9897
9968
  return 1;
9898
9969
  }
9970
+ const scopeDisplayRoot = getDisplayRoot();
9899
9971
  if (opts.json === true) {
9900
- emit2(JSON.stringify(enclosing, null, 2));
9972
+ emit2(
9973
+ JSON.stringify(
9974
+ enclosing.map((s) => ({ ...s, filePath: toDisplayPath(scopeDisplayRoot, s.filePath) })),
9975
+ null,
9976
+ 2
9977
+ )
9978
+ );
9901
9979
  return 0;
9902
9980
  }
9903
- const scopeDisplayRoot = getDisplayRoot();
9904
9981
  for (const s of enclosing) {
9905
9982
  emit2(`${s.name} ${s.kind} ${toDisplayPath(scopeDisplayRoot, s.filePath)}:${s.lineStart}-${s.lineEnd}`);
9906
9983
  }
@@ -9967,17 +10044,18 @@ function canonicalCycleKey(cyclePath) {
9967
10044
  }
9968
10045
  return [...nodes.slice(minIdx), ...nodes.slice(0, minIdx)].join(" ");
9969
10046
  }
9970
- function findCycles(graph) {
10047
+ function findCyclesCapped(graph) {
9971
10048
  const cycles = [];
9972
10049
  const seen = /* @__PURE__ */ new Set();
9973
10050
  const sccs = tarjanSCCs(graph);
10051
+ const probeLimit = MAX_CYCLES + 1;
9974
10052
  for (const component of sccs) {
9975
10053
  let dfs2 = function(start, node, pathSoFar) {
9976
- if (cycles.length >= MAX_CYCLES) return;
10054
+ if (cycles.length >= probeLimit) return;
9977
10055
  stack.add(node);
9978
10056
  for (const nb of graph.get(node) ?? []) {
9979
10057
  if (!sccSet.has(nb)) continue;
9980
- if (cycles.length >= MAX_CYCLES) break;
10058
+ if (cycles.length >= probeLimit) break;
9981
10059
  if (nb === start) {
9982
10060
  const cyclePath = [...pathSoFar, node, start];
9983
10061
  const key = canonicalCycleKey(cyclePath);
@@ -9992,6 +10070,7 @@ function findCycles(graph) {
9992
10070
  stack.delete(node);
9993
10071
  };
9994
10072
  var dfs = dfs2;
10073
+ if (cycles.length >= probeLimit) break;
9995
10074
  if (component.length === 1) {
9996
10075
  const [only] = component;
9997
10076
  if (only !== void 0 && (graph.get(only) ?? []).includes(only)) cycles.push([only, only]);
@@ -10000,11 +10079,12 @@ function findCycles(graph) {
10000
10079
  const sccSet = new Set(component);
10001
10080
  const stack = /* @__PURE__ */ new Set();
10002
10081
  for (const start of component) {
10003
- if (cycles.length >= MAX_CYCLES) break;
10082
+ if (cycles.length >= probeLimit) break;
10004
10083
  dfs2(start, start, []);
10005
10084
  }
10006
10085
  }
10007
- return cycles;
10086
+ if (cycles.length > MAX_CYCLES) return { cycles: cycles.slice(0, MAX_CYCLES), truncated: true };
10087
+ return { cycles, truncated: false };
10008
10088
  }
10009
10089
  function runSimilar(opts) {
10010
10090
  if (opts.top !== void 0 && opts.top <= 0) {
@@ -10022,8 +10102,12 @@ function runSimilar(opts) {
10022
10102
  const words = [anchor.name, ...(anchor.docstring ?? "").split(/\s+/).filter((w) => w.length > 4)];
10023
10103
  const query = words.slice(0, 8).join(" ");
10024
10104
  const rootDir = resolveProjectRoot({ project: process.cwd() });
10025
- const hits = searchSymbolsFts(query, top + 1, void 0, rootDir);
10026
- const results = hits.filter((h) => !(h.filePath === anchor.filePath && h.name === anchor.name)).slice(0, top);
10105
+ const hits = searchSymbolsFts(query, UNBOUNDED_REF_LIMIT, void 0, rootDir);
10106
+ const matches = hits.filter((h) => !(h.filePath === anchor.filePath && h.name === anchor.name));
10107
+ const results = matches.slice(0, top);
10108
+ if (matches.length > results.length) {
10109
+ emitErr2(`Showing top ${results.length} of ${matches.length} similar symbols (raise --top to see the rest).`);
10110
+ }
10027
10111
  if (opts.json === true) {
10028
10112
  emit2(JSON.stringify(results.map((h) => ({ name: h.name, kind: h.kind, file: h.filePath, line: h.lineStart })), null, 2));
10029
10113
  return 0;
@@ -10039,7 +10123,9 @@ function runContextFor(opts) {
10039
10123
  const top = opts.top ?? 12;
10040
10124
  const budget = opts.budget;
10041
10125
  const rootDir = resolveProjectRoot({ project: process.cwd() });
10042
- const hits = searchSymbolsFts(opts.task, top, void 0, rootDir);
10126
+ const fetched = searchSymbolsFts(opts.task, top + 1, void 0, rootDir);
10127
+ const moreBeyondTop = fetched.length > top;
10128
+ const hits = fetched.slice(0, top);
10043
10129
  if (hits.length === 0) {
10044
10130
  emitErr2(`No matches found for '${opts.task}'`);
10045
10131
  return 1;
@@ -10052,6 +10138,15 @@ function runContextFor(opts) {
10052
10138
  tokensSoFar += bodyTokens;
10053
10139
  entries.push({ file: h.filePath, symbol: h.name, kind: h.kind, line: h.lineStart, readCmd: `token-goat read "${h.filePath}::${h.name}@${h.lineStart}"` });
10054
10140
  }
10141
+ const skippedByBudget = hits.length - entries.length;
10142
+ const notices = [];
10143
+ if (skippedByBudget > 0) {
10144
+ notices.push(
10145
+ 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.`
10146
+ );
10147
+ }
10148
+ if (moreBeyondTop) notices.push(`More matches exist beyond --top ${top} (raise it to see them).`);
10149
+ for (const n of notices) emitErr2(n);
10055
10150
  if (opts.json === true) {
10056
10151
  emit2(JSON.stringify(entries, null, 2));
10057
10152
  return 0;
@@ -10108,8 +10203,8 @@ function runCoverageGaps(opts) {
10108
10203
  }
10109
10204
  const top = opts.top ?? 50;
10110
10205
  const rootDir = resolveProjectRoot({ project: process.cwd() });
10111
- const allFns = querySymbols({ kind: "function", limit: 2e3, rootDir });
10112
- const allMethods = querySymbols({ kind: "method", limit: 2e3, rootDir });
10206
+ const allFns = querySymbols({ kind: "function", limit: UNBOUNDED_REF_LIMIT, rootDir });
10207
+ const allMethods = querySymbols({ kind: "method", limit: UNBOUNDED_REF_LIMIT, rootDir });
10113
10208
  const candidates = [...allFns, ...allMethods];
10114
10209
  const gaps = [];
10115
10210
  for (const sym of candidates) {
@@ -10120,6 +10215,9 @@ function runCoverageGaps(opts) {
10120
10215
  if (!hasTestRef) gaps.push({ name: sym.name, kind: sym.kind, file: sym.filePath, line: sym.lineStart });
10121
10216
  }
10122
10217
  const sliced = gaps.slice(0, top);
10218
+ if (gaps.length > sliced.length) {
10219
+ emitErr2(`Showing top ${sliced.length} of ${gaps.length} coverage gaps (raise --top to see the rest).`);
10220
+ }
10123
10221
  if (opts.json === true) {
10124
10222
  emit2(JSON.stringify(sliced, null, 2));
10125
10223
  return 0;
@@ -10186,18 +10284,22 @@ function runArch(opts) {
10186
10284
  }
10187
10285
  graph.set(file, internal);
10188
10286
  }
10189
- const hubs = [...importedBy.entries()].sort((a, b) => b[1].size - a[1].size).slice(0, top).map(([f, importers]) => ({ file: f, importedBy: importers.size }));
10190
- const entryPoints = files.filter((f) => !importedBy.has(f) && (graph.get(f) ?? []).length > 0).slice(0, top).map((f) => ({ file: f }));
10191
- const cycles = findCycles(graph);
10287
+ const allHubs = [...importedBy.entries()].sort((a, b) => b[1].size - a[1].size).map(([f, importers]) => ({ file: f, importedBy: importers.size }));
10288
+ const hubsTotal = allHubs.length;
10289
+ const hubs = allHubs.slice(0, top);
10290
+ const allEntryPoints = files.filter((f) => !importedBy.has(f) && (graph.get(f) ?? []).length > 0).map((f) => ({ file: f }));
10291
+ const entryPointsTotal = allEntryPoints.length;
10292
+ const entryPoints = allEntryPoints.slice(0, top);
10293
+ const { cycles, truncated: cyclesTruncated } = findCyclesCapped(graph);
10192
10294
  if (opts.json === true) {
10193
- emit2(JSON.stringify({ hubs, entryPoints, cycles }, null, 2));
10295
+ emit2(JSON.stringify({ hubs, hubsTotal, hubsTruncated: hubs.length < hubsTotal, entryPoints, entryPointsTotal, entryPointsTruncated: entryPoints.length < entryPointsTotal, cycles, ...cyclesTruncated ? { cyclesTruncated: true } : {} }, null, 2));
10194
10296
  return 0;
10195
10297
  }
10196
- emit2(`hubs (top ${top} most-imported):`);
10298
+ emit2(hubs.length < hubsTotal ? `hubs (top ${hubs.length} of ${hubsTotal} most-imported):` : `hubs (${hubsTotal} most-imported):`);
10197
10299
  for (const h of hubs) emit2(` ${h.importedBy} importers ${toDisplayPath(getDisplayRoot(opts.cwd), h.file)}`);
10198
- emit2(`entry points (imported by nobody, top ${top}):`);
10300
+ emit2(entryPoints.length < entryPointsTotal ? `entry points (imported by nobody, top ${entryPoints.length} of ${entryPointsTotal}):` : `entry points (imported by nobody, ${entryPointsTotal} found):`);
10199
10301
  for (const e of entryPoints) emit2(` ${toDisplayPath(getDisplayRoot(opts.cwd), e.file)}`);
10200
- emit2(`cycles (${cycles.length} found):`);
10302
+ emit2(cyclesTruncated ? `cycles (first ${cycles.length}, truncated at the ${MAX_CYCLES}-cycle enumeration limit: more cycles exist):` : `cycles (${cycles.length} found):`);
10201
10303
  for (const c of cycles) emit2(` ${c.map((f) => toDisplayPath(getDisplayRoot(opts.cwd), f)).join(" -> ")}`);
10202
10304
  return 0;
10203
10305
  }
@@ -10245,21 +10347,30 @@ function runAsk(opts) {
10245
10347
  }
10246
10348
  const top = opts.top ?? 8;
10247
10349
  const rootDir = resolveProjectRoot({ project: process.cwd() });
10248
- const hits = searchSymbolsFts(opts.question, top, void 0, rootDir);
10350
+ const fetched = searchSymbolsFts(opts.question, top + 1, void 0, rootDir);
10351
+ const moreBeyondTop = fetched.length > top;
10352
+ const hits = fetched.slice(0, top);
10353
+ if (moreBeyondTop) {
10354
+ emitErr2(`Answer is grounded in the top ${countNoun(top, "match", "matches")}; more matched (raise --top to widen the evidence).`);
10355
+ }
10249
10356
  const BACKEND_ENV = "TOKEN_GOAT_ASK_BACKEND";
10250
10357
  const backendLabel = process.env[BACKEND_ENV] ?? "";
10251
10358
  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}"` }));
10252
- const degrade = () => {
10359
+ const degrade = (reason, extraNote) => {
10253
10360
  if (opts.json === true) {
10254
- emit2(JSON.stringify({ degraded: true, note: `Set ${BACKEND_ENV}=claude|codex for LLM synthesis`, context: entries }, null, 2));
10361
+ emit2(JSON.stringify({ degraded: true, note: reason, ...extraNote !== void 0 ? { hint: extraNote } : {}, context: entries }, null, 2));
10255
10362
  return 0;
10256
10363
  }
10257
- emit2(`[degraded mode - set ${BACKEND_ENV}=claude|codex for LLM synthesis]`);
10364
+ emit2(`[degraded mode - ${reason}]`);
10365
+ if (extraNote !== void 0) emit2(extraNote);
10258
10366
  for (const e of entries) emit2(`token-goat read "${toDisplayPath(rootDir, e.file)}::${e.symbol}@${e.line}"`);
10259
10367
  return 0;
10260
10368
  };
10261
- if (!backendLabel) return degrade();
10262
- if (hits.length === 0) return degrade();
10369
+ if (!backendLabel) return degrade(`set ${BACKEND_ENV}=claude|codex for LLM synthesis`);
10370
+ if (hits.length === 0) {
10371
+ if (isIndexEmptyForProject(globalDbPath(), rootDir)) return degrade(`${BACKEND_ENV}=${backendLabel} is set, but nothing is indexed for this project yet, so there is no context to answer from`, emptyIndexMessage(rootDir));
10372
+ return degrade(`${BACKEND_ENV}=${backendLabel} is set, but no indexed symbol matched this question, so there is no context to answer from -- try different wording or token-goat semantic`);
10373
+ }
10263
10374
  const isWin = process.platform === "win32";
10264
10375
  let backendPath = null;
10265
10376
  try {
@@ -10268,7 +10379,7 @@ function runAsk(opts) {
10268
10379
  if (found) backendPath = found;
10269
10380
  } catch {
10270
10381
  }
10271
- if (!backendPath) return degrade();
10382
+ if (!backendPath) return degrade(`${BACKEND_ENV}=${backendLabel} is set, but '${backendLabel}' was not found on PATH`);
10272
10383
  const context = hits.map((h, i) => `[${i + 1}] ${h.filePath}
10273
10384
  ${h.body ?? ""}`).join("\n\n");
10274
10385
  const prompt = `Answer the QUESTION using only the CODE SNIPPETS below.
@@ -10319,7 +10430,7 @@ ANSWER:`;
10319
10430
  }
10320
10431
  }
10321
10432
  }
10322
- return degrade();
10433
+ return degrade(`${BACKEND_ENV}=${backendLabel} ran but returned no answer`);
10323
10434
  }
10324
10435
 
10325
10436
  // src/content_store.ts