token-goat 2.9.4 → 2.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ permalink: /
11
11
 
12
12
  ***Give the model what it needs, not everything you have.***
13
13
 
14
- **85%** smaller reads · **97.4%** image compression · **180+** filter & interception rules · **94–99%** skill overhead cut · compaction memory · **prompt injection** guard · **3.7 GB** never reached the model · **1.1 Gt** tokens saved
14
+ **85%** smaller reads · **49%** of first-read bytes withheld · **97.4%** image compression · **180+** filter & interception rules · **94–99%** skill overhead cut · compaction memory · **prompt injection** guard · **3.7 GB** never reached the model · **1.1 Gt** tokens saved
15
15
 
16
16
  **Reduces AI token use/costs by 40–90%, and improves its focus. Fully automated, always online.**
17
17
 
@@ -70,7 +70,8 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
70
70
  | 3.3 MB screenshot lands in model context | 84 KB compressed copy, 97.4% smaller |
71
71
  | Agent re-reads files from earlier in the session | "Already read this" reminder with narrow slice suggestion |
72
72
  | Read tool asks for lines the session was already given | Answered with a pointer at the copy already delivered instead of the file, when the text this read would return matches the text already served for it whole line for whole line. Proof rather than a read count, so it also covers a file inside the recent-read protection window; a changed file, a wider range, and `reread_deny = false` all pass through |
73
- | Same unchanged file read again through the shell (`cat`, `head`, `tail`, `sed -n '1,40p'`) | Byte-identical repeat replaced with a one-line pointer instead of the whole body — 4,031 bytes to 189 on this project. Only fires when the two runs match exactly, so a changed file is untouched; full text stays available via `token-goat bash-output <id>` |
73
+ | Same unchanged file read again through the shell (`cat`, `head`, `tail`, `sed -n '1,40p'`) | Byte-identical repeat replaced with a one-line pointer instead of the whole body: 4,031 bytes to 189 on this project. A later read wholly inside an earlier one collapses the same way |
74
+ | A shell read of a file overlaps an earlier one without sitting inside it (`sed -n '100,140p'` after `sed -n '120,160p'`) | Only the already-shown stretch is withheld, replaced in place by one line naming its line numbers and the command that returns it; every line not shown before arrives untouched. 56% of the second read on a real pair over this project's README. Line numbers come from the command's own ranges, so a `tail`, whose start the command does not state, is left alone. Full text stays available via `token-goat bash-output <id>` |
74
75
  | Part of a file re-read under a different command (`head -40 F` then `sed -n '1,30p' F`) | Repeat replaced with a pointer when every line coming back was already served for that same file this session. Matched on the text, on whole-line boundaries, so a read that adds any new lines is left whole; the record is dropped when the file is edited or the conversation is compacted. A file read with the Read tool counts as served too, limited to the lines that read actually handed over, and a read that came back truncated counts for nothing |
75
76
  | Read tool asks for a range that overlaps one already delivered | Only the overlapping stretch is withheld, replaced by one line naming the line numbers and the recall command; the new lines come through with their own numbers and spacing, byte for byte. A stretch is kept whenever dropping it would save less than the line replacing it costs, and a result carrying anything the redactor would strip, a truncated result, or a file changed on disk all pass through whole. Set `elide_served_lines = false` to disable |
76
77
  | Shell command run with colour on (`git`, `npm`, `npx`, `token-goat`) | Terminal escape sequences removed, leaving byte-for-byte the same output with colour off: 13.2% smaller on a real `git diff` here, and about a quarter of the bytes on the calls it fires for. Nothing is summarized or withheld, so there is no recall pointer; it runs on failed commands too, and only when the escapes are worth a rewrite |
@@ -114,7 +115,7 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
114
115
  | Index-only files (lockfiles, source maps, bundles) read on every session | Pre-Read suppression for read-only files (package-lock.json, *.map, dist/), skipped unless explicitly edited |
115
116
  | Large markdown file read in full (README.md, CHANGELOG.md, CLAUDE.md ≥8 KB) | Heading tree intercepted instead — H1–H3 with `#2`/`#3` disambiguation; `token-goat section` shortcuts listed for well-known files; post-edit injects a re-read suggestion rather than the full file |
116
117
  | Subagent reads a ≥30 KB markdown file whole, on its first look at it | Off by default. Set `subagent_markdown_first_read_deny = true` under `[hints]` to block that read and answer with the heading tree instead. Only fires in a subagent, only on a first, un-ranged read of a `.md`/`.mdx`/`.markdown` file with at least three headings; a read that already asks for a line range, and every main-session read, pass through untouched |
117
- | A source file is read whole for the first time, with no earlier copy to compare it against | Off by default. Set `fold_code_bodies = true` under `[hints]` to keep each function's declaration and its first eight lines, replacing the rest with one line naming the symbol, the exact range removed, and the command that returns it. Comment blocks of twelve lines or more fold to their first two on the same terms. Imports, signatures, type declarations, classes and interfaces are never folded, and a file whose index no longer matches what is on disk keeps its bodies. Measured on this repository's own source: 229 of 256 files fold, and 43% of what a read of them delivers is removed. A read that already names an offset or a limit, and any file holding something that looks like a secret, pass through untouched |
118
+ | A source file is read whole for the first time, with no earlier copy to compare it against | Keeps each function's declaration and its first eight lines, replacing the rest with one line naming the symbol, the exact range removed, and the command that returns it. Set `fold_code_bodies = false` under `[hints]` to turn it off. Comment blocks of twelve lines or more fold to their first two on the same terms. Imports, signatures, type declarations, classes and interfaces are never folded, and a file whose index no longer matches what is on disk keeps its bodies. Measured on this repository's own source: 229 of 256 files fold, and 43% of what a read of them delivers is removed. A read that already names an offset or a limit, and any file holding something that looks like a secret, pass through untouched |
118
119
  | PDF opened via Read | Full read denied; PDF shows page count and outline (`token-goat pdf-extract` pulls the actual text, optionally paged/sliced, when the outline isn't enough) |
119
120
  | Excel/PowerPoint/Word file (.xlsx/.pptx/.docx) opened via Read | Full read denied; redirects to the matching narrow-slice command family (`xlsx-sheets`/`xlsx-head`/`xlsx-range`/`xlsx-query`, `pptx-outline`/`pptx-slide`/`pptx-notes`/`pptx-text`, `docx-outline`/`docx-text`) instead of extracting the whole document as text |
120
121
  | Other Office binary (.odt, .ods, .ott, .odp) opened via Read | Full read denied; redirects to `pandoc` for text extraction (no dedicated reader for these formats yet) |
@@ -185,10 +186,12 @@ Returns relevance-ranked, distance-scored hits straight from the notes, the same
185
186
 
186
187
  ## Token savings, measured
187
188
 
188
- Numbers below come from synthetic-fixture benchmarks in the test suite. Each row points at the source file where the measurement is reproduced.
189
+ Numbers below come from synthetic-fixture benchmarks in the test suite, except the first two rows, which are measured on a corpus of real sessions and say so. Each row points at the source file where the measurement is reproduced.
189
190
 
190
191
  | Source | Improvement | Measured impact | Where |
191
192
  |--------|-------------|-----------------|-------|
193
+ | Large-document outline | A read naming no `offset` or `limit` on a markdown document over 8 KB with six or more headings returns the lead-in and heading tree, plus the `token-goat section` command that reads any section verbatim | Measured on 13,870 real reads across 5,104 sessions: withholds 40.4% of all bytes the Read tool delivers. A 104,890 B document returns 1,541 B with its opening paragraph intact. Sections sum to the document, so a reader who pulls every one back pays about 3% over reading it whole | `src/hooks_read.ts` (`foldMarkdownOutline`) |
194
+ | Large-source skeleton | A read naming no `offset` or `limit` on a source file over 12 KB with eight or more declarations returns the preamble and one line per declaration, each withheld run naming the `token-goat read "file::Symbol"` or ranged `Read` that returns it | Measured on the same corpus: withholds up to 8.6% of all bytes the Read tool delivers, a ceiling rather than a booked total, since the shipping gates are narrower than the measurement's. The skeleton is 10.5% of a `.ts` file. Median symbol body is 4.1% of its file, so a reader breaks even at roughly 22 bodies pulled back out of an average 30.8 | `src/hooks_read.ts` (`foldSourceSkeleton`) |
192
195
  | Image shrink | Every still is encoded both ways and the smaller file wins, rather than one codec being assumed | 2560x1440 screenshot-shaped PNG: 2,782,963 -> 259,475 bytes (91% smaller) | `src/image_shrink.ts` (codec selection) |
193
196
  | Repomap output | `--compact` trims the top-symbols list to 10 (vs 30) and drops the recent-files section and per-symbol locations | Denser overview for the same byte budget | `src/baseline.ts` (`buildProjectMap`, `token-goat map --compact`) |
194
197
  | DB reindex | Batched single transaction + composite indexes on `(file_id, kind)` | 100 files / 10K rows: 84 s → 1 s (~80× faster) | `src/parser.ts`, `src/db.ts` (index migration) |
@@ -7,11 +7,11 @@ import {
7
7
  expandGlobs,
8
8
  leftoverIntegrations,
9
9
  run
10
- } from "./token-goat-chunk-6MU6FHE3.mjs";
11
- import "./token-goat-chunk-US57I7GN.mjs";
12
- import "./token-goat-chunk-FSC44QOH.mjs";
13
- import "./token-goat-chunk-CBO45DYZ.mjs";
14
- import "./token-goat-chunk-3ZR4NLB3.mjs";
10
+ } from "./token-goat-chunk-5T2K7DEE.mjs";
11
+ import "./token-goat-chunk-4P6GTCMM.mjs";
12
+ import "./token-goat-chunk-JO5JX72D.mjs";
13
+ import "./token-goat-chunk-U4FTM2SB.mjs";
14
+ import "./token-goat-chunk-ZOKNDG6V.mjs";
15
15
  import "./token-goat-chunk-EEIDFMEM.mjs";
16
16
  import "./token-goat-chunk-A37V4PBF.mjs";
17
17
  export {
@@ -1,6 +1,7 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
+ DEFAULT_QUERY_LIMIT,
4
5
  IMPORT_RE,
5
6
  ImageDecodeError,
6
7
  MAX_OVER_FETCH,
@@ -25,6 +26,7 @@ import {
25
26
  extractPdfText,
26
27
  fenceUntrusted,
27
28
  fenceUntrustedContent,
29
+ fenceUntrustedFileContent,
28
30
  formatCsvProfile,
29
31
  formatCsvTable,
30
32
  getEmbeddingCoverage,
@@ -68,7 +70,7 @@ import {
68
70
  walkProject,
69
71
  yamlLineClosesQuote,
70
72
  yamlOpenQuoteAfter
71
- } from "./token-goat-chunk-CBO45DYZ.mjs";
73
+ } from "./token-goat-chunk-U4FTM2SB.mjs";
72
74
  import {
73
75
  Database,
74
76
  PER_FILE_COUNTERFACTUAL_CEILING,
@@ -122,7 +124,7 @@ import {
122
124
  unsupportedLanguageName,
123
125
  windowsCmdQuoteArg,
124
126
  withExtension
125
- } from "./token-goat-chunk-3ZR4NLB3.mjs";
127
+ } from "./token-goat-chunk-ZOKNDG6V.mjs";
126
128
  import {
127
129
  registerReset
128
130
  } from "./token-goat-chunk-EEIDFMEM.mjs";
@@ -458,6 +460,69 @@ import * as path6 from "node:path";
458
460
  import { execFileSync, spawnSync as spawnSync2 } from "node:child_process";
459
461
  import { randomUUID } from "node:crypto";
460
462
 
463
+ // src/ref_blindness.ts
464
+ init_define_import_meta_env();
465
+ var REF_INDEXED_LANGUAGES = /* @__PURE__ */ new Set([
466
+ "typescript",
467
+ "javascript",
468
+ "python",
469
+ "go",
470
+ "rust",
471
+ "java",
472
+ "c",
473
+ "cpp",
474
+ "ruby"
475
+ ]);
476
+ function isRefIndexedLanguage(language) {
477
+ return REF_INDEXED_LANGUAGES.has(language);
478
+ }
479
+ function isRefIndexedFile(filePath) {
480
+ return isRefIndexedLanguage(detectLanguage(filePath));
481
+ }
482
+ var LANGUAGE_LABELS = /* @__PURE__ */ new Map([
483
+ ["csharp", "C#"],
484
+ ["cpp", "C++"],
485
+ ["c", "C"],
486
+ ["php", "PHP"],
487
+ ["powershell", "PowerShell"],
488
+ ["sql", "SQL"],
489
+ ["graphql", "GraphQL"],
490
+ ["proto", "Protocol Buffers"],
491
+ ["html", "HTML"],
492
+ ["css", "CSS"],
493
+ ["toml", "TOML"],
494
+ ["json", "JSON"],
495
+ ["yaml", "YAML"],
496
+ ["ini", "INI"],
497
+ ["env_file", "env file"],
498
+ ["ipynb", "Jupyter notebook"],
499
+ ["salesforce_metadata", "Salesforce metadata"],
500
+ ["salesforce_markup", "Salesforce markup"],
501
+ ["unknown", "this file type"]
502
+ ]);
503
+ function languageLabel(language) {
504
+ return LANGUAGE_LABELS.get(language) ?? language;
505
+ }
506
+ function refBlindLanguageNotice(symbolName, language, displayPath) {
507
+ return `Cannot determine references for '${symbolName}': ${languageLabel(language)} call sites are not indexed (${displayPath}). This is a gap in token-goat's index, not evidence the symbol is unreferenced. Search the source directly instead, e.g. \`rg -n -w ${symbolName}\`.`;
508
+ }
509
+ var REF_BLIND_KIND_REASON = "the index records value-position references only (calls, `new`, macro invocations), never type annotations, so a type declaration has no ref rows however widely it is used";
510
+ var REF_BLIND_DEF_PROBE_LIMIT = 50;
511
+ function kindWithArticle(kind) {
512
+ return `${/^[aeiou]/.test(kind) ? "an" : "a"} ${kind}`;
513
+ }
514
+ function refBlindKindClause(symbolName, kinds) {
515
+ const first = kinds[0];
516
+ if (kinds.length === 1 && first !== void 0) return `'${symbolName}' is ${kindWithArticle(first)}`;
517
+ return `every definition of '${symbolName}' is a type declaration (${kinds.map((k) => `'${k}'`).join(", ")})`;
518
+ }
519
+ function refBlindKindNotice(symbolName, kinds) {
520
+ return `Cannot determine references for '${symbolName}': ${refBlindKindClause(symbolName, kinds)}, and ${REF_BLIND_KIND_REASON}. This is a gap in token-goat's index, not evidence the symbol is unreferenced. Search the source directly instead, e.g. \`rg -n -w ${symbolName}\`.`;
521
+ }
522
+ function refBlindKindPartialNote(symbolName, kinds, blindCount, totalCount) {
523
+ return `Note: ${blindCount} of ${totalCount} definitions of '${symbolName}' (${kinds.map((k) => `'${k}'`).join(", ")}) are not covered by this result -- ${REF_BLIND_KIND_REASON}. The empty result above speaks only for the other ${totalCount - blindCount}; for the rest, search the source directly, e.g. \`rg -n -w ${symbolName}\`.`;
524
+ }
525
+
461
526
  // src/read_commands.ts
462
527
  init_define_import_meta_env();
463
528
  import * as fs4 from "node:fs";
@@ -5435,6 +5500,15 @@ function guardJsonRows(items) {
5435
5500
  if (!cfg.overflow_guard.enabled) return { items: [...items], truncated: false, totalCount: items.length };
5436
5501
  return capJsonRows(items, cfg.overflow_guard.max_tokens);
5437
5502
  }
5503
+ function changedDiffBaselineBytes(cwd, ref, files) {
5504
+ if (files.length === 0) return 0;
5505
+ try {
5506
+ const result = runGit(["diff", ref, "--unified=0", "--", ...files], { cwd });
5507
+ if (result.exitCode === 0) return Buffer.byteLength(result.stdout, "utf8");
5508
+ } catch {
5509
+ }
5510
+ return 0;
5511
+ }
5438
5512
  function sumFileSizes(filePaths) {
5439
5513
  let total = 0;
5440
5514
  for (const fp of new Set(filePaths)) {
@@ -5822,9 +5896,9 @@ function truncationFooter(shown, limit, total, plural, flag) {
5822
5896
 
5823
5897
  token-goat: ${notice}`;
5824
5898
  }
5825
- function refsTotal(clientFiltered, filteredTotal, shown, countExact, preScanCount) {
5899
+ function refsTotal(clientFiltered, filteredTotal, shown, countExact, preScanCount, scanLimit) {
5826
5900
  if (!clientFiltered) return { count: countExact(), exact: true };
5827
- return { count: filteredTotal ?? shown, exact: preScanCount < REFS_TOP_SCAN_LIMIT };
5901
+ return { count: filteredTotal ?? shown, exact: preScanCount < scanLimit };
5828
5902
  }
5829
5903
  function truncationNotice(shown, limit, total, plural, flag) {
5830
5904
  if (shown < limit) return null;
@@ -6037,9 +6111,11 @@ function resolveSymbolSpec(spec, forceRefresh, projectRoot) {
6037
6111
  });
6038
6112
  }
6039
6113
  if (methodName !== void 0 && candidates.length > 1) {
6114
+ const containerBaseName = file.slice(Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\")) + 1);
6040
6115
  const containers = querySymbols({
6041
6116
  name: symBase,
6042
6117
  limit: 50,
6118
+ ...containerBaseName !== "" ? { fileBaseName: containerBaseName } : {},
6043
6119
  ...projectRoot !== void 0 ? { rootDir: projectRoot } : {}
6044
6120
  });
6045
6121
  const symBaseLower = symBase.toLowerCase();
@@ -6355,7 +6431,9 @@ function renderRefsTargets(targets, opts, {
6355
6431
  if (rootDir !== void 0) queryOpts.rootDir = rootDir;
6356
6432
  const scanned = queryRefs(queryOpts);
6357
6433
  const preScanCount = scanned.length;
6434
+ const scanLimit = queryOpts.limit ?? DEFAULT_QUERY_LIMIT;
6358
6435
  let results = applyTypedRefsTier(symbol, file, scanned);
6436
+ const typedFilterDropped = results.length < scanned.length;
6359
6437
  let suppressed = 0;
6360
6438
  if (opts.excludeTests === true) {
6361
6439
  const f = applyExcludeTestsFilter(results);
@@ -6365,9 +6443,10 @@ function renderRefsTargets(targets, opts, {
6365
6443
  const preGrepCount = results.length;
6366
6444
  const matchesGrep = refGrepFilter(opts.grep);
6367
6445
  if (matchesGrep !== void 0) results = results.filter(matchesGrep);
6446
+ const clientFiltered = opts.excludeTests === true || matchesGrep !== void 0 || typedFilterDropped;
6368
6447
  let filteredTotal;
6369
- if (opts.excludeTests === true || matchesGrep !== void 0) filteredTotal = results.length;
6370
- if ((opts.excludeTests === true || matchesGrep !== void 0) && opts.top === void 0) {
6448
+ if (clientFiltered) filteredTotal = results.length;
6449
+ if (clientFiltered && opts.top === void 0) {
6371
6450
  results = results.slice(0, opts.limit ?? 100);
6372
6451
  }
6373
6452
  if (results.length > 0) anyFound = true;
@@ -6379,7 +6458,7 @@ function renderRefsTargets(targets, opts, {
6379
6458
  jsonOut[key] = withHidden(topFilesJsonPayload(results, opts.top));
6380
6459
  } else {
6381
6460
  const capped = guardJsonRows(results);
6382
- const trueTotal = opts.excludeTests === true || matchesGrep !== void 0 ? filteredTotal ?? results.length : countRefs(queryOpts);
6461
+ const trueTotal = clientFiltered ? filteredTotal ?? results.length : countRefs(queryOpts);
6383
6462
  jsonOut[key] = withHidden({ items: refsJsonItems(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal });
6384
6463
  }
6385
6464
  continue;
@@ -6406,7 +6485,7 @@ function renderRefsTargets(targets, opts, {
6406
6485
  const notice = truncationNotice(
6407
6486
  results.length,
6408
6487
  opts.limit ?? 100,
6409
- () => refsTotal(opts.excludeTests === true || matchesGrep !== void 0, filteredTotal, results.length, () => countRefs(queryOpts), preScanCount),
6488
+ () => refsTotal(clientFiltered, filteredTotal, results.length, () => countRefs(queryOpts), preScanCount, scanLimit),
6410
6489
  "references",
6411
6490
  "--limit"
6412
6491
  );
@@ -6482,7 +6561,9 @@ function runRefsSingle(opts) {
6482
6561
  if (rootDir !== void 0) queryOpts.rootDir = rootDir;
6483
6562
  const scanned = queryRefs(queryOpts);
6484
6563
  const preScanCount = scanned.length;
6564
+ const scanLimit = queryOpts.limit ?? DEFAULT_QUERY_LIMIT;
6485
6565
  let results = applyTypedRefsTier(symName, defFileHint, scanned);
6566
+ const typedFilterDropped = results.length < scanned.length;
6486
6567
  let suppressed = 0;
6487
6568
  if (opts.excludeTests === true) {
6488
6569
  const f = applyExcludeTestsFilter(results);
@@ -6492,9 +6573,10 @@ function runRefsSingle(opts) {
6492
6573
  const preGrepCount = results.length;
6493
6574
  const matchesGrep = refGrepFilter(opts.grep);
6494
6575
  if (matchesGrep !== void 0) results = results.filter(matchesGrep);
6576
+ const clientFiltered = opts.excludeTests === true || matchesGrep !== void 0 || typedFilterDropped;
6495
6577
  let filteredTotal;
6496
- if (opts.excludeTests === true || matchesGrep !== void 0) filteredTotal = results.length;
6497
- if ((opts.excludeTests === true || matchesGrep !== void 0) && opts.top === void 0) {
6578
+ if (clientFiltered) filteredTotal = results.length;
6579
+ if (clientFiltered && opts.top === void 0) {
6498
6580
  results = results.slice(0, opts.limit ?? 100);
6499
6581
  }
6500
6582
  if (results.length === 0) {
@@ -6511,12 +6593,26 @@ function runRefsSingle(opts) {
6511
6593
  return 1;
6512
6594
  }
6513
6595
  const rootDir2 = opts.projectRoot ?? resolveProjectRoot({ project: process.cwd() });
6514
- if (querySymbols({ name: symName, rootDir: rootDir2, limit: 1 }).length === 0) {
6596
+ const defRows = querySymbols({ name: symName, rootDir: rootDir2, limit: REF_BLIND_DEF_PROBE_LIMIT });
6597
+ if (defRows.length === 0) {
6515
6598
  emitErr(`Symbol not found: ${symName}${unknownSymbolSuggestion(symName, rootDir2)}`);
6516
6599
  if (opts.json !== true && isIndexEmptyForProject(globalDbPath(), rootDir2)) emitErr(emptyIndexMessage(rootDir2));
6517
6600
  return 1;
6518
6601
  }
6602
+ const defPaths = defFileHint !== void 0 ? [defFileHint] : defRows.map((r) => r.filePath);
6603
+ const firstDefPath = defPaths[0];
6604
+ if (firstDefPath !== void 0 && defPaths.every((fp) => !isRefIndexedFile(fp))) {
6605
+ emitErr(refBlindLanguageNotice(symName, detectLanguage(firstDefPath), refsDisplayPath(firstDefPath)));
6606
+ return 1;
6607
+ }
6608
+ const kindRows = defFileHint !== void 0 ? querySymbols({ name: symName, filePath: defFileHint, rootDir: rootDir2, limit: REF_BLIND_DEF_PROBE_LIMIT }) : defRows;
6609
+ const kindVerdict = refBlindKindVerdict(kindRows);
6610
+ if (kindVerdict.allBlind) {
6611
+ emitErr(refBlindKindNotice(symName, kindVerdict.blindKinds));
6612
+ return 1;
6613
+ }
6519
6614
  emitErr(`No references found for '${symName}'`);
6615
+ if (kindVerdict.blindCount > 0) emitErr(refBlindKindPartialNote(symName, kindVerdict.blindKinds, kindVerdict.blindCount, kindRows.length));
6520
6616
  if (opts.json !== true) {
6521
6617
  if (isIndexEmptyForProject(globalDbPath(), rootDir2)) emitErr(emptyIndexMessage(rootDir2));
6522
6618
  }
@@ -6529,7 +6625,7 @@ function runRefsSingle(opts) {
6529
6625
  payload = topFilesJsonPayload(results, opts.top);
6530
6626
  } else {
6531
6627
  const capped = guardJsonRows(results);
6532
- const trueTotal = opts.excludeTests === true || matchesGrep !== void 0 ? filteredTotal ?? results.length : countRefs(queryOpts);
6628
+ const trueTotal = clientFiltered ? filteredTotal ?? results.length : countRefs(queryOpts);
6533
6629
  payload = { items: refsJsonItems(capped.items, opts.context ?? 0), truncated: capped.truncated || trueTotal > results.length, totalCount: trueTotal };
6534
6630
  }
6535
6631
  const hiddenByGrep = matchesGrep !== void 0 ? preGrepCount - (filteredTotal ?? results.length) : 0;
@@ -6539,7 +6635,7 @@ function runRefsSingle(opts) {
6539
6635
  return 0;
6540
6636
  }
6541
6637
  const lines = opts.top !== void 0 ? renderTopFilesSummary(results, opts.top, suppressed) : opts.callers === true ? [...opts.excludeTests === true && suppressed > 0 ? [`${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`] : [], ...renderCallerGroups(results, opts.context ?? 0)] : [...opts.excludeTests === true && suppressed > 0 ? [`${countNoun(results.length, "reference")} (${excludeTestsHiddenNote(suppressed)})`] : [], ...results.flatMap((ref) => renderRefLines(ref, opts.context ?? 0, ""))];
6542
- const refsFooter = opts.top !== void 0 ? "" : truncationFooter(results.length, opts.limit ?? 100, () => refsTotal(opts.excludeTests === true || matchesGrep !== void 0, filteredTotal, results.length, () => countRefs(queryOpts), preScanCount), "references", "--limit");
6638
+ const refsFooter = opts.top !== void 0 ? "" : truncationFooter(results.length, opts.limit ?? 100, () => refsTotal(clientFiltered, filteredTotal, results.length, () => countRefs(queryOpts), preScanCount, scanLimit), "references", "--limit");
6543
6639
  const text = lines.join("\n");
6544
6640
  emit(guardText(text, "symbol") + refsFooter);
6545
6641
  recordReadStat("symbol_read", fullSourceBytes, text + refsFooter, symName);
@@ -6726,7 +6822,23 @@ function runSkeleton(opts) {
6726
6822
  return { text, code: 0 };
6727
6823
  }
6728
6824
  var DOC_SUMMARY_MAX_CHARS = 140;
6825
+ function firstSentenceEnd(line) {
6826
+ const MIN_SENTENCE_CHARS = 30;
6827
+ const ABBREV = new RegExp("(?:\\b(?:e\\.g|i\\.e|vs|cf|etc|approx|al|Dr|Mr|Ms|St|Fig|No)\\.|\\b\\p{L}\\.)$", "u");
6828
+ for (const m of line.matchAll(/[.!?](?=\s|$)/gu)) {
6829
+ const end = m.index + 1;
6830
+ if (end < MIN_SENTENCE_CHARS) continue;
6831
+ const head = line.slice(0, end);
6832
+ if (ABBREV.test(head)) continue;
6833
+ return end;
6834
+ }
6835
+ return null;
6836
+ }
6729
6837
  function clipDocSummary(firstLine) {
6838
+ const sentence = firstSentenceEnd(firstLine);
6839
+ if (sentence !== null && sentence <= DOC_SUMMARY_MAX_CHARS) {
6840
+ return sentence === firstLine.length ? firstLine : `${firstLine.slice(0, sentence)}\u2026`;
6841
+ }
6730
6842
  if (firstLine.length <= DOC_SUMMARY_MAX_CHARS) return firstLine;
6731
6843
  const cut = firstLine.lastIndexOf(" ", DOC_SUMMARY_MAX_CHARS);
6732
6844
  return `${firstLine.slice(0, cut > 40 ? cut : DOC_SUMMARY_MAX_CHARS).trimEnd()}\u2026`;
@@ -7930,10 +8042,12 @@ function runChanged(opts = {}) {
7930
8042
  }
7931
8043
  if (opts.symbolMode === true) {
7932
8044
  let hunksByFile = /* @__PURE__ */ new Map();
8045
+ let symbolDiffBaselineBytes = 0;
7933
8046
  try {
7934
- const diffResult = runGit(["diff", ref, "--unified=0"], { cwd });
8047
+ const diffResult = runGit(["diff", ref, "--unified=0", "--", ...changedFiles], { cwd });
7935
8048
  if (diffResult.exitCode === 0) {
7936
8049
  hunksByFile = parseDiffHunks(diffResult.stdout);
8050
+ symbolDiffBaselineBytes = Buffer.byteLength(diffResult.stdout, "utf8");
7937
8051
  }
7938
8052
  } catch {
7939
8053
  }
@@ -7949,7 +8063,7 @@ function runChanged(opts = {}) {
7949
8063
  emit("No symbols changed.");
7950
8064
  return 0;
7951
8065
  }
7952
- const symbolFullBytes = sumFileSizes(changedFiles.map((f) => resolveIndexPath(f, projectRoot)));
8066
+ const symbolFullBytes = symbolDiffBaselineBytes;
7953
8067
  if (opts.json === true) {
7954
8068
  const capped = guardJsonRows(allSymbols);
7955
8069
  const text = JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount }, null, 2);
@@ -7964,7 +8078,7 @@ function runChanged(opts = {}) {
7964
8078
  recordReadStat("changed_lookup", symbolFullBytes, symbolText, ref);
7965
8079
  return 0;
7966
8080
  }
7967
- const fullBytes = sumFileSizes(changedFiles.map((f) => resolveIndexPath(f, projectRoot)));
8081
+ const fullBytes = changedDiffBaselineBytes(cwd, ref, changedFiles);
7968
8082
  if (opts.json === true) {
7969
8083
  const capped = guardJsonRows(changedFiles);
7970
8084
  const text = JSON.stringify({ items: capped.items, truncated: capped.truncated, totalCount: capped.totalCount }, null, 2);
@@ -9701,12 +9815,26 @@ function runCallers(opts) {
9701
9815
  emitErr2(`No non-test references found for '${opts.symbol}' (${excludeTestsHiddenNote(suppressed)})`);
9702
9816
  return 1;
9703
9817
  }
9704
- if (querySymbols({ name, rootDir, limit: 1 }).length === 0) {
9818
+ const defRows = querySymbols({ name, rootDir, limit: REF_BLIND_DEF_PROBE_LIMIT });
9819
+ if (defRows.length === 0) {
9705
9820
  emitErr2(`Symbol not found: ${opts.symbol}${unknownSymbolSuggestion(name, rootDir)}`);
9706
9821
  if (opts.json !== true && isIndexEmptyForProject(globalDbPath(), rootDir)) emitErr2(emptyIndexMessage(rootDir));
9707
9822
  return 1;
9708
9823
  }
9824
+ const defPaths = fileHint !== void 0 ? [fileHint] : defRows.map((r) => r.filePath);
9825
+ const firstDefPath = defPaths[0];
9826
+ if (firstDefPath !== void 0 && defPaths.every((fp) => !isRefIndexedFile(fp))) {
9827
+ emitErr2(refBlindLanguageNotice(name, detectLanguage(firstDefPath), toDisplayPath(rootDir, firstDefPath)));
9828
+ return 1;
9829
+ }
9830
+ const kindRows = fileHint !== void 0 ? querySymbols({ name, filePath: fileHint, limit: REF_BLIND_DEF_PROBE_LIMIT }) : defRows;
9831
+ const kindVerdict = refBlindKindVerdict(kindRows);
9832
+ if (kindVerdict.allBlind) {
9833
+ emitErr2(refBlindKindNotice(name, kindVerdict.blindKinds));
9834
+ return 1;
9835
+ }
9709
9836
  emitErr2(`No references found for '${opts.symbol}'`);
9837
+ if (kindVerdict.blindCount > 0) emitErr2(refBlindKindPartialNote(name, kindVerdict.blindKinds, kindVerdict.blindCount, kindRows.length));
9710
9838
  if (opts.json !== true && isIndexEmptyForProject(globalDbPath(), rootDir)) emitErr2(emptyIndexMessage(rootDir));
9711
9839
  return 1;
9712
9840
  }
@@ -9754,6 +9882,14 @@ function runCallChain(opts) {
9754
9882
  if (opts.json !== true && isIndexEmptyForProject(globalDbPath(), rootDir)) emitErr2(emptyIndexMessage(rootDir));
9755
9883
  return 1;
9756
9884
  }
9885
+ const refBlindRootPath = () => {
9886
+ const paths = fileHint !== void 0 ? [fileHint] : querySymbols({ name, rootDir, limit: REF_BLIND_DEF_PROBE_LIMIT }).map((r) => r.filePath);
9887
+ return paths.length > 0 && paths.every((fp) => !isRefIndexedFile(fp)) ? paths[0] : void 0;
9888
+ };
9889
+ const refBlindRootKinds = () => {
9890
+ const rows = fileHint !== void 0 ? querySymbols({ name, filePath: fileHint, limit: REF_BLIND_DEF_PROBE_LIMIT }) : querySymbols({ name, rootDir, limit: REF_BLIND_DEF_PROBE_LIMIT });
9891
+ return { ...refBlindKindVerdict(rows), total: rows.length };
9892
+ };
9757
9893
  const getSyms = buildFileSymCache();
9758
9894
  let suppressedCount = 0;
9759
9895
  const callersOf = (n) => {
@@ -9778,7 +9914,10 @@ function runCallChain(opts) {
9778
9914
  if (opts.json === true) {
9779
9915
  const hiddenByGrep = chains.length - filteredChains.length;
9780
9916
  const hiddenByExcludeTests = noCallers && opts.excludeTests === true ? suppressedCount : 0;
9781
- emit2(JSON.stringify({ chains: filteredChains, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...hiddenByExcludeTests > 0 ? { hiddenByExcludeTests } : {} }, null, 2));
9917
+ const refBlindKinds = noCallers ? refBlindRootKinds().blindKinds : [];
9918
+ const blindRootPath = noCallers ? refBlindRootPath() : void 0;
9919
+ const refBlindLanguage = blindRootPath !== void 0 ? { language: detectLanguage(blindRootPath), definedIn: toDisplayPath(rootDir, blindRootPath) } : void 0;
9920
+ emit2(JSON.stringify({ chains: filteredChains, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...hiddenByExcludeTests > 0 ? { hiddenByExcludeTests } : {}, ...refBlindKinds.length > 0 ? { refBlindKinds } : {}, ...refBlindLanguage !== void 0 ? { refBlindLanguage } : {} }, null, 2));
9782
9921
  return 0;
9783
9922
  }
9784
9923
  if (noCallers) {
@@ -9786,7 +9925,20 @@ function runCallChain(opts) {
9786
9925
  emit2(`${name} (no non-test callers; ${excludeTestsHiddenNote(suppressedCount)})`);
9787
9926
  return 0;
9788
9927
  }
9928
+ const blindRoot = refBlindRootPath();
9929
+ if (blindRoot !== void 0) {
9930
+ emitErr2(refBlindLanguageNotice(name, detectLanguage(blindRoot), toDisplayPath(rootDir, blindRoot)));
9931
+ emit2(`${name} (no callers recorded)`);
9932
+ return 0;
9933
+ }
9934
+ const rootKinds = refBlindRootKinds();
9935
+ if (rootKinds.allBlind) {
9936
+ emitErr2(refBlindKindNotice(name, rootKinds.blindKinds));
9937
+ emit2(`${name} (no callers recorded)`);
9938
+ return 0;
9939
+ }
9789
9940
  emit2(`${name} (no callers)`);
9941
+ if (rootKinds.blindCount > 0) emitErr2(refBlindKindPartialNote(name, rootKinds.blindKinds, rootKinds.blindCount, rootKinds.total));
9790
9942
  return 0;
9791
9943
  }
9792
9944
  if (matchesGrep !== void 0 && filteredChains.length === 0) {
@@ -9881,11 +10033,25 @@ function runImpact(opts) {
9881
10033
  emitErr2(`No non-test impact found for '${opts.symbol}' (${excludeTestsHiddenNote(suppressedCount)})`);
9882
10034
  return 1;
9883
10035
  }
9884
- if (querySymbols({ name: rootName, rootDir, limit: 1 }).length === 0) {
10036
+ const defRows = querySymbols({ name: rootName, rootDir, limit: REF_BLIND_DEF_PROBE_LIMIT });
10037
+ if (defRows.length === 0) {
9885
10038
  emitErr2(`Symbol not found: ${opts.symbol}${unknownSymbolSuggestion(rootName, rootDir)}`);
9886
10039
  return 1;
9887
10040
  }
10041
+ const defPaths = fileHint !== void 0 ? [fileHint] : defRows.map((r) => r.filePath);
10042
+ const firstDefPath = defPaths[0];
10043
+ if (firstDefPath !== void 0 && defPaths.every((fp) => !isRefIndexedFile(fp))) {
10044
+ emitErr2(refBlindLanguageNotice(rootName, detectLanguage(firstDefPath), toDisplayPath(rootDir, firstDefPath)));
10045
+ return 1;
10046
+ }
10047
+ const kindRows = fileHint !== void 0 ? querySymbols({ name: rootName, filePath: fileHint, limit: REF_BLIND_DEF_PROBE_LIMIT }) : defRows;
10048
+ const kindVerdict = refBlindKindVerdict(kindRows);
10049
+ if (kindVerdict.allBlind) {
10050
+ emitErr2(refBlindKindNotice(rootName, kindVerdict.blindKinds));
10051
+ return 1;
10052
+ }
9888
10053
  emitErr2(`No callers found for '${opts.symbol}'`);
10054
+ if (kindVerdict.blindCount > 0) emitErr2(refBlindKindPartialNote(rootName, kindVerdict.blindKinds, kindVerdict.blindCount, kindRows.length));
9889
10055
  return 1;
9890
10056
  }
9891
10057
  if (opts.json === true) {
@@ -9918,7 +10084,10 @@ function hasAncestorDispatchRef(methodName, scopeName, classFile, rootDir) {
9918
10084
  const baseSyms = querySymbols({ name: baseName, kind: "class", limit: 1, rootDir });
9919
10085
  const baseSym = baseSyms[0];
9920
10086
  if (baseSym === void 0) return false;
9921
- const baseRefs = queryRefs({ name: methodName, filePath: baseSym.filePath, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
10087
+ let baseRefs = queryRefs({ name: methodName, filePath: baseSym.filePath, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
10088
+ if (baseRefs.length >= DEFAULT_REF_QUERY_LIMIT && !baseRefs.some((ref) => ref.line >= baseSym.lineStart && ref.line <= baseSym.lineEnd)) {
10089
+ baseRefs = queryRefs({ name: methodName, filePath: baseSym.filePath, limit: UNBOUNDED_REF_LIMIT, rootDir });
10090
+ }
9922
10091
  if (baseRefs.some((ref) => ref.line >= baseSym.lineStart && ref.line <= baseSym.lineEnd)) return true;
9923
10092
  currentName = baseName;
9924
10093
  currentFile = baseSym.filePath;
@@ -9992,11 +10161,26 @@ function runDead(opts) {
9992
10161
  }
9993
10162
  return 1;
9994
10163
  }
9995
- const syms = kinds.flatMap((k) => querySymbols({ kind: k, limit: UNBOUNDED_REF_LIMIT, rootDir }));
10164
+ const blindKinds = kinds.filter((k) => REF_BLIND_KINDS.includes(k));
10165
+ const assessableKinds = kinds.filter((k) => !REF_BLIND_KINDS.includes(k));
10166
+ if (blindKinds.length > 0 && assessableKinds.length === 0) {
10167
+ const blindLabel = blindKinds.length === 1 ? "kind" : "kinds";
10168
+ emitErr2(`Cannot assess deadness for ${blindLabel}: ${blindKinds.map((k) => `'${k}'`).join(", ")} -- ${REF_BLIND_KIND_REASON}.`);
10169
+ emitErr2(`Every symbol of ${blindKinds.length === 1 ? "this kind" : "these kinds"} would be reported dead, so no result is emitted rather than a wrong one. To hunt unused type declarations, list them with 'token-goat types --json' and search the source for each name directly.`);
10170
+ return 1;
10171
+ }
10172
+ const blindKindNote = blindKinds.length > 0 ? `Note: ${blindKinds.map((k) => `'${k}'`).join(", ")} excluded -- ${REF_BLIND_KIND_REASON}.` : void 0;
10173
+ if (blindKindNote !== void 0 && opts.json !== true) emitErr2(blindKindNote);
10174
+ const syms = assessableKinds.flatMap((k) => querySymbols({ kind: k, limit: UNBOUNDED_REF_LIMIT, rootDir }));
9996
10175
  const getSyms = buildFileSymCache();
9997
10176
  const results = [];
9998
10177
  let suppressed = 0;
10178
+ let refBlindByLanguage = 0;
9999
10179
  for (const sym of syms) {
10180
+ if (!isRefIndexedFile(sym.filePath)) {
10181
+ refBlindByLanguage += 1;
10182
+ continue;
10183
+ }
10000
10184
  if (opts.includePrivate !== true && sym.name.startsWith("_")) continue;
10001
10185
  const refs = queryRefs({ name: sym.name, limit: DEFAULT_REF_QUERY_LIMIT, rootDir });
10002
10186
  let scoped = filterRefsForSymbol(refs, sym.name, sym.filePath, getSyms);
@@ -10014,6 +10198,9 @@ function runDead(opts) {
10014
10198
  }
10015
10199
  results.push({ name: sym.name, kind: sym.kind, file: sym.filePath, line: sym.lineStart });
10016
10200
  }
10201
+ if (refBlindByLanguage > 0 && opts.json !== true) {
10202
+ emitErr2(`Note: ${countNoun(refBlindByLanguage, "symbol")} skipped -- defined in a language whose call sites token-goat does not index, so deadness cannot be determined for them.`);
10203
+ }
10017
10204
  const preGrepCount = results.length;
10018
10205
  const matchesGrep = opts.grep !== void 0 ? compileGrepMatcher(opts.grep) : void 0;
10019
10206
  const grepped = matchesGrep !== void 0 ? results.filter((r) => matchesGrep(r.name)) : results;
@@ -10025,7 +10212,7 @@ function runDead(opts) {
10025
10212
  }));
10026
10213
  const topTruncated = sliced.length < grepped.length;
10027
10214
  const hiddenByGrep = preGrepCount - grepped.length;
10028
- emit2(JSON.stringify({ items: capped.items, truncated: capped.truncated || topTruncated, totalCount: grepped.length, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...opts.excludeTests === true && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {} }, null, 2));
10215
+ emit2(JSON.stringify({ items: capped.items, truncated: capped.truncated || topTruncated, totalCount: grepped.length, ...hiddenByGrep > 0 ? { hiddenByGrep } : {}, ...opts.excludeTests === true && suppressed > 0 ? { hiddenByExcludeTests: suppressed } : {}, ...blindKinds.length > 0 ? { excludedKinds: blindKinds, excludedKindsReason: REF_BLIND_KIND_REASON } : {}, ...refBlindByLanguage > 0 ? { unassessableByLanguage: refBlindByLanguage } : {} }, null, 2));
10029
10216
  return 0;
10030
10217
  }
10031
10218
  if (sliced.length === 0) {
@@ -10220,6 +10407,15 @@ var TYPE_KINDS = [
10220
10407
  // Kotlin/Scala object.
10221
10408
  "graphql_extend"
10222
10409
  ];
10410
+ var REF_BLIND_KINDS = [.../* @__PURE__ */ new Set([...TYPE_KINDS, "impl"])];
10411
+ function refBlindKindVerdict(rows) {
10412
+ const blind = rows.filter((r) => REF_BLIND_KINDS.includes(r.kind));
10413
+ return {
10414
+ blindKinds: [...new Set(blind.map((r) => r.kind))],
10415
+ blindCount: blind.length,
10416
+ allBlind: rows.length > 0 && blind.length === rows.length
10417
+ };
10418
+ }
10223
10419
  var CORE_SYMBOL_KINDS = [
10224
10420
  .../* @__PURE__ */ new Set([...GENERIC_SYMBOL_KINDS, ...TYPE_KINDS, ...OTHER_ADAPTER_SYMBOL_KINDS])
10225
10421
  ];
@@ -10728,8 +10924,9 @@ function runAsk(opts) {
10728
10924
  } catch {
10729
10925
  }
10730
10926
  if (!backendPath) return degrade(`${BACKEND_ENV}=${backendLabel} is set, but '${backendLabel}' was not found on PATH`);
10731
- const context = hits.map((h, i) => `[${i + 1}] ${h.filePath}
10927
+ const rawContext = hits.map((h, i) => `[${i + 1}] ${h.filePath}
10732
10928
  ${h.body ?? ""}`).join("\n\n");
10929
+ const context = fenceUntrustedFileContent(redactSecrets(rawContext).text);
10733
10930
  const prompt = `Answer the QUESTION using only the CODE SNIPPETS below.
10734
10931
  QUESTION: ${opts.question}
10735
10932