clast-ai 0.1.2 → 0.2.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.
Files changed (294) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +11 -9
  3. package/README.md +468 -398
  4. package/bench/ab/onair-concept.json +22 -0
  5. package/bench/ab/onair-deadcode.json +7 -0
  6. package/bench/ab/onair-hard.json +32 -0
  7. package/bench/ab/onair-impact.json +17 -0
  8. package/bench/ab/onair-refactors.json +18 -0
  9. package/bench/ab/onair-survey.json +17 -0
  10. package/bench/ab/onair-tasks.json +20 -0
  11. package/bench/ab/onair.json +52 -0
  12. package/bench/ab/vscode-concept.json +22 -0
  13. package/bench/ab/vscode-hard.json +32 -0
  14. package/bench/ab/vscode-impact.json +17 -0
  15. package/bench/ab/vscode-refactors.json +18 -0
  16. package/bench/ab/vscode-tasks.json +20 -0
  17. package/bench/ab/vscode.json +52 -0
  18. package/bench/ab.mjs +213 -0
  19. package/bench/bench.mjs +98 -0
  20. package/bench/resolution.mjs +75 -0
  21. package/dist/cli.d.ts +2 -0
  22. package/dist/cli.js +266 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/config/defaults.d.ts +12 -2
  25. package/dist/config/defaults.js +38 -31
  26. package/dist/config/defaults.js.map +1 -1
  27. package/dist/config/index.d.ts +1 -1
  28. package/dist/config/index.js +26 -14
  29. package/dist/config/index.js.map +1 -1
  30. package/dist/db/index.d.ts +73 -13
  31. package/dist/db/index.js +481 -39
  32. package/dist/db/index.js.map +1 -1
  33. package/dist/engine.d.ts +15 -0
  34. package/dist/engine.js +28 -0
  35. package/dist/engine.js.map +1 -0
  36. package/dist/indexer/imports.d.ts +6 -0
  37. package/dist/indexer/imports.js +254 -0
  38. package/dist/indexer/imports.js.map +1 -0
  39. package/dist/indexer/index.d.ts +63 -0
  40. package/dist/indexer/index.js +360 -0
  41. package/dist/indexer/index.js.map +1 -0
  42. package/dist/indexer/process.d.ts +22 -0
  43. package/dist/indexer/process.js +42 -0
  44. package/dist/indexer/process.js.map +1 -0
  45. package/dist/indexer/scan.d.ts +43 -0
  46. package/dist/indexer/scan.js +223 -0
  47. package/dist/indexer/scan.js.map +1 -0
  48. package/dist/indexer/worker.d.ts +1 -0
  49. package/dist/indexer/worker.js +23 -0
  50. package/dist/indexer/worker.js.map +1 -0
  51. package/dist/llm/comment-generator.d.ts +17 -19
  52. package/dist/llm/comment-generator.js +46 -80
  53. package/dist/llm/comment-generator.js.map +1 -1
  54. package/dist/llm/index.d.ts +0 -1
  55. package/dist/llm/index.js.map +1 -1
  56. package/dist/llm/prompts.d.ts +0 -1
  57. package/dist/llm/prompts.js +16 -16
  58. package/dist/migrate.d.ts +35 -0
  59. package/dist/migrate.js +194 -0
  60. package/dist/migrate.js.map +1 -0
  61. package/dist/parser/extractor.d.ts +21 -16
  62. package/dist/parser/extractor.js +700 -228
  63. package/dist/parser/extractor.js.map +1 -1
  64. package/dist/parser/index.d.ts +17 -21
  65. package/dist/parser/index.js +115 -171
  66. package/dist/parser/index.js.map +1 -1
  67. package/dist/parser/langs/c.d.ts +3 -0
  68. package/dist/parser/langs/c.js +879 -0
  69. package/dist/parser/langs/c.js.map +1 -0
  70. package/dist/parser/langs/csharp.d.ts +2 -0
  71. package/dist/parser/langs/csharp.js +548 -0
  72. package/dist/parser/langs/csharp.js.map +1 -0
  73. package/dist/parser/langs/dart.d.ts +2 -0
  74. package/dist/parser/langs/dart.js +893 -0
  75. package/dist/parser/langs/dart.js.map +1 -0
  76. package/dist/parser/langs/go.d.ts +2 -0
  77. package/dist/parser/langs/go.js +455 -0
  78. package/dist/parser/langs/go.js.map +1 -0
  79. package/dist/parser/langs/java.d.ts +2 -0
  80. package/dist/parser/langs/java.js +390 -0
  81. package/dist/parser/langs/java.js.map +1 -0
  82. package/dist/parser/langs/php.d.ts +2 -0
  83. package/dist/parser/langs/php.js +486 -0
  84. package/dist/parser/langs/php.js.map +1 -0
  85. package/dist/parser/langs/python.d.ts +2 -0
  86. package/dist/parser/langs/python.js +216 -0
  87. package/dist/parser/langs/python.js.map +1 -0
  88. package/dist/parser/langs/ruby.d.ts +2 -0
  89. package/dist/parser/langs/ruby.js +602 -0
  90. package/dist/parser/langs/ruby.js.map +1 -0
  91. package/dist/parser/langs/rust.d.ts +2 -0
  92. package/dist/parser/langs/rust.js +931 -0
  93. package/dist/parser/langs/rust.js.map +1 -0
  94. package/dist/parser/langs/swift.d.ts +2 -0
  95. package/dist/parser/langs/swift.js +616 -0
  96. package/dist/parser/langs/swift.js.map +1 -0
  97. package/dist/parser/langs/typescript.d.ts +4 -0
  98. package/dist/parser/langs/typescript.js +535 -0
  99. package/dist/parser/langs/typescript.js.map +1 -0
  100. package/dist/parser/registry.d.ts +16 -0
  101. package/dist/parser/registry.js +87 -0
  102. package/dist/parser/registry.js.map +1 -0
  103. package/dist/parser/returns.d.ts +9 -0
  104. package/dist/parser/returns.js +152 -0
  105. package/dist/parser/returns.js.map +1 -0
  106. package/dist/parser/spec.d.ts +165 -0
  107. package/dist/parser/spec.js +188 -0
  108. package/dist/parser/spec.js.map +1 -0
  109. package/dist/query/arity.d.ts +27 -0
  110. package/dist/query/arity.js +187 -0
  111. package/dist/query/arity.js.map +1 -0
  112. package/dist/query/callgraph.d.ts +113 -0
  113. package/dist/query/callgraph.js +332 -0
  114. package/dist/query/callgraph.js.map +1 -0
  115. package/dist/query/format.d.ts +36 -0
  116. package/dist/query/format.js +136 -0
  117. package/dist/query/format.js.map +1 -0
  118. package/dist/query/hierarchy.d.ts +44 -0
  119. package/dist/query/hierarchy.js +441 -0
  120. package/dist/query/hierarchy.js.map +1 -0
  121. package/dist/query/resolve.d.ts +265 -0
  122. package/dist/query/resolve.js +1957 -0
  123. package/dist/query/resolve.js.map +1 -0
  124. package/dist/query/search.d.ts +35 -0
  125. package/dist/query/search.js +379 -0
  126. package/dist/query/search.js.map +1 -0
  127. package/dist/query/words.d.ts +30 -0
  128. package/dist/query/words.js +201 -0
  129. package/dist/query/words.js.map +1 -0
  130. package/dist/server/index.d.ts +1 -3
  131. package/dist/server/index.js +313 -73
  132. package/dist/server/index.js.map +1 -1
  133. package/dist/tools/context.d.ts +35 -0
  134. package/dist/tools/context.js +79 -0
  135. package/dist/tools/context.js.map +1 -0
  136. package/dist/tools/explore.d.ts +51 -0
  137. package/dist/tools/explore.js +779 -0
  138. package/dist/tools/explore.js.map +1 -0
  139. package/dist/tools/impact.d.ts +21 -0
  140. package/dist/tools/impact.js +743 -0
  141. package/dist/tools/impact.js.map +1 -0
  142. package/dist/tools/search.d.ts +59 -0
  143. package/dist/tools/search.js +843 -0
  144. package/dist/tools/search.js.map +1 -0
  145. package/dist/tools/stats.d.ts +26 -0
  146. package/dist/tools/stats.js +604 -0
  147. package/dist/tools/stats.js.map +1 -0
  148. package/dist/types.d.ts +95 -40
  149. package/dist/types.js +9 -1
  150. package/dist/types.js.map +1 -1
  151. package/dist/version.d.ts +2 -0
  152. package/dist/version.js +3 -0
  153. package/dist/version.js.map +1 -0
  154. package/dist/watcher/index.d.ts +18 -16
  155. package/dist/watcher/index.js +94 -77
  156. package/dist/watcher/index.js.map +1 -1
  157. package/package.json +21 -24
  158. package/plugin/server.mjs +72 -0
  159. package/skills/audit/SKILL.md +37 -0
  160. package/skills/deadcode/SKILL.md +33 -0
  161. package/skills/explore/SKILL.md +54 -0
  162. package/skills/impact/SKILL.md +24 -0
  163. package/skills/status/SKILL.md +11 -0
  164. package/src/cli.ts +265 -0
  165. package/src/config/defaults.ts +50 -32
  166. package/src/config/index.ts +23 -14
  167. package/src/db/index.ts +540 -43
  168. package/src/engine.ts +30 -0
  169. package/src/indexer/imports.ts +241 -0
  170. package/src/indexer/index.ts +369 -0
  171. package/src/indexer/process.ts +63 -0
  172. package/src/indexer/scan.ts +226 -0
  173. package/src/indexer/worker.ts +24 -0
  174. package/src/llm/comment-generator.ts +52 -103
  175. package/src/llm/index.ts +80 -80
  176. package/src/llm/prompts.ts +95 -95
  177. package/src/migrate.ts +213 -0
  178. package/src/parser/extractor.ts +643 -256
  179. package/src/parser/index.ts +105 -205
  180. package/src/parser/langs/c.ts +860 -0
  181. package/src/parser/langs/csharp.ts +519 -0
  182. package/src/parser/langs/dart.ts +820 -0
  183. package/src/parser/langs/go.ts +437 -0
  184. package/src/parser/langs/java.ts +361 -0
  185. package/src/parser/langs/php.ts +450 -0
  186. package/src/parser/langs/python.ts +206 -0
  187. package/src/parser/langs/ruby.ts +554 -0
  188. package/src/parser/langs/rust.ts +904 -0
  189. package/src/parser/langs/swift.ts +590 -0
  190. package/src/parser/langs/typescript.ts +499 -0
  191. package/src/parser/registry.ts +88 -0
  192. package/src/parser/returns.ts +137 -0
  193. package/src/parser/spec.ts +309 -0
  194. package/src/query/arity.ts +173 -0
  195. package/src/query/callgraph.ts +473 -0
  196. package/src/query/format.ts +136 -0
  197. package/src/query/hierarchy.ts +447 -0
  198. package/src/query/resolve.ts +1972 -0
  199. package/src/query/search.ts +430 -0
  200. package/src/query/words.ts +183 -0
  201. package/src/server/index.ts +390 -79
  202. package/src/tools/context.ts +82 -0
  203. package/src/tools/explore.ts +860 -0
  204. package/src/tools/impact.ts +748 -0
  205. package/src/tools/search.ts +886 -0
  206. package/src/tools/stats.ts +633 -0
  207. package/src/types.ts +113 -45
  208. package/src/version.ts +2 -0
  209. package/src/watcher/index.ts +82 -88
  210. package/tsconfig.json +6 -3
  211. package/.mcp.json +0 -8
  212. package/commands/clast-status.md +0 -9
  213. package/dist/config/defaults.d.ts.map +0 -1
  214. package/dist/config/index.d.ts.map +0 -1
  215. package/dist/db/index.d.ts.map +0 -1
  216. package/dist/db/queries.d.ts +0 -44
  217. package/dist/db/queries.d.ts.map +0 -1
  218. package/dist/db/queries.js +0 -251
  219. package/dist/db/queries.js.map +0 -1
  220. package/dist/db/schema.d.ts +0 -3
  221. package/dist/db/schema.d.ts.map +0 -1
  222. package/dist/db/schema.js +0 -94
  223. package/dist/db/schema.js.map +0 -1
  224. package/dist/db/types.d.ts +0 -44
  225. package/dist/db/types.d.ts.map +0 -1
  226. package/dist/db/types.js +0 -2
  227. package/dist/db/types.js.map +0 -1
  228. package/dist/llm/comment-generator.d.ts.map +0 -1
  229. package/dist/llm/index.d.ts.map +0 -1
  230. package/dist/llm/prompts.d.ts.map +0 -1
  231. package/dist/parser/call-resolver.d.ts +0 -15
  232. package/dist/parser/call-resolver.d.ts.map +0 -1
  233. package/dist/parser/call-resolver.js +0 -16
  234. package/dist/parser/call-resolver.js.map +0 -1
  235. package/dist/parser/extractor.d.ts.map +0 -1
  236. package/dist/parser/index.d.ts.map +0 -1
  237. package/dist/parser/languages.d.ts +0 -11
  238. package/dist/parser/languages.d.ts.map +0 -1
  239. package/dist/parser/languages.js +0 -74
  240. package/dist/parser/languages.js.map +0 -1
  241. package/dist/server/index.d.ts.map +0 -1
  242. package/dist/server/register-tools.d.ts +0 -7
  243. package/dist/server/register-tools.d.ts.map +0 -1
  244. package/dist/server/register-tools.js +0 -17
  245. package/dist/server/register-tools.js.map +0 -1
  246. package/dist/server/tools/clast-call-graph.d.ts +0 -4
  247. package/dist/server/tools/clast-call-graph.d.ts.map +0 -1
  248. package/dist/server/tools/clast-call-graph.js +0 -70
  249. package/dist/server/tools/clast-call-graph.js.map +0 -1
  250. package/dist/server/tools/clast-comment.d.ts +0 -5
  251. package/dist/server/tools/clast-comment.d.ts.map +0 -1
  252. package/dist/server/tools/clast-comment.js +0 -64
  253. package/dist/server/tools/clast-comment.js.map +0 -1
  254. package/dist/server/tools/clast-file-summary.d.ts +0 -4
  255. package/dist/server/tools/clast-file-summary.d.ts.map +0 -1
  256. package/dist/server/tools/clast-file-summary.js +0 -42
  257. package/dist/server/tools/clast-file-summary.js.map +0 -1
  258. package/dist/server/tools/clast-get-context.d.ts +0 -4
  259. package/dist/server/tools/clast-get-context.d.ts.map +0 -1
  260. package/dist/server/tools/clast-get-context.js +0 -80
  261. package/dist/server/tools/clast-get-context.js.map +0 -1
  262. package/dist/server/tools/clast-reindex.d.ts +0 -5
  263. package/dist/server/tools/clast-reindex.d.ts.map +0 -1
  264. package/dist/server/tools/clast-reindex.js +0 -36
  265. package/dist/server/tools/clast-reindex.js.map +0 -1
  266. package/dist/server/tools/clast-search.d.ts +0 -4
  267. package/dist/server/tools/clast-search.d.ts.map +0 -1
  268. package/dist/server/tools/clast-search.js +0 -29
  269. package/dist/server/tools/clast-search.js.map +0 -1
  270. package/dist/server/tools/clast-status.d.ts +0 -5
  271. package/dist/server/tools/clast-status.d.ts.map +0 -1
  272. package/dist/server/tools/clast-status.js +0 -23
  273. package/dist/server/tools/clast-status.js.map +0 -1
  274. package/dist/types.d.ts.map +0 -1
  275. package/dist/watcher/gitignore.d.ts +0 -9
  276. package/dist/watcher/gitignore.d.ts.map +0 -1
  277. package/dist/watcher/gitignore.js +0 -38
  278. package/dist/watcher/gitignore.js.map +0 -1
  279. package/dist/watcher/index.d.ts.map +0 -1
  280. package/skills/clast-explore/SKILL.md +0 -41
  281. package/src/db/queries.ts +0 -332
  282. package/src/db/schema.ts +0 -106
  283. package/src/db/types.ts +0 -47
  284. package/src/parser/call-resolver.ts +0 -17
  285. package/src/parser/languages.ts +0 -86
  286. package/src/server/register-tools.ts +0 -30
  287. package/src/server/tools/clast-call-graph.ts +0 -82
  288. package/src/server/tools/clast-comment.ts +0 -76
  289. package/src/server/tools/clast-file-summary.ts +0 -59
  290. package/src/server/tools/clast-get-context.ts +0 -97
  291. package/src/server/tools/clast-reindex.ts +0 -52
  292. package/src/server/tools/clast-search.ts +0 -38
  293. package/src/server/tools/clast-status.ts +0 -37
  294. package/src/watcher/gitignore.ts +0 -42
@@ -0,0 +1,633 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { isTestPath, isVendoredPath, plural } from '../query/format.js';
4
+ import { directSubtypes } from '../query/hierarchy.js';
5
+ import { findSymbols, pathMatches } from '../query/resolve.js';
6
+ import type { SymbolRow } from '../types.js';
7
+ import { ToolContext } from './context.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // clast_stats: exact counts over the index, for "how many X" questions
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export type StatsWhat = 'inventory' | 'symbols' | 'calls' | 'subtypes' | 'text' | 'imports' | 'endpoints' | 'database';
14
+ export type StatsGroup = 'dir' | 'file' | 'kind' | 'lang' | 'name' | 'container' | 'callee' | 'caller' | 'receiver' | 'match';
15
+
16
+ export interface StatsArgs {
17
+ /** What to count. Default: inventory. */
18
+ what?: StatsWhat;
19
+ /**
20
+ * symbols: name pattern with `*` wildcards (`*Receiver`), alternatives with `|`.
21
+ * calls: called name(s) (`query|execute`). subtypes: the base type. text: a regular expression.
22
+ */
23
+ name?: string;
24
+ /** symbols: only these kinds (comma separated). */
25
+ kind?: string;
26
+ /** calls: only calls whose receiver or receiver type matches this pattern (`*Repository|db|conn`). */
27
+ on?: string;
28
+ path?: string;
29
+ lang?: string;
30
+ groupBy?: StatsGroup;
31
+ /** Directory depth for groupBy=dir. Default 2 (below `path` when given). */
32
+ depth?: number;
33
+ /** Rows per table. Default 25. */
34
+ limit?: number;
35
+ /** Count test / example code too. Default: counted separately, not in the tables. */
36
+ includeTests?: boolean;
37
+ }
38
+
39
+ interface Filters {
40
+ path?: string;
41
+ lang?: string;
42
+ includeTests: boolean;
43
+ }
44
+
45
+ /** `*Foo|Bar*` -> SQL GLOB patterns (case-sensitive, like the names themselves). */
46
+ function globs(pattern: string): string[] {
47
+ return pattern.split('|').map(p => p.trim()).filter(Boolean);
48
+ }
49
+
50
+ function globToRegExp(pattern: string): RegExp {
51
+ const alts = globs(pattern).map(p => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'));
52
+ return new RegExp(`^(?:${alts.join('|')})$`);
53
+ }
54
+
55
+ function dirKey(path: string, depth: number, base?: string): string {
56
+ const parts = path.split('/');
57
+ parts.pop();
58
+ let skip = 0;
59
+ if (base) {
60
+ const b = base.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '').split('/');
61
+ if (b.every((seg, i) => parts[i] === seg)) skip = b.length;
62
+ }
63
+ const kept = parts.slice(0, skip + depth);
64
+ return kept.length ? kept.join('/') + '/' : '(root)';
65
+ }
66
+
67
+ class Tally {
68
+ private readonly counts = new Map<string, number>();
69
+ total = 0;
70
+ add(key: string, n = 1): void {
71
+ this.counts.set(key, (this.counts.get(key) ?? 0) + n);
72
+ this.total += n;
73
+ }
74
+ get size(): number {
75
+ return this.counts.size;
76
+ }
77
+ top(limit: number): Array<[string, number]> {
78
+ return [...this.counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, limit);
79
+ }
80
+ lines(title: string, limit: number, indent = ' '): string[] {
81
+ if (this.size === 0) return [];
82
+ const rows = this.top(limit);
83
+ const width = String(rows[0]![1]).length;
84
+ const out = [`${title} (${this.size}):`];
85
+ for (const [key, n] of rows) out.push(`${indent}${String(n).padStart(width)} ${key}`);
86
+ if (this.size > rows.length) out.push(`${indent}... ${this.size - rows.length} more (raise limit)`);
87
+ return out;
88
+ }
89
+ }
90
+
91
+ /** Splits production / test / vendored so that headline numbers mean "code this project wrote". */
92
+ class Split {
93
+ prod = 0;
94
+ tests = 0;
95
+ vendored = 0;
96
+ private testsShown = false;
97
+ /** Returns true when the item belongs in the main tables. */
98
+ add(path: string, includeTests: boolean, n = 1): boolean {
99
+ if (isVendoredPath(path)) { this.vendored += n; return false; }
100
+ if (isTestPath(path)) { this.tests += n; this.testsShown = includeTests; return includeTests; }
101
+ this.prod += n;
102
+ return true;
103
+ }
104
+ describe(): string {
105
+ const inside = this.testsShown && this.tests ? ` (${this.tests} of them in tests/examples)` : '';
106
+ const extra: string[] = [];
107
+ if (this.tests && !this.testsShown) extra.push(`${this.tests} in tests/examples`);
108
+ if (this.vendored) extra.push(`${this.vendored} in vendored/generated code`);
109
+ return inside + (extra.length ? ` (plus ${extra.join(', ')}; not in the tables below)` : '');
110
+ }
111
+ }
112
+
113
+ function fileOk(path: string, lang: string | null, f: Filters): boolean {
114
+ if (f.path && !pathMatches(path, f.path)) return false;
115
+ if (f.lang && lang !== f.lang) return false;
116
+ return true;
117
+ }
118
+
119
+ // --- symbols ----------------------------------------------------------------
120
+
121
+ function symbolStats(ctx: ToolContext, args: StatsArgs, f: Filters, limit: number, depth: number): string {
122
+ const where: string[] = [];
123
+ const params: string[] = [];
124
+ if (args.name) {
125
+ const g = globs(args.name);
126
+ where.push('(' + g.map(() => 's.name GLOB ?').join(' OR ') + ')');
127
+ params.push(...g);
128
+ }
129
+ const kinds = (args.kind ?? '').split(',').map(k => k.trim()).filter(Boolean);
130
+ if (kinds.length) {
131
+ where.push('s.kind IN (' + kinds.map(() => '?').join(',') + ')');
132
+ params.push(...kinds);
133
+ }
134
+ const rows = ctx.db.all<{ name: string; qname: string; kind: string; path: string; lang: string }>(
135
+ `SELECT s.name, s.qname, s.kind, f.path, f.lang FROM symbols s JOIN files f ON f.id = s.file_id` +
136
+ (where.length ? ` WHERE ${where.join(' AND ')}` : ''),
137
+ ...params
138
+ );
139
+ const split = new Split();
140
+ const groupBy = args.groupBy ?? 'dir';
141
+ const main = new Tally();
142
+ const byKind = new Tally();
143
+ const names = new Set<string>();
144
+ for (const r of rows) {
145
+ if (!fileOk(r.path, r.lang, f)) continue;
146
+ if (!split.add(r.path, f.includeTests)) continue;
147
+ names.add(r.name);
148
+ byKind.add(r.kind);
149
+ const key =
150
+ groupBy === 'file' ? r.path
151
+ : groupBy === 'kind' ? r.kind
152
+ : groupBy === 'lang' ? r.lang
153
+ : groupBy === 'name' ? r.name
154
+ : groupBy === 'container' ? (r.qname.includes('.') ? r.qname.slice(0, r.qname.lastIndexOf('.')) : '(top level)')
155
+ : dirKey(r.path, depth, f.path);
156
+ main.add(key);
157
+ }
158
+ const what = [args.name ? `named ${args.name}` : '', kinds.length ? `of kind ${kinds.join('/')}` : ''].filter(Boolean).join(' ');
159
+ const out = [
160
+ `${plural(main.total, 'symbol definition')}${what ? ' ' + what : ''}${scopeNote(f)}${split.describe()}`,
161
+ `distinct names: ${names.size}`,
162
+ ];
163
+ if (groupBy !== 'kind') out.push(...byKind.lines('by kind', 12));
164
+ out.push(...main.lines(`by ${groupBy}`, limit));
165
+ return out.join('\n');
166
+ }
167
+
168
+ function scopeNote(f: Filters): string {
169
+ const parts: string[] = [];
170
+ if (f.path) parts.push(`under ${f.path}`);
171
+ if (f.lang) parts.push(`in ${f.lang}`);
172
+ return parts.length ? ' ' + parts.join(' ') : '';
173
+ }
174
+
175
+ // --- calls ------------------------------------------------------------------
176
+
177
+ function callStats(ctx: ToolContext, args: StatsArgs, f: Filters, limit: number, depth: number): string {
178
+ if (!args.name) return 'error: name is required for what=calls (the called name, e.g. "query|execute" or "find*")';
179
+ const g = globs(args.name);
180
+ const rows = ctx.db.all<{
181
+ name: string; path: string; lang: string; receiver: string | null; rtype: string | null; count: number; caller: string | null;
182
+ }>(
183
+ `SELECT n.name, f.path, f.lang, c.receiver, c.rtype, c.count, s.qname AS caller
184
+ FROM calls c JOIN names n ON n.id = c.name_id JOIN files f ON f.id = c.file_id
185
+ LEFT JOIN symbols s ON s.id = c.caller_id
186
+ WHERE ${g.map(() => 'n.name GLOB ?').join(' OR ')}`,
187
+ ...g
188
+ );
189
+ const on = args.on ? globToRegExp(args.on) : null;
190
+ const split = new Split();
191
+ const groupBy = args.groupBy ?? 'dir';
192
+ const main = new Tally();
193
+ const byCallee = new Tally();
194
+ const byReceiver = new Tally();
195
+ const files = new Set<string>();
196
+ for (const r of rows) {
197
+ if (!fileOk(r.path, r.lang, f)) continue;
198
+ const recv = r.rtype ?? r.receiver ?? '';
199
+ if (on && !on.test(recv) && !(r.receiver && on.test(r.receiver))) continue;
200
+ if (!split.add(r.path, f.includeTests, r.count)) continue;
201
+ files.add(r.path);
202
+ byCallee.add(r.name, r.count);
203
+ byReceiver.add(recv || '(no receiver)', r.count);
204
+ const key =
205
+ groupBy === 'file' ? r.path
206
+ : groupBy === 'lang' ? r.lang
207
+ : groupBy === 'callee' || groupBy === 'name' ? r.name
208
+ : groupBy === 'receiver' ? recv || '(no receiver)'
209
+ : groupBy === 'caller' ? r.caller ?? '(top level)'
210
+ : dirKey(r.path, depth, f.path);
211
+ main.add(key, r.count);
212
+ }
213
+ const out = [
214
+ `${plural(main.total, 'call site')} of ${args.name}${args.on ? ` on ${args.on}` : ''}${scopeNote(f)} in ${plural(files.size, 'file')}${split.describe()}`,
215
+ 'counted by called name' + (args.on ? ' and receiver text / declared receiver type' : '') +
216
+ '; use clast_call_graph for the resolved callers of one definition',
217
+ ];
218
+ if (groupBy !== 'callee' && groupBy !== 'name' && byCallee.size > 1) out.push(...byCallee.lines('by called name', 15));
219
+ if (groupBy !== 'receiver' && byReceiver.size > 1) out.push(...byReceiver.lines('by receiver (declared type where known)', 12));
220
+ out.push(...main.lines(`by ${groupBy}`, limit));
221
+ return out.join('\n');
222
+ }
223
+
224
+ // --- subtypes ---------------------------------------------------------------
225
+
226
+ function subtypeStats(ctx: ToolContext, args: StatsArgs, f: Filters, limit: number, depth: number): string {
227
+ if (!args.name) return 'error: name is required for what=subtypes (the base class or interface)';
228
+ const roots = findSymbols(ctx.db, args.name, { exact: true }).filter(s => /class|interface|struct|trait|protocol|type|enum/.test(s.kind));
229
+ const seen = new Map<number, { sym: SymbolRow; level: number }>();
230
+ let frontier: SymbolRow[] = [];
231
+ let startLevel = 1;
232
+ let root: SymbolRow | null = roots[0] ?? null;
233
+ if (root) {
234
+ frontier = [root];
235
+ } else {
236
+ // A library type (JpaRepository, React.Component): start from the types that name it as a supertype.
237
+ const bare = args.name.split(/[.:\\]/).pop()!;
238
+ const ids = ctx.db.all<{ symbol_id: number }>(`SELECT DISTINCT symbol_id FROM heritage WHERE name = ?`, bare);
239
+ for (const { symbol_id } of ids) {
240
+ const sym = ctx.db.symbolById(symbol_id);
241
+ if (!sym) continue;
242
+ seen.set(sym.id, { sym, level: 1 });
243
+ frontier.push(sym);
244
+ }
245
+ if (frontier.length === 0) return `no type named ${args.name} in the index, and nothing extends or implements it (try clast_search)`;
246
+ startLevel = 2;
247
+ }
248
+ for (let level = startLevel; frontier.length && level <= 40 && seen.size < 20000; level++) {
249
+ const next: SymbolRow[] = [];
250
+ for (const s of frontier) {
251
+ for (const sub of directSubtypes(ctx.db, ctx.resolver, s)) {
252
+ if (sub.sym.id === root?.id || seen.has(sub.sym.id)) continue;
253
+ seen.set(sub.sym.id, { sym: sub.sym, level });
254
+ next.push(sub.sym);
255
+ }
256
+ }
257
+ frontier = next;
258
+ }
259
+ const split = new Split();
260
+ const groupBy = args.groupBy ?? 'dir';
261
+ const main = new Tally();
262
+ const byLevel = new Tally();
263
+ let direct = 0;
264
+ for (const { sym, level } of seen.values()) {
265
+ if (f.path && !pathMatches(sym.path, f.path)) continue;
266
+ if (!split.add(sym.path, f.includeTests)) continue;
267
+ if (level === 1) direct++;
268
+ byLevel.add(level === 1 ? 'direct' : `level ${level}`);
269
+ main.add(groupBy === 'file' ? sym.path : groupBy === 'kind' ? sym.kind : groupBy === 'name' ? sym.qname : dirKey(sym.path, depth, f.path));
270
+ }
271
+ const out = [
272
+ `${plural(main.total, 'subtype')} of ` +
273
+ (root ? `${root.kind} ${root.qname} (${root.path}:${root.start_line})` : `${args.name} (not defined in this project: a library type, matched by name)`) +
274
+ `${scopeNote(f)}: ${direct} direct, ${main.total - direct} indirect${split.describe()}`,
275
+ ];
276
+ if (roots.length > 1) out.push(`note: ${roots.length} types are named ${args.name}; counted the first. Select another with path:name.`);
277
+ if (byLevel.size > 1) out.push(...byLevel.lines('by inheritance level', 10));
278
+ out.push(...main.lines(`by ${groupBy}`, limit));
279
+ if (root) out.push(`(clast_hierarchy ${root.name} lists them as a tree)`);
280
+ else if (main.size <= limit && groupBy !== 'name') out.push('(groupBy=name lists them)');
281
+ return out.join('\n');
282
+ }
283
+
284
+ // --- imports ----------------------------------------------------------------
285
+
286
+ function importStats(ctx: ToolContext, f: Filters, limit: number): string {
287
+ const rows = ctx.db.all<{ source: string; path: string; lang: string; resolved_id: number | null }>(
288
+ `SELECT i.source, f.path, f.lang, i.resolved_id FROM imports i JOIN files f ON f.id = i.file_id`
289
+ );
290
+ const split = new Split();
291
+ const external = new Map<string, Set<string>>();
292
+ let internal = 0;
293
+ for (const r of rows) {
294
+ if (!fileOk(r.path, r.lang, f)) continue;
295
+ if (!split.add(r.path, f.includeTests)) continue;
296
+ if (r.resolved_id !== null) { internal++; continue; }
297
+ // Package name: `@scope/pkg/sub` -> `@scope/pkg`, `pkg/sub` -> `pkg`, `a.b.c` (Java / Python) -> `a.b`.
298
+ let pkg = r.source.replace(/^<|>$/g, '');
299
+ if (pkg.startsWith('.')) continue;
300
+ if (pkg.startsWith('@')) pkg = pkg.split('/').slice(0, 2).join('/');
301
+ else if (pkg.includes('/')) pkg = /\.(h|hpp|hh)$/.test(pkg) ? pkg.split('/')[0]! : pkg.includes('.') && !pkg.startsWith('node:') ? pkg.split('/').slice(0, 3).join('/') : pkg.split('/')[0]!;
302
+ else if (pkg.includes('::')) pkg = pkg.split('::')[0]!;
303
+ else if (/^[\w$]+(\.[\w$]+){2,}$/.test(pkg)) pkg = pkg.split('.').slice(0, 2).join('.');
304
+ let set = external.get(pkg);
305
+ if (!set) external.set(pkg, (set = new Set()));
306
+ set.add(r.path);
307
+ }
308
+ const tally = new Tally();
309
+ for (const [pkg, files] of external) tally.add(pkg, files.size);
310
+ const out = [
311
+ `imports${scopeNote(f)}: ${internal} resolve to files of this project, ${external.size} distinct external packages / headers${split.describe()}`,
312
+ ...tally.lines('external dependencies by number of importing files', limit),
313
+ ];
314
+ return out.join('\n');
315
+ }
316
+
317
+ // --- text / presets ---------------------------------------------------------
318
+
319
+ interface Pattern {
320
+ label: string;
321
+ re: RegExp;
322
+ /** Only files in these languages (index language ids). */
323
+ langs?: string[];
324
+ /** Only files whose path matches. */
325
+ file?: RegExp;
326
+ /** Count non-indexed text files too (proto, sql, config). */
327
+ others?: boolean;
328
+ /** Counts files, not text: keep the matched text out of the keyword table. */
329
+ filesOnly?: boolean;
330
+ }
331
+
332
+ interface TextCount {
333
+ byLabel: Tally;
334
+ byDir: Tally;
335
+ byFile: Tally;
336
+ byMatch: Tally;
337
+ split: Split;
338
+ scanned: number;
339
+ timedOut: boolean;
340
+ examples: Map<string, string[]>;
341
+ }
342
+
343
+ async function countText(
344
+ ctx: ToolContext, patterns: Pattern[], f: Filters, depth: number, budgetMs = 25000
345
+ ): Promise<TextCount> {
346
+ const res: TextCount = {
347
+ byLabel: new Tally(), byDir: new Tally(), byFile: new Tally(), byMatch: new Tally(), split: new Split(),
348
+ scanned: 0, timedOut: false, examples: new Map(),
349
+ };
350
+ const indexed = ctx.db.all<{ path: string; lang: string }>(`SELECT path, lang FROM files ORDER BY path`);
351
+ const work: Array<{ path: string; lang: string | null }> = indexed.filter(x => fileOk(x.path, x.lang, f));
352
+ if (!f.lang && patterns.some(p => p.others)) {
353
+ for (const p of ctx.indexer.rules.otherTextFiles()) if (fileOk(p, null, f)) work.push({ path: p, lang: null });
354
+ }
355
+ const started = performance.now();
356
+ const BATCH = 96;
357
+ for (let i = 0; i < work.length; i += BATCH) {
358
+ if (performance.now() - started > budgetMs) { res.timedOut = true; break; }
359
+ const batch = work.slice(i, i + BATCH).filter(w =>
360
+ patterns.some(p => (w.lang === null ? p.others : !p.langs || p.langs.includes(w.lang)) && (!p.file || p.file.test(w.path)))
361
+ );
362
+ const bufs = await Promise.all(batch.map(w => readFile(join(ctx.clast.root, w.path)).catch(() => null)));
363
+ for (let k = 0; k < batch.length; k++) {
364
+ const buf = bufs[k];
365
+ const w = batch[k]!;
366
+ if (!buf || buf.length > 4_000_000 || buf.subarray(0, 1024).includes(0)) continue;
367
+ res.scanned++;
368
+ const text = buf.toString('utf-8');
369
+ for (const p of patterns) {
370
+ if (w.lang === null ? !p.others : p.langs && !p.langs.includes(w.lang)) continue;
371
+ if (p.file && !p.file.test(w.path)) continue;
372
+ p.re.lastIndex = 0;
373
+ let n = 0;
374
+ let m: RegExpExecArray | null;
375
+ const local = new Map<string, number>();
376
+ while ((m = p.re.exec(text)) !== null) {
377
+ if (m[0].length === 0) { p.re.lastIndex++; continue; }
378
+ n++;
379
+ const shown = (m[1] ?? m[0]).trim().replace(/\s+/g, ' ').slice(0, 60);
380
+ local.set(shown, (local.get(shown) ?? 0) + 1);
381
+ }
382
+ if (n === 0) continue;
383
+ if (!res.split.add(w.path, f.includeTests, n)) continue;
384
+ res.byLabel.add(p.label, n);
385
+ res.byDir.add(dirKey(w.path, depth, f.path), n);
386
+ res.byFile.add(w.path, n);
387
+ if (!p.filesOnly) for (const [key, c] of local) res.byMatch.add(key, c);
388
+ const ex = res.examples.get(p.label) ?? [];
389
+ if (ex.length < 3) { ex.push(w.path); res.examples.set(p.label, ex); }
390
+ }
391
+ }
392
+ }
393
+ return res;
394
+ }
395
+
396
+ async function textStats(ctx: ToolContext, args: StatsArgs, f: Filters, limit: number, depth: number): Promise<string> {
397
+ if (!args.name) return 'error: name is required for what=text (a regular expression)';
398
+ let re: RegExp;
399
+ try {
400
+ re = new RegExp(args.name, /[A-Z]/.test(args.name) ? 'g' : 'gi');
401
+ } catch (err) {
402
+ return `error: invalid regular expression: ${err instanceof Error ? err.message : err}`;
403
+ }
404
+ const res = await countText(ctx, [{ label: 'matches', re, others: true }], f, depth);
405
+ const groupBy = args.groupBy ?? 'dir';
406
+ const main = groupBy === 'file' ? res.byFile : groupBy === 'match' || groupBy === 'name' ? res.byMatch : res.byDir;
407
+ const out = [
408
+ `${plural(res.byLabel.total, 'match')} of /${args.name}/${scopeNote(f)} in ${plural(res.byFile.size, 'file')}` +
409
+ ` (${res.scanned} files read)${res.split.describe()}` + (res.timedOut ? ' INCOMPLETE: time budget hit, narrow with path' : ''),
410
+ ];
411
+ if (groupBy !== 'match' && res.byMatch.size > 1 && res.byMatch.size <= 40) out.push(...res.byMatch.lines('by matched text', 15));
412
+ out.push(...main.lines(`by ${groupBy === 'name' ? 'match' : groupBy}`, limit));
413
+ return out.join('\n');
414
+ }
415
+
416
+ const JS = ['javascript', 'typescript', 'tsx'];
417
+
418
+ /** Route declarations of the common web frameworks. Pattern-based: a count to start from, not a proof. */
419
+ const ENDPOINT_PATTERNS: Pattern[] = [
420
+ // (Not `api.get(...)` / `client.post(...)`: in front-end code those are outbound requests, counted separately.)
421
+ { label: 'Express / Koa / Fastify / Hono route calls', langs: JS, re: /\b(?:app|router|routes?|server|fastify)\.(get|post|put|patch|delete|head|options|all)\(\s*['"`\/]/g },
422
+ { label: 'NestJS / TypeScript route decorators', langs: JS, re: /@(Get|Post|Put|Patch|Delete|All|Head|Options)\(/g },
423
+ { label: 'Spring mappings', langs: ['java', 'kotlin'], re: /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\b/g },
424
+ { label: 'JAX-RS verbs', langs: ['java', 'kotlin'], re: /^\s*@(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*$/gm },
425
+ { label: 'ASP.NET attributes', langs: ['csharp'], re: /\[(Http(?:Get|Post|Put|Patch|Delete|Head|Options))\b/g },
426
+ { label: 'ASP.NET minimal API', langs: ['csharp'], re: /\.(Map(?:Get|Post|Put|Patch|Delete))\(/g },
427
+ { label: 'Flask / FastAPI decorators', langs: ['python'], re: /^\s*@[\w.]+\.(get|post|put|patch|delete|route|websocket|api_route)\(/gm },
428
+ { label: 'Django url patterns', langs: ['python'], file: /(^|\/)urls[^/]*\.py$/, re: /\b((?:re_)?path|url)\(\s*r?['"]/g },
429
+ { label: 'Rails routes', langs: ['ruby'], file: /(^|\/)routes(\.rb|\/[^/]+\.rb)$/, re: /^\s*(get|post|put|patch|delete|match|root|resources?)\s+['":]/gm },
430
+ { label: 'Laravel routes', langs: ['php'], re: /Route::(get|post|put|patch|delete|any|match|resource|apiResource)\(/g },
431
+ { label: 'Go router registrations', langs: ['go'], re: /\.(GET|POST|PUT|PATCH|DELETE|Get|Post|Put|Patch|Delete|HandleFunc|Handle)\(\s*"/g },
432
+ { label: 'Rust route attributes (actix / rocket)', langs: ['rust'], re: /#\[(get|post|put|patch|delete)\(/g },
433
+ { label: 'Rust axum routes', langs: ['rust'], re: /\.(route)\(\s*"/g },
434
+ { label: 'gRPC methods (.proto)', others: true, file: /\.proto$/, re: /^\s*(rpc)\s+\w+\s*\(/gm },
435
+ { label: 'GraphQL operations (schema files)', others: true, file: /\.(graphql|gql)$/, re: /^\s*(?:extend\s+)?type\s+(Query|Mutation|Subscription)\b/gm },
436
+ ];
437
+
438
+ /** Requests this code sends to other services. Reported next to the endpoints, never added to them. */
439
+ const CLIENT_PATTERNS: Pattern[] = [
440
+ { label: 'JS HTTP client calls (axios / api client)', langs: JS, re: /\b(?:axios|api|apiClient|client|http|httpClient|\$http|request)\.(get|post|put|patch|delete)\(/g },
441
+ { label: 'fetch()', langs: JS, re: /\b(fetch)\(/g },
442
+ { label: 'Python requests / httpx', langs: ['python'], re: /\b(?:requests|httpx|session|client)\.(get|post|put|patch|delete)\(/g },
443
+ { label: 'Java / C# HTTP clients', langs: ['java', 'kotlin', 'csharp'], re: /\b(RestTemplate|WebClient|HttpClient|OkHttpClient)\b/g },
444
+ ];
445
+
446
+ const DB_PATTERNS: Pattern[] = [
447
+ { label: 'SQL statements in source', re: /\b(SELECT)\s[^;'"`]{0,200}?\sFROM\b|\b(?:INSERT\s+INTO|UPDATE\s+[\w."`\[\]]+\s+SET|DELETE\s+FROM)\b/g },
448
+ { label: 'CREATE TABLE statements (schema files and every migration)', others: true, re: /\b(CREATE\s+TABLE)\b/gi },
449
+ { label: 'ORM entities / models', re: /@(Entity|Document|MappedSuperclass)\b|\bclass\s+\w+\((?:[\w.]*\.)?Model\)|\bextends\s+(?:Model|ActiveRecord::Base|ApplicationRecord)\b|<\s*(?:ApplicationRecord|ActiveRecord::Base)\b|\bDbSet<\w+>|\b(?:sequelize\.define|mongoose\.model)\(/g },
450
+ { label: 'Prisma models (schema.prisma)', others: true, file: /\.prisma$/, re: /^\s*(model)\s+\w+\s*\{/gm },
451
+ { label: 'migration files', others: true, file: /(^|\/)migrations?\/.*\.(sql|py|rb|ts|js|php|cs)$/i, re: /^[\s\S]/g, filesOnly: true },
452
+ ];
453
+
454
+ const DB_CALLS = 'query|execute|exec|executeQuery|executeUpdate|prepare|prepareStatement|rawQuery|raw|findOne|findMany|findAll|findById|findUnique|findUniqueOrThrow|findFirst|findFirstOrThrow|aggregate|groupBy|count|create|createMany|update|upsert|delete|insertOne|insertMany|updateOne|updateMany|deleteOne|deleteMany|$queryRaw|$queryRawUnsafe|$executeRaw|$executeRawUnsafe|$transaction|ExecuteReader|ExecuteNonQuery|ExecuteScalar|SaveChanges|SaveChangesAsync|fetchall|fetchone|executemany|QueryRow|QueryContext|ExecContext';
455
+ const DB_RECEIVERS = /^(?:.*(?:db|database|conn|connection|pool|client|session|cursor|cur|tx|trx|transaction|repo|repository|dao|knex|prisma|sequelize|mongo|collection|em|entitymanager|context|dbcontext|stmt|statement).*)$/i;
456
+
457
+ async function presetStats(
458
+ ctx: ToolContext, title: string, patterns: Pattern[], f: Filters, limit: number, depth: number
459
+ ): Promise<string[]> {
460
+ const res = await countText(ctx, patterns, f, depth);
461
+ const out = [
462
+ `${title}${scopeNote(f)}: ${res.byLabel.total} found by pattern in ${plural(res.byFile.size, 'file')}${res.split.describe()}` +
463
+ (res.timedOut ? ' INCOMPLETE: time budget hit, narrow with path' : ''),
464
+ ];
465
+ if (res.byLabel.total === 0) return out;
466
+ out.push(`by kind (${res.byLabel.size}):`);
467
+ for (const [label, n] of res.byLabel.top(40)) {
468
+ out.push(` ${String(n).padStart(6)} ${label} e.g. ${(res.examples.get(label) ?? []).slice(0, 2).join(', ')}`);
469
+ }
470
+ if (res.byMatch.size > 1 && res.byMatch.size <= 60) out.push(...res.byMatch.lines('by matched keyword', 20));
471
+ out.push(...res.byDir.lines('by dir', limit));
472
+ out.push(...res.byFile.lines('densest files', Math.min(limit, 10)));
473
+ return out;
474
+ }
475
+
476
+ async function endpointStats(ctx: ToolContext, f: Filters, limit: number, depth: number): Promise<string> {
477
+ const out = await presetStats(ctx, 'HTTP / RPC endpoints', ENDPOINT_PATTERNS, f, limit, depth);
478
+ const clients = await countText(ctx, CLIENT_PATTERNS, f, depth);
479
+ if (clients.byLabel.total) {
480
+ out.push('');
481
+ out.push(
482
+ `outbound HTTP requests (calls this code makes; not endpoints): ${clients.byLabel.total} in ${plural(clients.byFile.size, 'file')}${clients.split.describe()}`
483
+ );
484
+ for (const [label, n] of clients.byLabel.top(10)) out.push(` ${String(n).padStart(6)} ${label}`);
485
+ out.push(...clients.byDir.lines('by dir', Math.min(limit, 8)));
486
+ }
487
+ out.push(
488
+ 'Pattern-based: counts route declarations of common frameworks (Express, NestJS, Spring, JAX-RS, ASP.NET, Flask, FastAPI, ' +
489
+ 'Django, Rails, Laravel, Go routers, actix, axum, gRPC, GraphQL). Routes built in loops or by a custom framework are not ' +
490
+ 'seen; count those with what=text or what=calls and your own pattern.'
491
+ );
492
+ return out.join('\n');
493
+ }
494
+
495
+ async function databaseStats(ctx: ToolContext, f: Filters, limit: number, depth: number): Promise<string> {
496
+ const out = await presetStats(ctx, 'database access', DB_PATTERNS, f, limit, depth);
497
+ // Call sites of query-like methods on receivers that look like a database handle.
498
+ const g = globs(DB_CALLS);
499
+ const rows = ctx.db.all<{ name: string; path: string; lang: string; receiver: string | null; rtype: string | null; count: number }>(
500
+ `SELECT n.name, f.path, f.lang, c.receiver, c.rtype, c.count
501
+ FROM calls c JOIN names n ON n.id = c.name_id JOIN files f ON f.id = c.file_id
502
+ WHERE n.name IN (${g.map(() => '?').join(',')})`,
503
+ ...g
504
+ );
505
+ const split = new Split();
506
+ const byName = new Tally();
507
+ const byDir = new Tally();
508
+ const byTarget = new Tally();
509
+ for (const r of rows) {
510
+ if (!fileOk(r.path, r.lang, f)) continue;
511
+ const recv = `${r.rtype ?? ''} ${r.receiver ?? ''}`.trim();
512
+ if (!recv || !recv.split(/[\s.]+/).some(part => DB_RECEIVERS.test(part))) continue;
513
+ if (!split.add(r.path, f.includeTests, r.count)) continue;
514
+ byName.add(r.name, r.count);
515
+ byDir.add(dirKey(r.path, depth, f.path), r.count);
516
+ // `this.prisma.user.findMany()`, `userRepository.save()`: the last receiver segment names the table / model.
517
+ const target = (r.receiver ?? r.rtype ?? '').replace(/^(?:this|self)\./, '').replace(/^@/, '');
518
+ if (target) byTarget.add(target, r.count);
519
+ }
520
+ out.push('');
521
+ out.push(
522
+ `query-method call sites on database-like receivers (db, conn, pool, session, cursor, repository, ...): ${byName.total}${split.describe()}`
523
+ );
524
+ out.push(...byName.lines('by method', 20));
525
+ out.push(...byTarget.lines('by receiver (model / table / handle)', Math.min(limit, 20)));
526
+ out.push(...byDir.lines('by dir', limit));
527
+ out.push(
528
+ 'Heuristic: SQL text, schema statements, ORM model declarations, and calls named like query methods on receivers named like ' +
529
+ 'a database handle. Treat as an inventory to verify, not an exact count; refine with what=calls name=... on=...'
530
+ );
531
+ return out.join('\n');
532
+ }
533
+
534
+ // --- inventory --------------------------------------------------------------
535
+
536
+ function inventory(ctx: ToolContext, f: Filters, limit: number, depth: number): string {
537
+ const files = ctx.db.all<{ id: number; path: string; lang: string; line_count: number; symbol_count: number }>(
538
+ `SELECT id, path, lang, line_count, symbol_count FROM files`
539
+ ).filter(x => fileOk(x.path, x.lang, f));
540
+ const groups = { prod: { files: 0, lines: 0 }, tests: { files: 0, lines: 0 }, vendored: { files: 0, lines: 0 } };
541
+ const langFiles = new Tally();
542
+ const langLines = new Tally();
543
+ const dirLines = new Tally();
544
+ const prodIds = new Set<number>();
545
+ const biggest: Array<{ path: string; lines: number }> = [];
546
+ for (const file of files) {
547
+ const g = isVendoredPath(file.path) ? groups.vendored : isTestPath(file.path) ? groups.tests : groups.prod;
548
+ g.files++;
549
+ g.lines += file.line_count;
550
+ if (g !== groups.prod && !(f.includeTests && g === groups.tests)) continue;
551
+ prodIds.add(file.id);
552
+ langFiles.add(file.lang);
553
+ langLines.add(file.lang, file.line_count);
554
+ dirLines.add(dirKey(file.path, depth, f.path), file.line_count);
555
+ biggest.push({ path: file.path, lines: file.line_count });
556
+ }
557
+ const out = [
558
+ `inventory${scopeNote(f)}: ${files.length} indexed source files, ${groups.prod.lines + groups.tests.lines + groups.vendored.lines} lines`,
559
+ ` production: ${groups.prod.files} files, ${groups.prod.lines} lines`,
560
+ ` tests / examples: ${groups.tests.files} files, ${groups.tests.lines} lines`,
561
+ ` vendored / generated: ${groups.vendored.files} files, ${groups.vendored.lines} lines`,
562
+ `The tables below cover production code${f.includeTests ? ' and tests' : ''}.`,
563
+ `by language (${langFiles.size}):`,
564
+ ];
565
+ for (const [lang, lines] of langLines.top(20)) {
566
+ out.push(` ${lang.padEnd(12)} ${String(langFiles.top(100).find(x => x[0] === lang)?.[1] ?? 0).padStart(6)} files ${String(lines).padStart(9)} lines`);
567
+ }
568
+ out.push(...dirLines.lines('lines by dir', limit));
569
+
570
+ const kinds = new Tally();
571
+ const longest: Array<{ qname: string; path: string; line: number; len: number }> = [];
572
+ const members = new Map<number, number>();
573
+ const syms = ctx.db.all<{ id: number; file_id: number; qname: string; kind: string; start_line: number; end_line: number; parent_id: number | null; path: string }>(
574
+ `SELECT s.id, s.file_id, s.qname, s.kind, s.start_line, s.end_line, s.parent_id, f.path FROM symbols s JOIN files f ON f.id = s.file_id`
575
+ );
576
+ const byId = new Map<number, { qname: string; path: string; line: number; kind: string }>();
577
+ for (const s of syms) {
578
+ if (!prodIds.has(s.file_id)) continue;
579
+ kinds.add(s.kind);
580
+ byId.set(s.id, { qname: s.qname, path: s.path, line: s.start_line, kind: s.kind });
581
+ if (s.kind === 'function' || s.kind === 'method') {
582
+ longest.push({ qname: s.qname, path: s.path, line: s.start_line, len: s.end_line - s.start_line + 1 });
583
+ if (s.parent_id !== null) members.set(s.parent_id, (members.get(s.parent_id) ?? 0) + 1);
584
+ }
585
+ }
586
+ out.push(...kinds.lines('symbols by kind', 20));
587
+
588
+ const callable = longest.length;
589
+ longest.sort((a, b) => b.len - a.len);
590
+ const over = (n: number): number => longest.filter(x => x.len > n).length;
591
+ out.push(`functions and methods: ${callable}; longer than 50 lines: ${over(50)}, than 100: ${over(100)}, than 300: ${over(300)}`);
592
+ out.push('longest functions:');
593
+ for (const x of longest.slice(0, 8)) out.push(` ${String(x.len).padStart(6)} lines ${x.qname} ${x.path}:${x.line}`);
594
+ biggest.sort((a, b) => b.lines - a.lines);
595
+ out.push('largest files:');
596
+ for (const x of biggest.slice(0, 8)) out.push(` ${String(x.lines).padStart(6)} lines ${x.path}`);
597
+ const fat = [...members.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8);
598
+ if (fat.length) {
599
+ out.push('types with the most methods:');
600
+ for (const [id, n] of fat) {
601
+ const t = byId.get(id);
602
+ if (t) out.push(` ${String(n).padStart(6)} methods ${t.kind} ${t.qname} ${t.path}:${t.line}`);
603
+ }
604
+ }
605
+ out.push(
606
+ 'More: what=endpoints (routes), what=database (SQL / ORM / query calls), what=imports (external dependencies), ' +
607
+ 'what=symbols name=*Controller, what=subtypes name=BaseClass, what=calls name=..., what=text name=<regex>.'
608
+ );
609
+ return out.join('\n');
610
+ }
611
+
612
+ // --- entry point ------------------------------------------------------------
613
+
614
+ export async function stats(ctx: ToolContext, args: StatsArgs = {}): Promise<string> {
615
+ const note = await ctx.ready();
616
+ const limit = Math.max(1, Math.min(args.limit ?? 25, 500));
617
+ const depth = Math.max(1, Math.min(args.depth ?? 2, 12));
618
+ const f: Filters = { path: args.path?.trim() || undefined, lang: args.lang?.trim() || undefined, includeTests: args.includeTests ?? false };
619
+ const what = args.what ?? (args.name ? 'symbols' : 'inventory');
620
+ let body: string;
621
+ switch (what) {
622
+ case 'symbols': body = symbolStats(ctx, args, f, limit, depth); break;
623
+ case 'calls': body = callStats(ctx, args, f, limit, depth); break;
624
+ case 'subtypes': body = subtypeStats(ctx, args, f, limit, depth); break;
625
+ case 'text': body = await textStats(ctx, args, f, limit, depth); break;
626
+ case 'imports': body = importStats(ctx, f, limit); break;
627
+ case 'endpoints': body = await endpointStats(ctx, f, limit, depth); break;
628
+ case 'database': body = await databaseStats(ctx, f, limit, depth); break;
629
+ case 'inventory': body = inventory(ctx, f, limit, depth); break;
630
+ default: return `error: unknown what=${String(what)}`;
631
+ }
632
+ return note + body;
633
+ }