scip-query 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/IMPROVEMENTS.md +143 -0
  2. package/PLAN.md +320 -0
  3. package/README.md +1213 -0
  4. package/dist/chunk-2QZ23IBN.js +55 -0
  5. package/dist/chunk-2QZ23IBN.js.map +1 -0
  6. package/dist/chunk-36OMT7ZJ.js +144 -0
  7. package/dist/chunk-36OMT7ZJ.js.map +1 -0
  8. package/dist/chunk-3E2X7RIE.js +101 -0
  9. package/dist/chunk-3E2X7RIE.js.map +1 -0
  10. package/dist/chunk-3UOUTZQT.js +45 -0
  11. package/dist/chunk-3UOUTZQT.js.map +1 -0
  12. package/dist/chunk-3ZZJVBIO.js +88 -0
  13. package/dist/chunk-3ZZJVBIO.js.map +1 -0
  14. package/dist/chunk-4TYLS5XX.js +10 -0
  15. package/dist/chunk-4TYLS5XX.js.map +1 -0
  16. package/dist/chunk-5FGUEU7N.js +101 -0
  17. package/dist/chunk-5FGUEU7N.js.map +1 -0
  18. package/dist/chunk-5WTJAXY2.js +61 -0
  19. package/dist/chunk-5WTJAXY2.js.map +1 -0
  20. package/dist/chunk-6NBLIDF4.js +24 -0
  21. package/dist/chunk-6NBLIDF4.js.map +1 -0
  22. package/dist/chunk-6SXADWLW.js +43 -0
  23. package/dist/chunk-6SXADWLW.js.map +1 -0
  24. package/dist/chunk-6VJ6Q7IE.js +65 -0
  25. package/dist/chunk-6VJ6Q7IE.js.map +1 -0
  26. package/dist/chunk-7OZPA5OO.js +258 -0
  27. package/dist/chunk-7OZPA5OO.js.map +1 -0
  28. package/dist/chunk-BEPIEVLR.js +76 -0
  29. package/dist/chunk-BEPIEVLR.js.map +1 -0
  30. package/dist/chunk-BFSCMC22.js +42 -0
  31. package/dist/chunk-BFSCMC22.js.map +1 -0
  32. package/dist/chunk-BP2ATLK2.js +110 -0
  33. package/dist/chunk-BP2ATLK2.js.map +1 -0
  34. package/dist/chunk-CM454WL3.js +114 -0
  35. package/dist/chunk-CM454WL3.js.map +1 -0
  36. package/dist/chunk-DCKMSTJ4.js +74 -0
  37. package/dist/chunk-DCKMSTJ4.js.map +1 -0
  38. package/dist/chunk-DEZKCZXD.js +40 -0
  39. package/dist/chunk-DEZKCZXD.js.map +1 -0
  40. package/dist/chunk-DVWGWHFW.js +99 -0
  41. package/dist/chunk-DVWGWHFW.js.map +1 -0
  42. package/dist/chunk-EMDQWNYR.js +102 -0
  43. package/dist/chunk-EMDQWNYR.js.map +1 -0
  44. package/dist/chunk-FFSWWE5O.js +33 -0
  45. package/dist/chunk-FFSWWE5O.js.map +1 -0
  46. package/dist/chunk-FGXRVW7G.js +73 -0
  47. package/dist/chunk-FGXRVW7G.js.map +1 -0
  48. package/dist/chunk-FUHJCHS4.js +158 -0
  49. package/dist/chunk-FUHJCHS4.js.map +1 -0
  50. package/dist/chunk-GJFURBEW.js +64 -0
  51. package/dist/chunk-GJFURBEW.js.map +1 -0
  52. package/dist/chunk-GTILYBH6.js +102 -0
  53. package/dist/chunk-GTILYBH6.js.map +1 -0
  54. package/dist/chunk-JJP7KQND.js +1 -0
  55. package/dist/chunk-JJP7KQND.js.map +1 -0
  56. package/dist/chunk-JKP5GH6T.js +213 -0
  57. package/dist/chunk-JKP5GH6T.js.map +1 -0
  58. package/dist/chunk-KCBMVQL5.js +38 -0
  59. package/dist/chunk-KCBMVQL5.js.map +1 -0
  60. package/dist/chunk-KVSW5KYP.js +78 -0
  61. package/dist/chunk-KVSW5KYP.js.map +1 -0
  62. package/dist/chunk-LAWMH22O.js +172 -0
  63. package/dist/chunk-LAWMH22O.js.map +1 -0
  64. package/dist/chunk-LB7OS35Q.js +72 -0
  65. package/dist/chunk-LB7OS35Q.js.map +1 -0
  66. package/dist/chunk-LUSIFBXO.js +57 -0
  67. package/dist/chunk-LUSIFBXO.js.map +1 -0
  68. package/dist/chunk-MBVNHJVN.js +44 -0
  69. package/dist/chunk-MBVNHJVN.js.map +1 -0
  70. package/dist/chunk-MGNMHKX3.js +15 -0
  71. package/dist/chunk-MGNMHKX3.js.map +1 -0
  72. package/dist/chunk-N5KEREIA.js +41 -0
  73. package/dist/chunk-N5KEREIA.js.map +1 -0
  74. package/dist/chunk-NDSQYIWT.js +71 -0
  75. package/dist/chunk-NDSQYIWT.js.map +1 -0
  76. package/dist/chunk-NUZ4OMU3.js +28 -0
  77. package/dist/chunk-NUZ4OMU3.js.map +1 -0
  78. package/dist/chunk-QOV2R2WT.js +170 -0
  79. package/dist/chunk-QOV2R2WT.js.map +1 -0
  80. package/dist/chunk-SEFSL2GF.js +78 -0
  81. package/dist/chunk-SEFSL2GF.js.map +1 -0
  82. package/dist/chunk-T6ARFSBZ.js +103 -0
  83. package/dist/chunk-T6ARFSBZ.js.map +1 -0
  84. package/dist/chunk-TBP6BICL.js +46 -0
  85. package/dist/chunk-TBP6BICL.js.map +1 -0
  86. package/dist/chunk-TDNNOR6D.js +97 -0
  87. package/dist/chunk-TDNNOR6D.js.map +1 -0
  88. package/dist/chunk-TSPZOMHC.js +195 -0
  89. package/dist/chunk-TSPZOMHC.js.map +1 -0
  90. package/dist/chunk-UNTPVD36.js +55 -0
  91. package/dist/chunk-UNTPVD36.js.map +1 -0
  92. package/dist/chunk-VRUJH4BO.js +88 -0
  93. package/dist/chunk-VRUJH4BO.js.map +1 -0
  94. package/dist/chunk-VZ7AMAFL.js +76 -0
  95. package/dist/chunk-VZ7AMAFL.js.map +1 -0
  96. package/dist/chunk-XFXDXEUN.js +24 -0
  97. package/dist/chunk-XFXDXEUN.js.map +1 -0
  98. package/dist/chunk-YZAA4LYG.js +169 -0
  99. package/dist/chunk-YZAA4LYG.js.map +1 -0
  100. package/dist/chunk-Z73NYSBZ.js +92 -0
  101. package/dist/chunk-Z73NYSBZ.js.map +1 -0
  102. package/dist/chunk-ZJRYBOEE.js +125 -0
  103. package/dist/chunk-ZJRYBOEE.js.map +1 -0
  104. package/dist/cli.js +5798 -0
  105. package/dist/cli.js.map +1 -0
  106. package/dist/db-BxaevAyc.d.ts +683 -0
  107. package/dist/index.d.ts +254 -0
  108. package/dist/index.js +1271 -0
  109. package/dist/index.js.map +1 -0
  110. package/dist/postinstall.js +167 -0
  111. package/dist/postinstall.js.map +1 -0
  112. package/dist/queries/affected.d.ts +14 -0
  113. package/dist/queries/affected.js +9 -0
  114. package/dist/queries/affected.js.map +1 -0
  115. package/dist/queries/bottlenecks.d.ts +18 -0
  116. package/dist/queries/bottlenecks.js +8 -0
  117. package/dist/queries/bottlenecks.js.map +1 -0
  118. package/dist/queries/by-kind.d.ts +20 -0
  119. package/dist/queries/by-kind.js +10 -0
  120. package/dist/queries/by-kind.js.map +1 -0
  121. package/dist/queries/call-graph.d.ts +13 -0
  122. package/dist/queries/call-graph.js +9 -0
  123. package/dist/queries/call-graph.js.map +1 -0
  124. package/dist/queries/change-surface.d.ts +10 -0
  125. package/dist/queries/change-surface.js +9 -0
  126. package/dist/queries/change-surface.js.map +1 -0
  127. package/dist/queries/clean-signature.d.ts +9 -0
  128. package/dist/queries/clean-signature.js +7 -0
  129. package/dist/queries/clean-signature.js.map +1 -0
  130. package/dist/queries/code.d.ts +17 -0
  131. package/dist/queries/code.js +9 -0
  132. package/dist/queries/code.js.map +1 -0
  133. package/dist/queries/complexity-hotspots.d.ts +19 -0
  134. package/dist/queries/complexity-hotspots.js +9 -0
  135. package/dist/queries/complexity-hotspots.js.map +1 -0
  136. package/dist/queries/complexity.d.ts +13 -0
  137. package/dist/queries/complexity.js +9 -0
  138. package/dist/queries/complexity.js.map +1 -0
  139. package/dist/queries/convergence.d.ts +11 -0
  140. package/dist/queries/convergence.js +9 -0
  141. package/dist/queries/convergence.js.map +1 -0
  142. package/dist/queries/coupling.d.ts +17 -0
  143. package/dist/queries/coupling.js +9 -0
  144. package/dist/queries/coupling.js.map +1 -0
  145. package/dist/queries/cycles.d.ts +16 -0
  146. package/dist/queries/cycles.js +8 -0
  147. package/dist/queries/cycles.js.map +1 -0
  148. package/dist/queries/dataflow.d.ts +19 -0
  149. package/dist/queries/dataflow.js +9 -0
  150. package/dist/queries/dataflow.js.map +1 -0
  151. package/dist/queries/dead.d.ts +10 -0
  152. package/dist/queries/dead.js +9 -0
  153. package/dist/queries/dead.js.map +1 -0
  154. package/dist/queries/deep-chains.d.ts +16 -0
  155. package/dist/queries/deep-chains.js +8 -0
  156. package/dist/queries/deep-chains.js.map +1 -0
  157. package/dist/queries/deps.d.ts +9 -0
  158. package/dist/queries/deps.js +9 -0
  159. package/dist/queries/deps.js.map +1 -0
  160. package/dist/queries/diff-impact.d.ts +13 -0
  161. package/dist/queries/diff-impact.js +9 -0
  162. package/dist/queries/diff-impact.js.map +1 -0
  163. package/dist/queries/doc-coverage.d.ts +14 -0
  164. package/dist/queries/doc-coverage.js +8 -0
  165. package/dist/queries/doc-coverage.js.map +1 -0
  166. package/dist/queries/drift.d.ts +25 -0
  167. package/dist/queries/drift.js +8 -0
  168. package/dist/queries/drift.js.map +1 -0
  169. package/dist/queries/extract-candidates.d.ts +25 -0
  170. package/dist/queries/extract-candidates.js +9 -0
  171. package/dist/queries/extract-candidates.js.map +1 -0
  172. package/dist/queries/fan.d.ts +29 -0
  173. package/dist/queries/fan.js +14 -0
  174. package/dist/queries/fan.js.map +1 -0
  175. package/dist/queries/files.d.ts +6 -0
  176. package/dist/queries/files.js +7 -0
  177. package/dist/queries/files.js.map +1 -0
  178. package/dist/queries/health.d.ts +18 -0
  179. package/dist/queries/health.js +21 -0
  180. package/dist/queries/health.js.map +1 -0
  181. package/dist/queries/hierarchy.d.ts +13 -0
  182. package/dist/queries/hierarchy.js +8 -0
  183. package/dist/queries/hierarchy.js.map +1 -0
  184. package/dist/queries/hotspots.d.ts +13 -0
  185. package/dist/queries/hotspots.js +8 -0
  186. package/dist/queries/hotspots.js.map +1 -0
  187. package/dist/queries/imports.d.ts +19 -0
  188. package/dist/queries/imports.js +12 -0
  189. package/dist/queries/imports.js.map +1 -0
  190. package/dist/queries/index.d.ts +47 -0
  191. package/dist/queries/index.js +207 -0
  192. package/dist/queries/index.js.map +1 -0
  193. package/dist/queries/isolated.d.ts +14 -0
  194. package/dist/queries/isolated.js +9 -0
  195. package/dist/queries/isolated.js.map +1 -0
  196. package/dist/queries/members.d.ts +10 -0
  197. package/dist/queries/members.js +8 -0
  198. package/dist/queries/members.js.map +1 -0
  199. package/dist/queries/methods.d.ts +6 -0
  200. package/dist/queries/methods.js +8 -0
  201. package/dist/queries/methods.js.map +1 -0
  202. package/dist/queries/outline.d.ts +10 -0
  203. package/dist/queries/outline.js +8 -0
  204. package/dist/queries/outline.js.map +1 -0
  205. package/dist/queries/passthrough-candidates.d.ts +18 -0
  206. package/dist/queries/passthrough-candidates.js +9 -0
  207. package/dist/queries/passthrough-candidates.js.map +1 -0
  208. package/dist/queries/redundant-reexports.d.ts +22 -0
  209. package/dist/queries/redundant-reexports.js +8 -0
  210. package/dist/queries/redundant-reexports.js.map +1 -0
  211. package/dist/queries/refs.d.ts +6 -0
  212. package/dist/queries/refs.js +7 -0
  213. package/dist/queries/refs.js.map +1 -0
  214. package/dist/queries/similar-chains.d.ts +29 -0
  215. package/dist/queries/similar-chains.js +8 -0
  216. package/dist/queries/similar-chains.js.map +1 -0
  217. package/dist/queries/similar-files.d.ts +19 -0
  218. package/dist/queries/similar-files.js +8 -0
  219. package/dist/queries/similar-files.js.map +1 -0
  220. package/dist/queries/similar-signatures.d.ts +21 -0
  221. package/dist/queries/similar-signatures.js +8 -0
  222. package/dist/queries/similar-signatures.js.map +1 -0
  223. package/dist/queries/similar.d.ts +34 -0
  224. package/dist/queries/similar.js +11 -0
  225. package/dist/queries/similar.js.map +1 -0
  226. package/dist/queries/slice.d.ts +21 -0
  227. package/dist/queries/slice.js +9 -0
  228. package/dist/queries/slice.js.map +1 -0
  229. package/dist/queries/stale-abstractions.d.ts +18 -0
  230. package/dist/queries/stale-abstractions.js +9 -0
  231. package/dist/queries/stale-abstractions.js.map +1 -0
  232. package/dist/queries/stats.d.ts +6 -0
  233. package/dist/queries/stats.js +7 -0
  234. package/dist/queries/stats.js.map +1 -0
  235. package/dist/queries/surface.d.ts +7 -0
  236. package/dist/queries/surface.js +8 -0
  237. package/dist/queries/surface.js.map +1 -0
  238. package/dist/queries/symbols.d.ts +6 -0
  239. package/dist/queries/symbols.js +9 -0
  240. package/dist/queries/symbols.js.map +1 -0
  241. package/dist/queries/system.d.ts +7 -0
  242. package/dist/queries/system.js +9 -0
  243. package/dist/queries/system.js.map +1 -0
  244. package/dist/queries/test-coverage.d.ts +22 -0
  245. package/dist/queries/test-coverage.js +11 -0
  246. package/dist/queries/test-coverage.js.map +1 -0
  247. package/dist/queries/trace.d.ts +6 -0
  248. package/dist/queries/trace.js +8 -0
  249. package/dist/queries/trace.js.map +1 -0
  250. package/dist/queries/wrapper-candidates.d.ts +17 -0
  251. package/dist/queries/wrapper-candidates.js +9 -0
  252. package/dist/queries/wrapper-candidates.js.map +1 -0
  253. package/dist/reindex-worker.js +368 -0
  254. package/dist/reindex-worker.js.map +1 -0
  255. package/docs/AGENT_GUIDE.md +359 -0
  256. package/package.json +70 -0
  257. package/reports/debloat/2026-04-10-scip-query-self-audit.md +161 -0
  258. package/skills/concrete-plan/SKILL.md +318 -0
  259. package/skills/scip-debloat/SKILL.md +413 -0
  260. package/skills/scip-explore/SKILL.md +235 -0
  261. package/skills/scip-verify/SKILL.md +323 -0
  262. package/src/cli.ts +1480 -0
  263. package/src/config.ts +117 -0
  264. package/src/db.ts +127 -0
  265. package/src/gitignore-filter.ts +143 -0
  266. package/src/index.ts +11 -0
  267. package/src/postinstall.ts +8 -0
  268. package/src/queries/affected.ts +86 -0
  269. package/src/queries/bottlenecks.ts +67 -0
  270. package/src/queries/by-kind.ts +204 -0
  271. package/src/queries/call-graph.ts +66 -0
  272. package/src/queries/change-surface.ts +110 -0
  273. package/src/queries/clean-signature.ts +22 -0
  274. package/src/queries/code.ts +101 -0
  275. package/src/queries/complexity-hotspots.ts +119 -0
  276. package/src/queries/complexity.ts +152 -0
  277. package/src/queries/convergence.ts +82 -0
  278. package/src/queries/coupling.ts +99 -0
  279. package/src/queries/cycles.ts +78 -0
  280. package/src/queries/dataflow.ts +128 -0
  281. package/src/queries/dead.ts +122 -0
  282. package/src/queries/deep-chains.ts +59 -0
  283. package/src/queries/deps.ts +46 -0
  284. package/src/queries/diff-impact.ts +204 -0
  285. package/src/queries/doc-coverage.ts +86 -0
  286. package/src/queries/drift.ts +224 -0
  287. package/src/queries/extract-candidates.ts +167 -0
  288. package/src/queries/fan.ts +148 -0
  289. package/src/queries/files.ts +16 -0
  290. package/src/queries/health.ts +324 -0
  291. package/src/queries/hierarchy.ts +49 -0
  292. package/src/queries/hotspots.ts +53 -0
  293. package/src/queries/imports.ts +95 -0
  294. package/src/queries/index.ts +45 -0
  295. package/src/queries/isolated.ts +67 -0
  296. package/src/queries/members.ts +54 -0
  297. package/src/queries/methods.ts +27 -0
  298. package/src/queries/outline.ts +52 -0
  299. package/src/queries/passthrough-candidates.ts +94 -0
  300. package/src/queries/redundant-reexports.ts +170 -0
  301. package/src/queries/refs.ts +27 -0
  302. package/src/queries/similar-chains.ts +314 -0
  303. package/src/queries/similar-files.ts +140 -0
  304. package/src/queries/similar-signatures.ts +151 -0
  305. package/src/queries/similar.ts +305 -0
  306. package/src/queries/slice.ts +154 -0
  307. package/src/queries/stale-abstractions.ts +82 -0
  308. package/src/queries/stats.ts +22 -0
  309. package/src/queries/surface.ts +34 -0
  310. package/src/queries/symbols.ts +39 -0
  311. package/src/queries/system.ts +86 -0
  312. package/src/queries/test-coverage.ts +106 -0
  313. package/src/queries/trace.ts +55 -0
  314. package/src/queries/wrapper-candidates.ts +112 -0
  315. package/src/query-support.ts +226 -0
  316. package/src/reindex/detect.ts +58 -0
  317. package/src/reindex/index.ts +153 -0
  318. package/src/reindex/indexers.ts +220 -0
  319. package/src/reindex/install.ts +125 -0
  320. package/src/reindex-worker.ts +35 -0
  321. package/src/setup.ts +202 -0
  322. package/src/symbol-parser.ts +278 -0
  323. package/src/types.ts +654 -0
  324. package/src/watch.ts +274 -0
  325. package/tests/gitignore-filter.test.ts +48 -0
  326. package/tests/queries.test.ts +300 -0
  327. package/tests/symbol-parser.test.ts +157 -0
  328. package/tsconfig.json +20 -0
  329. package/tsup.config.ts +40 -0
  330. package/vitest.config.ts +7 -0
package/src/cli.ts ADDED
@@ -0,0 +1,1480 @@
1
+ import { program } from 'commander';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { ScipDatabase } from './db.js';
5
+ import { createGitignoreFilter } from './gitignore-filter.js';
6
+ import { loadProjectConfig, resolveIndexPaths, initProjectConfig } from './config.js';
7
+ import { reindex, detectLanguages } from './reindex/index.js';
8
+ import { Watcher } from './watch.js';
9
+ import * as queries from './queries/index.js';
10
+ import type { ScipQueryConfig, DeadOptions, WatcherStatus } from './types.js';
11
+ import { installSkills, isScipInstalled, printScipInstallInstructions } from './setup.js';
12
+
13
+ // ── Helpers ────────────────────────────────────────────────
14
+
15
+ function resolveProjectRoot(): string {
16
+ return process.env['SCIP_QUERY_PROJECT_ROOT'] ?? process.cwd();
17
+ }
18
+
19
+ function openDb(): ScipDatabase {
20
+ const projectRoot = resolveProjectRoot();
21
+ const config = loadProjectConfig(projectRoot);
22
+ const paths = resolveIndexPaths(projectRoot, config);
23
+
24
+ // Also check legacy location (project root) for backwards compat
25
+ const dbPath = process.env['SCIP_QUERY_INDEX_DB']
26
+ ?? (existsSync(paths.dbPath) ? paths.dbPath : join(projectRoot, 'index.db'));
27
+
28
+ if (!existsSync(dbPath)) {
29
+ console.error(`error: No index.db found. Run: scip-query reindex`);
30
+ process.exit(1);
31
+ }
32
+
33
+ const dbConfig: ScipQueryConfig = {
34
+ dbPath,
35
+ indexPath: process.env['SCIP_QUERY_INDEX_SCIP'] ?? paths.indexPath,
36
+ projectRoot,
37
+ };
38
+
39
+ const filter = createGitignoreFilter(projectRoot);
40
+ return new ScipDatabase(dbConfig, filter);
41
+ }
42
+
43
+ function withDb(run: (db: ScipDatabase) => void): void {
44
+ const db = openDb();
45
+ try {
46
+ run(db);
47
+ } finally {
48
+ db.close();
49
+ }
50
+ }
51
+
52
+ function runQuery<T>(
53
+ query: (db: ScipDatabase) => T,
54
+ render: (result: T) => void,
55
+ ): void {
56
+ withDb((db) => {
57
+ render(query(db));
58
+ });
59
+ }
60
+
61
+ // ── CLI Definition ─────────────────────────────────────────
62
+
63
+ program
64
+ .name('scip-query')
65
+ .description('Language-agnostic code intelligence CLI powered by SCIP indexes')
66
+ .version('0.1.0');
67
+
68
+ // reindex
69
+ program
70
+ .command('reindex')
71
+ .description('Index the codebase and convert to SQLite')
72
+ .option('-l, --language <lang>', 'Index only this language (can be repeated)', collect, [])
73
+ .option('--pnpm-workspaces', 'Enable pnpm workspace support (TypeScript)')
74
+ .action(async (opts) => {
75
+ const projectRoot = resolveProjectRoot();
76
+ try {
77
+ const result = await reindex({
78
+ projectRoot,
79
+ languages: opts.language.length > 0 ? opts.language : undefined,
80
+ pnpmWorkspaces: opts.pnpmWorkspaces,
81
+ });
82
+ console.log(`Indexed ${result.languages.join(', ')} in ${(result.durationMs / 1000).toFixed(1)}s`);
83
+ } catch (err) {
84
+ console.error(`error: ${err instanceof Error ? err.message : err}`);
85
+ process.exit(1);
86
+ }
87
+ });
88
+
89
+ // stats
90
+ program
91
+ .command('stats')
92
+ .description('Show index statistics')
93
+ .action(() => {
94
+ runQuery(
95
+ (db) => queries.stats(db),
96
+ (s) => {
97
+ console.log(`Documents: ${s.documents}`);
98
+ console.log(`Symbols: ${s.symbols}`);
99
+ console.log(`Definitions: ${s.definitions}`);
100
+ console.log(`References: ${s.references}`);
101
+ console.log(`Index size: ${formatBytes(s.indexSizeBytes)}`);
102
+ if (s.lastBuilt) {
103
+ console.log(`Last built: ${s.lastBuilt.toISOString().replace('T', ' ').slice(0, 19)}`);
104
+ }
105
+ },
106
+ );
107
+ });
108
+
109
+ // files
110
+ program
111
+ .command('files <pattern>')
112
+ .description('Find files matching a pattern')
113
+ .action((pattern) => {
114
+ runQuery(
115
+ (db) => queries.files(db, pattern),
116
+ (results) => {
117
+ for (const r of results) console.log(r.relativePath);
118
+ },
119
+ );
120
+ });
121
+
122
+ // symbols
123
+ program
124
+ .command('symbols <file>')
125
+ .description('List symbols defined in a file (with line ranges + signatures)')
126
+ .action((file) => {
127
+ runQuery(
128
+ (db) => queries.symbols(db, file),
129
+ (results) => {
130
+ for (const r of results) {
131
+ const sig = r.signature ? ` — ${r.signature}` : '';
132
+ console.log(` ${r.startLine}-${r.endLine} ${r.shortName}${sig}`);
133
+ }
134
+ },
135
+ );
136
+ });
137
+
138
+ // methods
139
+ program
140
+ .command('methods <className>')
141
+ .description('List methods of a class (with line ranges)')
142
+ .action((className) => {
143
+ runQuery(
144
+ (db) => queries.methods(db, className),
145
+ (results) => {
146
+ for (const r of results) {
147
+ console.log(` ${r.startLine}-${r.endLine} ${r.name}`);
148
+ }
149
+ },
150
+ );
151
+ });
152
+
153
+ // refs
154
+ program
155
+ .command('refs <symbol>')
156
+ .description('Find all files referencing a symbol')
157
+ .action((symbol) => {
158
+ runQuery(
159
+ (db) => queries.refs(db, symbol),
160
+ (results) => {
161
+ let prevFile = '';
162
+ for (const r of results) {
163
+ if (r.relativePath !== prevFile) {
164
+ if (prevFile) console.log('');
165
+ console.log(r.relativePath);
166
+ prevFile = r.relativePath;
167
+ }
168
+ console.log(` line ${r.line}`);
169
+ }
170
+ },
171
+ );
172
+ });
173
+
174
+ // trace
175
+ program
176
+ .command('trace <symbol>')
177
+ .description('Trace a symbol: definition + all references')
178
+ .action((symbol) => {
179
+ runQuery(
180
+ (db) => queries.trace(db, symbol),
181
+ (result) => {
182
+ console.log('═══ DEFINITION ═══');
183
+ for (const d of result.definitions) {
184
+ const sig = d.signature ? ` — ${d.signature}` : '';
185
+ console.log(` ${d.relativePath}:${d.startLine}-${d.endLine}${sig}`);
186
+ }
187
+
188
+ console.log('\n═══ REFERENCED BY ═══');
189
+ for (const ref of result.referencedBy) {
190
+ console.log(` ${ref}`);
191
+ }
192
+ },
193
+ );
194
+ });
195
+
196
+ // deps
197
+ program
198
+ .command('deps <file>')
199
+ .description('Files this file depends on (internal)')
200
+ .action((file) => {
201
+ runQuery(
202
+ (db) => queries.deps(db, file),
203
+ (results) => {
204
+ for (const r of results) console.log(r.relativePath);
205
+ },
206
+ );
207
+ });
208
+
209
+ // rdeps
210
+ program
211
+ .command('rdeps <file>')
212
+ .description('Files that depend on this file/module')
213
+ .action((file) => {
214
+ runQuery(
215
+ (db) => queries.rdeps(db, file),
216
+ (results) => {
217
+ for (const r of results) console.log(r.relativePath);
218
+ },
219
+ );
220
+ });
221
+
222
+ // system
223
+ program
224
+ .command('system <module>')
225
+ .description('Full module map: files, symbols, deps in/out')
226
+ .action((module) => {
227
+ runQuery(
228
+ (db) => queries.system(db, module),
229
+ (result) => {
230
+ console.log('═══ FILES ═══');
231
+ for (const f of result.files) console.log(f);
232
+
233
+ console.log('\n═══ EXPORTED SYMBOLS ═══');
234
+ for (const s of result.symbols) {
235
+ console.log(` ${s.startLine}-${s.endLine} ${s.shortName}`);
236
+ }
237
+
238
+ console.log('\n═══ DEPENDS ON (internal) ═══');
239
+ for (const d of result.dependsOn) console.log(` ${d}`);
240
+
241
+ console.log('\n═══ DEPENDED ON BY ═══');
242
+ for (const d of result.dependedOnBy) console.log(` ${d}`);
243
+ },
244
+ );
245
+ });
246
+
247
+ // surface
248
+ program
249
+ .command('surface <module>')
250
+ .description('What symbols consumers actually use from this module')
251
+ .action((module) => {
252
+ runQuery(
253
+ (db) => queries.surface(db, module),
254
+ (results) => {
255
+ for (const r of results) {
256
+ console.log(` ${r.consumer} → ${r.shortName}`);
257
+ }
258
+ },
259
+ );
260
+ });
261
+
262
+ // dead
263
+ program
264
+ .command('dead [scope]')
265
+ .description('Find dead code and file-internal symbols (no cross-file consumers)')
266
+ .option('--min-loc <n>', 'Only show symbols >= N lines', parseIntSafe, 1)
267
+ .option('--include-tests', 'Include test files')
268
+ .option('--skip-barrels', 'Ignore refs from barrel re-export files')
269
+ .option('--include-members', 'Include class members')
270
+ .action((scope, opts) => {
271
+ withDb((db) => {
272
+ const deadOpts: DeadOptions = {
273
+ scope: scope || undefined,
274
+ minLoc: opts.minLoc,
275
+ includeTests: opts.includeTests,
276
+ skipBarrels: opts.skipBarrels,
277
+ includeMembers: opts.includeMembers,
278
+ };
279
+
280
+ const result = queries.dead(db, deadOpts);
281
+
282
+ if (result.symbols.length === 0) {
283
+ console.log('No dead code found.');
284
+ return;
285
+ }
286
+
287
+ let prevFile = '';
288
+ for (const s of result.symbols) {
289
+ if (s.relativePath !== prevFile) {
290
+ if (prevFile) console.log('');
291
+ console.log(s.relativePath);
292
+ prevFile = s.relativePath;
293
+ }
294
+ const tag = s.kind === 'dead-code' ? '[dead code]' : '[file-internal only]';
295
+ console.log(` ${s.startLine}-${s.endLine} (${s.loc} LOC) ${s.shortName} ${tag}`);
296
+ }
297
+
298
+ console.log('\n───────────────────────────');
299
+ console.log(
300
+ `Total: ${result.totalCount} symbols (${result.deadCodeCount} dead code, ` +
301
+ `${result.fileInternalCount} file-internal), ${result.totalLoc} LOC`,
302
+ );
303
+ });
304
+ });
305
+
306
+ // hotspots
307
+ program
308
+ .command('hotspots')
309
+ .description('Most-referenced symbols in the codebase (choke points)')
310
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 30)
311
+ .option('-s, --scope <path>', 'Limit to files matching path')
312
+ .action((opts) => {
313
+ runQuery(
314
+ (db) => queries.hotspots(db, { limit: opts.limit, scope: opts.scope }),
315
+ (results) => {
316
+ console.log(' refs files symbol');
317
+ console.log(' ──── ───── ──────');
318
+ for (const r of results) {
319
+ console.log(` ${String(r.refCount).padStart(4)} ${String(r.fileCount).padStart(5)} ${r.shortName}`);
320
+ }
321
+ },
322
+ );
323
+ });
324
+
325
+ // imports
326
+ program
327
+ .command('imports <file>')
328
+ .description('What symbols does this file import?')
329
+ .action((file) => {
330
+ runQuery(
331
+ (db) => queries.imports(db, file),
332
+ (results) => {
333
+ if (results.length === 0) {
334
+ console.log('No imports found (indexer may not emit role=2 for this language).');
335
+ }
336
+ for (const r of results) {
337
+ console.log(` ${r.shortName} ← ${r.fromFile}`);
338
+ }
339
+ },
340
+ );
341
+ });
342
+
343
+ // imported-by
344
+ program
345
+ .command('imported-by <symbol>')
346
+ .description('Which files import this symbol?')
347
+ .action((symbol) => {
348
+ runQuery(
349
+ (db) => queries.importedBy(db, symbol),
350
+ (results) => {
351
+ for (const r of results) {
352
+ console.log(` ${r.fromFile}`);
353
+ }
354
+ },
355
+ );
356
+ });
357
+
358
+ // unused-imports
359
+ program
360
+ .command('unused-imports <file>')
361
+ .description('Find imports not referenced in the same file')
362
+ .action((file) => {
363
+ runQuery(
364
+ (db) => queries.unusedImports(db, file),
365
+ (results) => {
366
+ if (results.length === 0) {
367
+ console.log('No unused imports found.');
368
+ } else {
369
+ for (const r of results) {
370
+ console.log(` ${r.shortName} in ${r.importedIn}`);
371
+ }
372
+ console.log(`\n${results.length} unused import(s)`);
373
+ }
374
+ },
375
+ );
376
+ });
377
+
378
+ // outline
379
+ program
380
+ .command('outline <file>')
381
+ .description('Tree view of symbols in a file (using nesting hierarchy)')
382
+ .action((file) => {
383
+ runQuery(
384
+ (db) => queries.outline(db, file),
385
+ (roots) => {
386
+ function printTree(nodes: typeof roots, indent: number): void {
387
+ for (const n of nodes) {
388
+ const prefix = ' '.repeat(indent);
389
+ console.log(`${prefix}${n.startLine}-${n.endLine} ${n.shortName}`);
390
+ printTree(n.children, indent + 1);
391
+ }
392
+ }
393
+ printTree(roots, 0);
394
+ },
395
+ );
396
+ });
397
+
398
+ // members
399
+ program
400
+ .command('members <symbol>')
401
+ .description('All children of a symbol (methods, fields, nested types)')
402
+ .action((symbol) => {
403
+ runQuery(
404
+ (db) => queries.members(db, symbol),
405
+ (results) => {
406
+ for (const r of results) {
407
+ console.log(` ${r.startLine}-${r.endLine} [${r.kind}] ${r.shortName}`);
408
+ }
409
+ },
410
+ );
411
+ });
412
+
413
+ // fan-in
414
+ program
415
+ .command('fan-in [symbol]')
416
+ .description('How many files reference a symbol (or top fan-in across codebase)')
417
+ .option('-n, --limit <n>', 'Number of results for top mode', parseIntSafe, 30)
418
+ .option('-s, --scope <path>', 'Limit to files matching path')
419
+ .action((symbol, opts) => {
420
+ withDb((db) => {
421
+ if (symbol) {
422
+ const results = queries.fanIn(db, symbol);
423
+ for (const r of results) {
424
+ console.log(` ${String(r.count).padStart(4)} files ${r.name}`);
425
+ }
426
+ } else {
427
+ const results = queries.topFanIn(db, { limit: opts.limit, scope: opts.scope });
428
+ console.log(' files symbol');
429
+ console.log(' ───── ──────');
430
+ for (const r of results) {
431
+ console.log(` ${String(r.count).padStart(5)} ${r.name}`);
432
+ }
433
+ }
434
+ });
435
+ });
436
+
437
+ // fan-out
438
+ program
439
+ .command('fan-out [file]')
440
+ .description('How many external symbols a file uses (or top fan-out across codebase)')
441
+ .option('-n, --limit <n>', 'Number of results for top mode', parseIntSafe, 30)
442
+ .option('-s, --scope <path>', 'Limit to files matching path')
443
+ .action((file, opts) => {
444
+ withDb((db) => {
445
+ if (file) {
446
+ const results = queries.fanOut(db, file);
447
+ for (const r of results) {
448
+ console.log(` ${String(r.count).padStart(4)} symbols ${r.name}`);
449
+ }
450
+ } else {
451
+ const results = queries.topFanOut(db, { limit: opts.limit, scope: opts.scope });
452
+ console.log(' symbols file');
453
+ console.log(' ─────── ────');
454
+ for (const r of results) {
455
+ console.log(` ${String(r.count).padStart(7)} ${r.name}`);
456
+ }
457
+ }
458
+ });
459
+ });
460
+
461
+ // coupling
462
+ program
463
+ .command('coupling [file1] [file2]')
464
+ .description('Coupling between two files, or top coupled pairs in codebase')
465
+ .option('-n, --limit <n>', 'Number of results for top mode', parseIntSafe, 20)
466
+ .option('-s, --scope <path>', 'Limit to files matching path')
467
+ .action((file1, file2, opts) => {
468
+ withDb((db) => {
469
+ if (file1 && file2) {
470
+ const result = queries.coupling(db, file1, file2);
471
+ console.log(`${result.file1} ↔ ${result.file2}: ${result.sharedSymbols} shared symbols`);
472
+ } else {
473
+ const results = queries.topCoupling(db, { limit: opts.limit, scope: opts.scope });
474
+ console.log(' shared file1 → file2');
475
+ console.log(' ────── ─────────────');
476
+ for (const r of results) {
477
+ console.log(` ${String(r.sharedSymbols).padStart(6)} ${r.file1} → ${r.file2}`);
478
+ }
479
+ }
480
+ });
481
+ });
482
+
483
+ // cycles
484
+ program
485
+ .command('cycles')
486
+ .description('Detect circular dependency chains between files')
487
+ .option('-s, --scope <path>', 'Limit to files matching path')
488
+ .option('--max-depth <n>', 'Maximum cycle depth', parseIntSafe, 10)
489
+ .action((opts) => {
490
+ runQuery(
491
+ (db) => queries.cycles(db, { scope: opts.scope, maxDepth: opts.maxDepth }),
492
+ (results) => {
493
+ if (results.length === 0) {
494
+ console.log('No circular dependencies found.');
495
+ } else {
496
+ for (let i = 0; i < results.length; i++) {
497
+ console.log(`\nCycle ${i + 1} (${results[i]!.path.length - 1} files):`);
498
+ for (let j = 0; j < results[i]!.path.length; j++) {
499
+ const arrow = j < results[i]!.path.length - 1 ? ' →' : ' (cycle)';
500
+ console.log(` ${results[i]!.path[j]}${arrow}`);
501
+ }
502
+ }
503
+ console.log(`\n${results.length} cycle(s) found.`);
504
+ }
505
+ },
506
+ );
507
+ });
508
+
509
+ // bottlenecks
510
+ program
511
+ .command('bottlenecks')
512
+ .description('Find coupling hubs: high fan-in AND high fan-out')
513
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 20)
514
+ .option('-s, --scope <path>', 'Limit to files matching path')
515
+ .option('--min-fan-in <n>', 'Minimum fan-in', parseIntSafe, 2)
516
+ .option('--min-fan-out <n>', 'Minimum fan-out', parseIntSafe, 2)
517
+ .action((opts) => {
518
+ runQuery(
519
+ (db) => queries.bottlenecks(db, {
520
+ limit: opts.limit,
521
+ scope: opts.scope,
522
+ minFanIn: opts.minFanIn,
523
+ minFanOut: opts.minFanOut,
524
+ }),
525
+ (results) => {
526
+ if (results.length === 0) {
527
+ console.log('No bottlenecks found.');
528
+ } else {
529
+ console.log(' score fan-in fan-out symbol');
530
+ console.log(' ───── ────── ─────── ──────');
531
+ for (const r of results) {
532
+ console.log(` ${String(r.score).padStart(5)} ${String(r.fanIn).padStart(6)} ${String(r.fanOut).padStart(7)} ${r.shortName}`);
533
+ }
534
+ }
535
+ },
536
+ );
537
+ });
538
+
539
+ // isolated
540
+ program
541
+ .command('isolated')
542
+ .description('Find completely orphaned symbols (no references at all)')
543
+ .option('-s, --scope <path>', 'Limit to files matching path')
544
+ .option('--min-loc <n>', 'Minimum lines of code', parseIntSafe, 3)
545
+ .action((opts) => {
546
+ runQuery(
547
+ (db) => queries.isolated(db, { scope: opts.scope, minLoc: opts.minLoc }),
548
+ (results) => {
549
+ if (results.length === 0) {
550
+ console.log('No isolated symbols found.');
551
+ } else {
552
+ let prevFile = '';
553
+ for (const r of results) {
554
+ if (r.relativePath !== prevFile) {
555
+ if (prevFile) console.log('');
556
+ console.log(r.relativePath);
557
+ prevFile = r.relativePath;
558
+ }
559
+ console.log(` ${r.startLine}-${r.endLine} (${r.loc} LOC) ${r.shortName}`);
560
+ }
561
+ console.log(`\n${results.length} isolated symbol(s)`);
562
+ }
563
+ },
564
+ );
565
+ });
566
+
567
+ // by-kind
568
+ program
569
+ .command('by-kind <kind>')
570
+ .description('Find symbols by SCIP kind (class, interface, enum, function, etc.)')
571
+ .option('-s, --scope <path>', 'Limit to files matching path')
572
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 100)
573
+ .action((kind, opts) => {
574
+ runQuery(
575
+ (db) => queries.byKind(db, kind, { scope: opts.scope, limit: opts.limit }),
576
+ (results) => {
577
+ if (results.length === 0) {
578
+ console.log(`No symbols found for kind "${kind}". Use "kind-counts" to see available kinds.`);
579
+ } else {
580
+ for (const r of results) {
581
+ console.log(` ${r.relativePath}:${r.startLine}-${r.endLine} [${r.kindName}] ${r.shortName}`);
582
+ }
583
+ console.log(`\n${results.length} symbol(s)`);
584
+ }
585
+ },
586
+ );
587
+ });
588
+
589
+ // kind-counts
590
+ program
591
+ .command('kind-counts')
592
+ .description('Histogram of symbol kinds in the codebase')
593
+ .option('-s, --scope <path>', 'Limit to files matching path')
594
+ .action((opts) => {
595
+ runQuery(
596
+ (db) => queries.kindCounts(db, { scope: opts.scope }),
597
+ (results) => {
598
+ console.log(' count kind');
599
+ console.log(' ───── ────');
600
+ for (const r of results) {
601
+ console.log(` ${String(r.count).padStart(5)} ${r.kindName} (${r.kind})`);
602
+ }
603
+ },
604
+ );
605
+ });
606
+
607
+ // test-coverage
608
+ program
609
+ .command('test-coverage [symbol]')
610
+ .description('Check if symbols are referenced by test files')
611
+ .option('-s, --scope <path>', 'Limit to files matching path')
612
+ .option('--min-loc <n>', 'Minimum LOC for summary mode', parseIntSafe, 3)
613
+ .action((symbol, opts) => {
614
+ withDb((db) => {
615
+ if (symbol) {
616
+ const results = queries.testCoverage(db, symbol);
617
+ for (const r of results) {
618
+ const status = r.covered ? 'covered' : 'NOT COVERED';
619
+ console.log(` [${status}] ${r.shortName} (${r.definedIn})`);
620
+ for (const tf of r.testFiles) {
621
+ console.log(` ← ${tf}`);
622
+ }
623
+ }
624
+ } else {
625
+ const summary = queries.testCoverageSummary(db, { scope: opts.scope, minLoc: opts.minLoc });
626
+ console.log(`Test coverage: ${summary.percent}%`);
627
+ console.log(` Total symbols: ${summary.total}`);
628
+ console.log(` Covered: ${summary.covered}`);
629
+ console.log(` Not covered: ${summary.uncovered}`);
630
+ }
631
+ });
632
+ });
633
+
634
+ // doc-coverage
635
+ program
636
+ .command('doc-coverage')
637
+ .description('Check documentation coverage across symbols')
638
+ .option('-s, --scope <path>', 'Limit to files matching path')
639
+ .option('--min-loc <n>', 'Minimum LOC to consider', parseIntSafe, 3)
640
+ .option('-n, --limit <n>', 'Max undocumented symbols to show', parseIntSafe, 50)
641
+ .action((opts) => {
642
+ runQuery(
643
+ (db) => queries.docCoverage(db, {
644
+ scope: opts.scope,
645
+ minLoc: opts.minLoc,
646
+ limit: opts.limit,
647
+ }),
648
+ (result) => {
649
+ console.log(`Documentation coverage: ${result.coveragePercent}%`);
650
+ console.log(` Total symbols: ${result.totalSymbols}`);
651
+ console.log(` Documented: ${result.documented}`);
652
+ console.log(` Undocumented: ${result.undocumented}`);
653
+ if (result.undocumentedSymbols.length > 0) {
654
+ console.log('\nUndocumented:');
655
+ for (const s of result.undocumentedSymbols) {
656
+ console.log(` ${s.relativePath}:${s.startLine} ${s.shortName}`);
657
+ }
658
+ }
659
+ },
660
+ );
661
+ });
662
+
663
+ // deep-chains
664
+ program
665
+ .command('deep-chains')
666
+ .description('Find the longest transitive dependency chains')
667
+ .option('-n, --limit <n>', 'Number of chains to show', parseIntSafe, 10)
668
+ .option('-s, --scope <path>', 'Limit to files matching path')
669
+ .option('--min-depth <n>', 'Minimum chain depth', parseIntSafe, 3)
670
+ .action((opts) => {
671
+ runQuery(
672
+ (db) => queries.deepChains(db, {
673
+ limit: opts.limit,
674
+ scope: opts.scope,
675
+ minDepth: opts.minDepth,
676
+ }),
677
+ (results) => {
678
+ if (results.length === 0) {
679
+ console.log('No deep chains found.');
680
+ } else {
681
+ for (let i = 0; i < results.length; i++) {
682
+ console.log(`\nChain ${i + 1} (depth ${results[i]!.depth}):`);
683
+ for (const file of results[i]!.chain) {
684
+ console.log(` → ${file}`);
685
+ }
686
+ }
687
+ }
688
+ },
689
+ );
690
+ });
691
+
692
+ // hierarchy
693
+ program
694
+ .command('hierarchy <symbol>')
695
+ .description('Show a symbol\'s ancestry chain (method → class → module)')
696
+ .action((symbol) => {
697
+ runQuery(
698
+ (db) => queries.hierarchy(db, symbol),
699
+ (chain) => {
700
+ if (chain.length === 0) {
701
+ console.log('Symbol not found.');
702
+ } else {
703
+ for (const node of chain) {
704
+ const indent = ' '.repeat(node.depth);
705
+ console.log(`${indent}${node.shortName}`);
706
+ }
707
+ }
708
+ },
709
+ );
710
+ });
711
+
712
+ // call-graph
713
+ program
714
+ .command('call-graph <symbol>')
715
+ .description('Show incoming callers and outgoing callees for a symbol')
716
+ .action((symbol) => {
717
+ runQuery(
718
+ (db) => queries.callGraph(db, symbol),
719
+ (result) => {
720
+ if (!result) {
721
+ console.log('Symbol not found.');
722
+ return;
723
+ }
724
+ console.log(`Symbol: ${result.shortName}\n`);
725
+ console.log(`═══ CALLERS (${result.callers.length}) ═══`);
726
+ for (const c of result.callers) {
727
+ console.log(` ${c.file} ${c.shortName}`);
728
+ }
729
+ console.log(`\n═══ CALLEES (${result.callees.length}) ═══`);
730
+ for (const c of result.callees) {
731
+ console.log(` ${c.file} ${c.shortName}`);
732
+ }
733
+ },
734
+ );
735
+ });
736
+
737
+ // similar
738
+ program
739
+ .command('similar [symbol]')
740
+ .description('Find functions with similar callee fingerprints (consolidation candidates)')
741
+ .option('--min-similarity <n>', 'Minimum Jaccard similarity (0-1)', parseFloat, 0.4)
742
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 20)
743
+ .option('-s, --scope <path>', 'Limit to files matching path')
744
+ .option('--min-callees <n>', 'Minimum callees to consider', parseIntSafe, 4)
745
+ .action((symbol, opts) => {
746
+ withDb((db) => {
747
+ if (symbol) {
748
+ const results = queries.similar(db, symbol, {
749
+ minSimilarity: opts.minSimilarity,
750
+ limit: opts.limit,
751
+ });
752
+ if (results.length === 0) {
753
+ console.log('No similar symbols found.');
754
+ } else {
755
+ for (const r of results) {
756
+ console.log(`\n${Math.round(r.similarity * 100)}% similar:`);
757
+ console.log(` A: ${r.shortNameA} (${r.fileA})`);
758
+ console.log(` B: ${r.shortNameB} (${r.fileB})`);
759
+ console.log(` Shared callees: ${r.sharedCallees.join(', ')}`);
760
+ if (r.uniqueToA.length) console.log(` Only in A: ${r.uniqueToA.join(', ')}`);
761
+ if (r.uniqueToB.length) console.log(` Only in B: ${r.uniqueToB.join(', ')}`);
762
+ }
763
+ }
764
+ } else {
765
+ const results = queries.similarAll(db, {
766
+ minSimilarity: opts.minSimilarity,
767
+ limit: opts.limit,
768
+ scope: opts.scope,
769
+ minCallees: opts.minCallees,
770
+ });
771
+ if (results.length === 0) {
772
+ console.log('No similar symbol pairs found.');
773
+ } else {
774
+ for (const r of results) {
775
+ console.log(`\n${Math.round(r.similarity * 100)}% similar:`);
776
+ console.log(` A: ${r.shortNameA} (${r.fileA})`);
777
+ console.log(` B: ${r.shortNameB} (${r.fileB})`);
778
+ console.log(` Shared: ${r.sharedCallees.join(', ')}`);
779
+ }
780
+ console.log(`\n${results.length} similar pair(s) found.`);
781
+ }
782
+ }
783
+ });
784
+ });
785
+
786
+ // similar-files
787
+ program
788
+ .command('similar-files [file]')
789
+ .description('Find files with similar dependency profiles')
790
+ .option('--min-similarity <n>', 'Minimum Jaccard similarity (0-1)', parseFloat, 0.5)
791
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 20)
792
+ .option('-s, --scope <path>', 'Limit to files matching path')
793
+ .option('--min-deps <n>', 'Minimum dependencies to consider', parseIntSafe, 3)
794
+ .action((file, opts) => {
795
+ runQuery(
796
+ (db) => queries.similarFiles(db, {
797
+ minSimilarity: opts.minSimilarity,
798
+ limit: opts.limit,
799
+ scope: opts.scope,
800
+ minDeps: opts.minDeps,
801
+ filePattern: file,
802
+ }),
803
+ (results) => {
804
+ if (results.length === 0) {
805
+ console.log('No similar file pairs found.');
806
+ } else {
807
+ for (const r of results) {
808
+ console.log(`\n${Math.round(r.similarity * 100)}% similar:`);
809
+ console.log(` ${r.fileA}`);
810
+ console.log(` ${r.fileB}`);
811
+ console.log(` Shared deps (${r.sharedDeps.length}): ${r.sharedDeps.join(', ')}`);
812
+ if (r.uniqueToA.length) console.log(` Only in first: ${r.uniqueToA.join(', ')}`);
813
+ if (r.uniqueToB.length) console.log(` Only in second: ${r.uniqueToB.join(', ')}`);
814
+ }
815
+ console.log(`\n${results.length} similar pair(s) found.`);
816
+ }
817
+ },
818
+ );
819
+ });
820
+
821
+ // similar-chains
822
+ program
823
+ .command('similar-chains')
824
+ .description('Find end-to-end dependency flows that diverge at few points')
825
+ .option('--min-similarity <n>', 'Minimum chain similarity (0-1)', parseFloat, 0.5)
826
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 15)
827
+ .option('-s, --scope <path>', 'Limit to files matching path')
828
+ .option('--min-length <n>', 'Minimum chain length', parseIntSafe, 3)
829
+ .option('--max-length <n>', 'Maximum chain length', parseIntSafe, 8)
830
+ .action((opts) => {
831
+ runQuery(
832
+ (db) => queries.similarChains(db, {
833
+ minSimilarity: opts.minSimilarity,
834
+ limit: opts.limit,
835
+ scope: opts.scope,
836
+ minChainLength: opts.minLength,
837
+ maxChainLength: opts.maxLength,
838
+ }),
839
+ (results) => {
840
+ if (results.length === 0) {
841
+ console.log('No similar chains found.');
842
+ } else {
843
+ for (let i = 0; i < results.length; i++) {
844
+ const r = results[i]!;
845
+ console.log(`\n── Chain pair ${i + 1} (${Math.round(r.similarity * 100)}% similar, ${r.divergencePoints.length} divergence point(s)) ──`);
846
+ console.log(` Chain A: ${r.chainA.join(' → ')}`);
847
+ console.log(` Chain B: ${r.chainB.join(' → ')}`);
848
+ if (r.commonPrefix.length) console.log(` Common prefix: ${r.commonPrefix.join(' → ')}`);
849
+ if (r.commonSuffix.length) console.log(` Common suffix: ${r.commonSuffix.join(' → ')}`);
850
+ console.log(' Divergence points (consolidation targets):');
851
+ for (const d of r.divergencePoints) {
852
+ console.log(` [${d.index}] ${d.nodeA} ↔ ${d.nodeB}`);
853
+ }
854
+ }
855
+ console.log(`\n${results.length} similar chain pair(s) found.`);
856
+ }
857
+ },
858
+ );
859
+ });
860
+
861
+ // extract-candidates
862
+ program
863
+ .command('extract-candidates')
864
+ .description('Find functions with natural extraction seams (isolated callee clusters)')
865
+ .option('-s, --scope <path>', 'Limit to files matching path')
866
+ .option('--min-loc <n>', 'Minimum function LOC', parseIntSafe, 10)
867
+ .option('--min-callees <n>', 'Minimum callees to analyze', parseIntSafe, 6)
868
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 20)
869
+ .action((opts) => {
870
+ runQuery(
871
+ (db) => queries.extractCandidates(db, {
872
+ scope: opts.scope,
873
+ minLoc: opts.minLoc,
874
+ minCallees: opts.minCallees,
875
+ limit: opts.limit,
876
+ }),
877
+ (results) => {
878
+ if (results.length === 0) {
879
+ console.log('No extraction candidates found.');
880
+ } else {
881
+ for (const r of results) {
882
+ console.log(`\n${r.relativePath}:${r.startLine}-${r.endLine} ${r.shortName} (${r.loc} LOC, ${r.totalCallees} callees)`);
883
+ for (let i = 0; i < r.clusters.length; i++) {
884
+ const c = r.clusters[i]!;
885
+ console.log(` Cluster ${i + 1} (${Math.round(c.isolation * 100)}% isolated, ${c.callees.length} callees):`);
886
+ for (const callee of c.callees) {
887
+ console.log(` ${callee}`);
888
+ }
889
+ }
890
+ }
891
+ console.log(`\n${results.length} extraction candidate(s) found.`);
892
+ }
893
+ },
894
+ );
895
+ });
896
+
897
+ // affected
898
+ program
899
+ .command('affected <symbol>')
900
+ .description('Transitive closure of symbols that could break if this symbol changes')
901
+ .option('--max-depth <n>', 'Maximum traversal depth', parseIntSafe, 5)
902
+ .option('-s, --scope <path>', 'Limit to files matching path')
903
+ .action((symbol, opts) => {
904
+ const db = openDb();
905
+ const results = queries.affected(db, symbol, { maxDepth: opts.maxDepth, scope: opts.scope });
906
+ if (results.length === 0) {
907
+ console.log('No affected symbols found.');
908
+ } else {
909
+ let prevDepth = -1;
910
+ for (const r of results) {
911
+ if (r.depth !== prevDepth) {
912
+ console.log(`\n ── Depth ${r.depth} ──`);
913
+ prevDepth = r.depth;
914
+ }
915
+ console.log(` ${r.file} ${r.shortName}`);
916
+ }
917
+ console.log(`\n${results.length} affected symbol(s) across ${new Set(results.map((r) => r.file)).size} files.`);
918
+ }
919
+ db.close();
920
+ });
921
+
922
+ // change-surface
923
+ program
924
+ .command('change-surface <file>')
925
+ .description('Pre-change briefing: exports, consumers, test coverage, risk')
926
+ .action((file) => {
927
+ const db = openDb();
928
+ const result = queries.changeSurface(db, file);
929
+ if (!result) {
930
+ console.log('File not found in index.');
931
+ db.close();
932
+ return;
933
+ }
934
+ console.log(`File: ${result.file}`);
935
+ console.log(`Test coverage: ${result.testCoveragePercent}% | External consumers: ${result.totalExternalConsumers}\n`);
936
+ for (const s of result.symbols) {
937
+ const risk = s.riskLevel === 'high' ? ' *** HIGH RISK ***' : s.riskLevel === 'medium' ? ' * medium risk *' : '';
938
+ const tests = s.testFiles.length > 0 ? ` (${s.testFiles.length} test file(s))` : ' (no tests)';
939
+ console.log(` ${s.startLine}-${s.endLine} ${s.shortName} [${s.externalConsumers} consumers]${tests}${risk}`);
940
+ }
941
+ db.close();
942
+ });
943
+
944
+ // diff-impact
945
+ program
946
+ .command('diff-impact')
947
+ .description('Compute affected symbols from current git diff')
948
+ .option('--base <ref>', 'Git ref to diff against (default: HEAD)')
949
+ .action((opts) => {
950
+ const db = openDb();
951
+ const result = queries.diffImpact(db, { base: opts.base });
952
+ console.log(`Changed files: ${result.summary.totalChangedFiles}`);
953
+ console.log(`Changed symbols: ${result.summary.totalChangedSymbols}`);
954
+ console.log(`Affected consumer files: ${result.summary.totalAffectedFiles}`);
955
+ console.log(`Test coverage: ${result.summary.testCoveragePercent}%\n`);
956
+ if (result.changedSymbols.length > 0) {
957
+ console.log('Changed symbols:');
958
+ for (const s of result.changedSymbols) {
959
+ console.log(` ${s.file} ${s.shortName} (fan-in: ${s.fanIn})`);
960
+ }
961
+ }
962
+ if (result.uncoveredSymbols.length > 0) {
963
+ console.log('\nUncovered (no test references):');
964
+ for (const s of result.uncoveredSymbols) {
965
+ console.log(` ${s.file} ${s.shortName}`);
966
+ }
967
+ }
968
+ if (result.affectedConsumers.length > 0) {
969
+ console.log('\nAffected consumer files:');
970
+ for (const c of result.affectedConsumers) {
971
+ console.log(` ${c.file} (${c.consumedSymbols} symbol(s))`);
972
+ }
973
+ }
974
+ db.close();
975
+ });
976
+
977
+ // drift
978
+ program
979
+ .command('drift [module]')
980
+ .description('Detect unused imports, layer violations, and pattern deviations')
981
+ .action((module) => {
982
+ const db = openDb();
983
+ const summary = queries.drift(db, { scope: module });
984
+ if (summary.results.length === 0) {
985
+ console.log('No drift detected.');
986
+ } else {
987
+ const grouped = new Map<string, typeof summary.results>();
988
+ for (const r of summary.results) {
989
+ if (!grouped.has(r.file)) grouped.set(r.file, []);
990
+ grouped.get(r.file)!.push(r);
991
+ }
992
+ for (const [file, items] of grouped) {
993
+ console.log(`\n${file}`);
994
+ for (const r of items) {
995
+ const tag = r.kind === 'unused-import' ? 'UNUSED' : r.kind === 'layer-violation' ? 'LAYER' : 'UNIQUE';
996
+ console.log(` [${tag}] ${r.description}`);
997
+ if (r.detail) console.log(` ${r.detail}`);
998
+ }
999
+ }
1000
+ console.log(`\n${summary.unusedImports} unused import(s), ${summary.layerViolations} layer violation(s), ${summary.patternDeviations} pattern deviation(s)`);
1001
+ }
1002
+ db.close();
1003
+ });
1004
+
1005
+ // wrapper-candidates
1006
+ program
1007
+ .command('wrapper-candidates')
1008
+ .description('Find symbols only called by one consumer (premature abstractions)')
1009
+ .option('-s, --scope <path>', 'Limit to files matching path')
1010
+ .option('--max-loc <n>', 'Maximum LOC for candidates', parseIntSafe, 15)
1011
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 30)
1012
+ .action((opts) => {
1013
+ const db = openDb();
1014
+ const results = queries.wrapperCandidates(db, { scope: opts.scope, maxLoc: opts.maxLoc, limit: opts.limit });
1015
+ if (results.length === 0) {
1016
+ console.log('No wrapper candidates found.');
1017
+ } else {
1018
+ for (const r of results) {
1019
+ console.log(` ${r.file}:${r.startLine}-${r.endLine} ${r.shortName} (${r.loc} LOC)`);
1020
+ console.log(` Only called by: ${r.singleCallerShort} (fan-in: ${r.callerFanIn})`);
1021
+ }
1022
+ console.log(`\n${results.length} wrapper candidate(s).`);
1023
+ }
1024
+ db.close();
1025
+ });
1026
+
1027
+ // passthrough-candidates
1028
+ program
1029
+ .command('passthrough-candidates')
1030
+ .description('Find functions that just forward to one other function')
1031
+ .option('-s, --scope <path>', 'Limit to files matching path')
1032
+ .option('--max-loc <n>', 'Maximum LOC for candidates', parseIntSafe, 15)
1033
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 30)
1034
+ .action((opts) => {
1035
+ const db = openDb();
1036
+ const results = queries.passthroughCandidates(db, { scope: opts.scope, maxLoc: opts.maxLoc, limit: opts.limit });
1037
+ if (results.length === 0) {
1038
+ console.log('No passthrough candidates found.');
1039
+ } else {
1040
+ for (const r of results) {
1041
+ console.log(` ${r.file}:${r.startLine}-${r.endLine} ${r.shortName} (${r.loc} LOC)`);
1042
+ console.log(` Forwards to: ${r.forwardsToShort} (${r.forwardsToFile})`);
1043
+ }
1044
+ console.log(`\n${results.length} passthrough candidate(s).`);
1045
+ }
1046
+ db.close();
1047
+ });
1048
+
1049
+ // stale-abstractions
1050
+ program
1051
+ .command('stale-abstractions')
1052
+ .description('Find types/interfaces with 0-1 consumers (premature abstractions)')
1053
+ .option('-s, --scope <path>', 'Limit to files matching path')
1054
+ .option('--min-loc <n>', 'Minimum LOC', parseIntSafe, 3)
1055
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 30)
1056
+ .action((opts) => {
1057
+ const db = openDb();
1058
+ const results = queries.staleAbstractions(db, { scope: opts.scope, minLoc: opts.minLoc, limit: opts.limit });
1059
+ if (results.length === 0) {
1060
+ console.log('No stale abstractions found.');
1061
+ } else {
1062
+ for (const r of results) {
1063
+ const label = r.consumers === 0 ? 'unused' : '1 consumer';
1064
+ console.log(` ${r.file}:${r.startLine}-${r.endLine} ${r.shortName} (${r.loc} LOC, ${label})`);
1065
+ }
1066
+ console.log(`\n${results.length} stale abstraction(s).`);
1067
+ }
1068
+ db.close();
1069
+ });
1070
+
1071
+ // complexity-hotspots
1072
+ program
1073
+ .command('complexity-hotspots')
1074
+ .description('Composite complexity score: LOC x fan-in x fan-out')
1075
+ .option('-s, --scope <path>', 'Limit to files matching path')
1076
+ .option('--min-loc <n>', 'Minimum LOC', parseIntSafe, 10)
1077
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 20)
1078
+ .action((opts) => {
1079
+ const db = openDb();
1080
+ const results = queries.complexityHotspots(db, { scope: opts.scope, minLoc: opts.minLoc, limit: opts.limit });
1081
+ if (results.length === 0) {
1082
+ console.log('No complexity hotspots found.');
1083
+ } else {
1084
+ console.log(' score LOC fan-in fan-out callees symbol');
1085
+ console.log(' ───── ──── ────── ─────── ─────── ──────');
1086
+ for (const r of results) {
1087
+ console.log(` ${r.score.toFixed(1).padStart(5)} ${String(r.loc).padStart(4)} ${String(r.fanIn).padStart(6)} ${String(r.fanOut).padStart(7)} ${String(r.calleeCount).padStart(7)} ${r.shortName}`);
1088
+ }
1089
+ }
1090
+ db.close();
1091
+ });
1092
+
1093
+ // health
1094
+ program
1095
+ .command('health')
1096
+ .description('Composite codebase health report with prioritized action list')
1097
+ .option('-s, --scope <path>', 'Limit to files matching path')
1098
+ .option('--json', 'Output as JSON for programmatic consumption')
1099
+ .action((opts) => {
1100
+ const db = openDb();
1101
+ const report = queries.health(db, { scope: opts.scope });
1102
+ if (opts.json) {
1103
+ console.log(JSON.stringify(report, null, 2));
1104
+ } else {
1105
+ console.log(`\n Codebase Health Score: ${report.score}/100\n`);
1106
+ console.log(` ${report.overview.documents} files | ${report.overview.symbols} symbols | ${formatBytes(report.overview.indexSizeBytes)}\n`);
1107
+
1108
+ console.log(' Findings:');
1109
+ const f = report.findings;
1110
+ if (f.deadSymbols > 0) console.log(` Dead code: ${f.deadSymbols} symbols (${f.deadLoc} LOC)`);
1111
+ if (f.isolatedSymbols > 0) console.log(` Isolated symbols: ${f.isolatedSymbols} (${f.isolatedLoc} LOC)`);
1112
+ if (f.cycles > 0) console.log(` Circular deps: ${f.cycles}`);
1113
+ if (f.similarPairs > 0) console.log(` Similar pairs: ${f.similarPairs}`);
1114
+ if (f.extractionCandidates > 0) console.log(` Extract candidates: ${f.extractionCandidates}`);
1115
+ if (f.wrappers > 0) console.log(` Wrapper functions: ${f.wrappers}`);
1116
+ if (f.passthroughs > 0) console.log(` Passthroughs: ${f.passthroughs}`);
1117
+ if (f.staleTypes > 0) console.log(` Stale abstractions: ${f.staleTypes}`);
1118
+ if (f.driftedFiles > 0) console.log(` Pattern drift: ${f.driftedFiles} files`);
1119
+ if (f.complexityHotspotCount > 0) console.log(` Complexity hotspots: ${f.complexityHotspotCount}`);
1120
+ console.log(` Test coverage: ${f.testCoveragePercent}%`);
1121
+
1122
+ if (report.actions.length > 0) {
1123
+ console.log('\n Prioritized Actions (highest impact + lowest effort first):');
1124
+ for (let i = 0; i < report.actions.length; i++) {
1125
+ const a = report.actions[i]!;
1126
+ const loc = a.locRecoverable > 0 ? ` (~${a.locRecoverable} LOC recoverable)` : '';
1127
+ console.log(` ${i + 1}. [${a.effort} effort / ${a.impact} impact] ${a.description}${loc}`);
1128
+ }
1129
+ }
1130
+
1131
+ if (report.topComplexity.length > 0) {
1132
+ console.log('\n Top Complexity Hotspots:');
1133
+ for (const c of report.topComplexity) {
1134
+ console.log(` ${c.score.toFixed(1).padStart(6)} ${c.symbol}`);
1135
+ }
1136
+ }
1137
+
1138
+ if (report.actions.length === 0) {
1139
+ console.log('\n No issues found. Codebase is clean.');
1140
+ }
1141
+ }
1142
+ db.close();
1143
+ });
1144
+
1145
+ // convergence
1146
+ program
1147
+ .command('convergence <symbol1> <symbol2>')
1148
+ .description('Show what a consolidated version of two similar functions would look like')
1149
+ .action((symbol1, symbol2) => {
1150
+ const db = openDb();
1151
+ const result = queries.convergence(db, symbol1, symbol2);
1152
+ if (!result) {
1153
+ console.log('One or both symbols not found.');
1154
+ db.close();
1155
+ return;
1156
+ }
1157
+ console.log(`\n${Math.round(result.similarity * 100)}% callee overlap\n`);
1158
+ console.log(` A: ${result.symbolA.shortName} (${result.symbolA.file}, ${result.symbolA.loc} LOC)`);
1159
+ console.log(` B: ${result.symbolB.shortName} (${result.symbolB.file}, ${result.symbolB.loc} LOC)\n`);
1160
+ console.log(` Shared callees (${result.sharedCallees.length}):`);
1161
+ for (const c of result.sharedCallees) console.log(` ${c}`);
1162
+ if (result.uniqueToA.length > 0) {
1163
+ console.log(`\n Unique to A (${result.uniqueToA.length}):`);
1164
+ for (const c of result.uniqueToA) console.log(` ${c}`);
1165
+ }
1166
+ if (result.uniqueToB.length > 0) {
1167
+ console.log(`\n Unique to B (${result.uniqueToB.length}):`);
1168
+ for (const c of result.uniqueToB) console.log(` ${c}`);
1169
+ }
1170
+ console.log(`\n Strategy: ${result.consolidationStrategy}`);
1171
+ db.close();
1172
+ });
1173
+
1174
+ // code
1175
+ program
1176
+ .command('code <symbol>')
1177
+ .description('Read the source code for a symbol (bounded to its definition range)')
1178
+ .option('-C, --context <n>', 'Extra lines of context above/below', parseIntSafe, 0)
1179
+ .action((symbol, opts) => {
1180
+ const db = openDb();
1181
+ const result = queries.code(db, symbol, { context: opts.context });
1182
+ if (!result) {
1183
+ console.log('Symbol not found or file unreadable.');
1184
+ db.close();
1185
+ return;
1186
+ }
1187
+ console.log(`${result.relativePath}:${result.startLine}-${result.endLine} ${result.shortName} [${result.language ?? 'unknown'}]\n`);
1188
+ const lines = result.source.split('\n');
1189
+ for (let i = 0; i < lines.length; i++) {
1190
+ console.log(` ${String(result.startLine + i).padStart(4)} ${lines[i]}`);
1191
+ }
1192
+ db.close();
1193
+ });
1194
+
1195
+ // complexity
1196
+ program
1197
+ .command('complexity <symbol>')
1198
+ .description('Per-symbol complexity: branches, cyclomatic estimate, fan-in/out, callees')
1199
+ .action((symbol) => {
1200
+ const db = openDb();
1201
+ const result = queries.complexity(db, symbol);
1202
+ if (!result) {
1203
+ console.log('Symbol not found.');
1204
+ db.close();
1205
+ return;
1206
+ }
1207
+ console.log(`${result.relativePath}:${result.startLine}-${result.endLine} ${result.shortName}\n`);
1208
+ console.log(` LOC: ${result.loc}`);
1209
+ console.log(` Branches: ${result.branches}`);
1210
+ console.log(` Cyclomatic estimate: ${result.cyclomaticEstimate}`);
1211
+ console.log(` Callees: ${result.calleeCount}`);
1212
+ console.log(` Fan-in: ${result.fanIn}`);
1213
+ console.log(` Fan-out: ${result.fanOut}`);
1214
+ db.close();
1215
+ });
1216
+
1217
+ // dataflow
1218
+ program
1219
+ .command('dataflow <symbol>')
1220
+ .description('Reference-level dataflow: definition sites, usage sites, producers, consumers')
1221
+ .action((symbol) => {
1222
+ const db = openDb();
1223
+ const result = queries.dataflow(db, symbol);
1224
+ if (!result) {
1225
+ console.log('Symbol not found.');
1226
+ db.close();
1227
+ return;
1228
+ }
1229
+ console.log(`${result.shortName} (${result.relativePath})\n`);
1230
+
1231
+ if (result.definitionSites.length > 0) {
1232
+ console.log(' ═══ DEFINED AT ═══');
1233
+ for (const s of result.definitionSites) {
1234
+ console.log(` ${s.file}:${s.line}`);
1235
+ }
1236
+ }
1237
+
1238
+ if (result.usageSites.length > 0) {
1239
+ console.log('\n ═══ USED AT ═══');
1240
+ for (const s of result.usageSites) {
1241
+ console.log(` ${s.file}:${s.line} in ${s.enclosingShort}`);
1242
+ }
1243
+ }
1244
+
1245
+ if (result.producers.length > 0) {
1246
+ console.log('\n ═══ PRODUCERS (feeds into this) ═══');
1247
+ for (const p of result.producers) {
1248
+ console.log(` ${p.file} ${p.shortName}`);
1249
+ }
1250
+ }
1251
+
1252
+ if (result.consumers.length > 0) {
1253
+ console.log('\n ═══ CONSUMERS (this feeds into) ═══');
1254
+ for (const c of result.consumers) {
1255
+ console.log(` ${c.file} ${c.shortName}`);
1256
+ }
1257
+ }
1258
+ db.close();
1259
+ });
1260
+
1261
+ // slice
1262
+ program
1263
+ .command('slice <symbol>')
1264
+ .description('Reference-level program slice: what affects this (backward) or what this affects (forward)')
1265
+ .option('--forward', 'Forward slice (what does this affect). Default is backward.')
1266
+ .action((symbol, opts) => {
1267
+ const db = openDb();
1268
+ const direction = opts.forward ? 'forward' : 'backward';
1269
+ const result = queries.slice(db, symbol, { direction });
1270
+ if (!result) {
1271
+ console.log('Symbol not found.');
1272
+ db.close();
1273
+ return;
1274
+ }
1275
+ console.log(`${result.direction} slice of ${result.shortName}\n`);
1276
+ if (result.connectedSymbols.length === 0) {
1277
+ console.log(' No connected symbols found.');
1278
+ } else {
1279
+ for (const s of result.connectedSymbols) {
1280
+ console.log(` ${s.file} ${s.shortName}`);
1281
+ console.log(` ${s.relationship}`);
1282
+ }
1283
+ console.log(`\n${result.connectedSymbols.length} connected symbol(s).`);
1284
+ }
1285
+ db.close();
1286
+ });
1287
+
1288
+ // install-skills
1289
+ program
1290
+ .command('install-skills')
1291
+ .description('Install skills (concrete-plan, scip-explore, scip-debloat, scip-verify) into Claude Code and Codex')
1292
+ .action(() => {
1293
+ const result = installSkills();
1294
+ const total = result.installed.length + result.alreadyLinked.length;
1295
+ console.log(`\n${result.installed.length} installed, ${result.alreadyLinked.length} already linked, ${result.skipped.length} skipped.`);
1296
+ if (total > 0) {
1297
+ console.log('Skills will be available in your next Claude Code / Codex session.');
1298
+ }
1299
+ });
1300
+
1301
+ // check-deps
1302
+ program
1303
+ .command('check-deps')
1304
+ .description('Check if required dependencies (scip CLI) are installed')
1305
+ .action(() => {
1306
+ if (isScipInstalled()) {
1307
+ console.log('scip CLI: installed');
1308
+ } else {
1309
+ printScipInstallInstructions();
1310
+ }
1311
+ });
1312
+
1313
+ // redundant-reexports
1314
+ program
1315
+ .command('redundant-reexports')
1316
+ .description('Find barrel re-exports that nobody imports through')
1317
+ .option('-s, --scope <path>', 'Limit to files matching path')
1318
+ .option('-n, --limit <n>', 'Number of results', parseIntSafe, 30)
1319
+ .action((opts) => {
1320
+ const db = openDb();
1321
+ const results = queries.redundantReexports(db, { scope: opts.scope, limit: opts.limit });
1322
+ if (results.length === 0) {
1323
+ console.log('No redundant re-exports found.');
1324
+ } else {
1325
+ let prevBarrel = '';
1326
+ for (const r of results) {
1327
+ if (r.barrelFile !== prevBarrel) {
1328
+ if (prevBarrel) console.log('');
1329
+ console.log(r.barrelFile);
1330
+ prevBarrel = r.barrelFile;
1331
+ }
1332
+ console.log(` ${r.shortName} (from ${r.originalFile})`);
1333
+ console.log(` barrel: ${r.barrelConsumers} consumer(s) | direct: ${r.directConsumers} consumer(s)`);
1334
+ }
1335
+ console.log(`\n${results.length} redundant re-export(s).`);
1336
+ }
1337
+ db.close();
1338
+ });
1339
+
1340
+ // similar-signatures
1341
+ program
1342
+ .command('similar-signatures')
1343
+ .description('Find functions with near-identical type signatures (same shape)')
1344
+ .option('-s, --scope <path>', 'Limit to files matching path')
1345
+ .option('--min-loc <n>', 'Minimum LOC per function', parseIntSafe, 3)
1346
+ .option('-n, --limit <n>', 'Number of groups', parseIntSafe, 20)
1347
+ .action((opts) => {
1348
+ const db = openDb();
1349
+ const groups = queries.similarSignatures(db, { scope: opts.scope, minLoc: opts.minLoc, limit: opts.limit });
1350
+ if (groups.length === 0) {
1351
+ console.log('No same-shape function groups found.');
1352
+ } else {
1353
+ for (const g of groups) {
1354
+ console.log(`\nSignature: ${g.signature} (${g.functions.length} functions)`);
1355
+ for (const f of g.functions) {
1356
+ console.log(` ${f.file}:${f.startLine}-${f.endLine} ${f.shortName} (${f.loc} LOC)`);
1357
+ }
1358
+ }
1359
+ console.log(`\n${groups.length} group(s) found.`);
1360
+ }
1361
+ db.close();
1362
+ });
1363
+
1364
+ // init
1365
+ program
1366
+ .command('init')
1367
+ .description('Create a .scipquery.json config file for this project')
1368
+ .action(() => {
1369
+ const projectRoot = resolveProjectRoot();
1370
+ const languages = detectLanguages(projectRoot);
1371
+ const configPath = initProjectConfig(projectRoot, languages);
1372
+ console.log(`Config written to ${configPath}`);
1373
+ console.log(`Detected languages: ${languages.join(', ') || '(none)'}`);
1374
+ });
1375
+
1376
+ // watch
1377
+ program
1378
+ .command('watch')
1379
+ .description('Watch for file changes and reindex automatically')
1380
+ .option('--debounce <ms>', 'Ms to wait after last change (default: 30000)', parseInt)
1381
+ .option('--cooldown <ms>', 'Min ms between reindexes (default: 60000)', parseInt)
1382
+ .action((opts) => {
1383
+ const projectRoot = resolveProjectRoot();
1384
+ const config = loadProjectConfig(projectRoot);
1385
+
1386
+ // CLI flags override config
1387
+ if (opts.debounce) (config.watch ??= {}).debounceMs = opts.debounce;
1388
+ if (opts.cooldown) (config.watch ??= {}).cooldownMs = opts.cooldown;
1389
+
1390
+ const watcher = new Watcher({
1391
+ projectRoot,
1392
+ config,
1393
+ onStatus: (status) => {
1394
+ process.stdout.write(`\r\x1b[K${formatStatus(status)}`);
1395
+ },
1396
+ onReindexComplete: (durationMs) => {
1397
+ console.log(`\nReindex complete in ${(durationMs / 1000).toFixed(1)}s`);
1398
+ },
1399
+ onError: (err) => {
1400
+ console.error(`\nWatch error: ${err.message}`);
1401
+ },
1402
+ });
1403
+
1404
+ console.log(`Watching ${projectRoot}`);
1405
+ console.log(`Debounce: ${config.watch?.debounceMs ?? 30000}ms | Cooldown: ${config.watch?.cooldownMs ?? 60000}ms`);
1406
+ console.log('Press Ctrl+C to stop.\n');
1407
+ watcher.start();
1408
+
1409
+ process.on('SIGINT', () => {
1410
+ watcher.stop();
1411
+ console.log('\nStopped.');
1412
+ process.exit(0);
1413
+ });
1414
+ });
1415
+
1416
+ // status
1417
+ program
1418
+ .command('status')
1419
+ .description('Show index status for this project')
1420
+ .action(() => {
1421
+ const projectRoot = resolveProjectRoot();
1422
+ const config = loadProjectConfig(projectRoot);
1423
+ const paths = resolveIndexPaths(projectRoot, config);
1424
+
1425
+ console.log(`Project: ${projectRoot}`);
1426
+ console.log(`DB path: ${paths.dbPath}`);
1427
+ console.log(`Exists: ${existsSync(paths.dbPath) ? 'yes' : 'no'}`);
1428
+
1429
+ if (existsSync(paths.dbPath)) {
1430
+ withDb((db) => {
1431
+ const s = queries.stats(db);
1432
+ console.log(`Symbols: ${s.symbols}`);
1433
+ console.log(`Files: ${s.documents}`);
1434
+ console.log(`Size: ${formatBytes(s.indexSizeBytes)}`);
1435
+ if (s.lastBuilt) {
1436
+ const ago = Math.round((Date.now() - s.lastBuilt.getTime()) / 1000);
1437
+ console.log(`Built: ${ago}s ago`);
1438
+ }
1439
+ });
1440
+ }
1441
+ });
1442
+
1443
+ // ── Parse & Run ────────────────────────────────────────────
1444
+
1445
+ program.parse();
1446
+
1447
+ // ── Utility ────────────────────────────────────────────────
1448
+
1449
+ function collect(value: string, prev: string[]): string[] {
1450
+ return prev.concat([value]);
1451
+ }
1452
+
1453
+ /** parseInt wrapper safe for commander (which passes default as 2nd arg = radix) */
1454
+ function parseIntSafe(value: string): number {
1455
+ return parseInt(value, 10);
1456
+ }
1457
+
1458
+ function formatBytes(bytes: number): string {
1459
+ if (bytes < 1024) return `${bytes} B`;
1460
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
1461
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1462
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
1463
+ }
1464
+
1465
+ function formatStatus(status: WatcherStatus): string {
1466
+ switch (status.state) {
1467
+ case 'idle':
1468
+ return 'Watching (idle)';
1469
+ case 'waiting': {
1470
+ const secs = Math.round((status.reindexAt - Date.now()) / 1000);
1471
+ return `${status.changedFiles} file(s) changed, reindexing in ${secs}s...`;
1472
+ }
1473
+ case 'indexing':
1474
+ return `Reindexing... (${Math.round((Date.now() - status.startedAt) / 1000)}s)`;
1475
+ case 'cooldown': {
1476
+ const secs = Math.round((status.until - Date.now()) / 1000);
1477
+ return `Cooldown (${secs}s)${status.dirty ? ' — changes pending' : ''}`;
1478
+ }
1479
+ }
1480
+ }