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/config.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { homedir } from 'node:os';
5
+ import type { ProjectConfig, WatchConfig } from './types.js';
6
+
7
+ const CONFIG_FILENAME = '.scipquery.json';
8
+
9
+ const DEFAULT_WATCH: Required<WatchConfig> = {
10
+ enabled: false,
11
+ debounceMs: 30_000,
12
+ cooldownMs: 60_000,
13
+ ignore: [],
14
+ };
15
+
16
+ /**
17
+ * Load project config from .scipquery.json in the project root.
18
+ * Returns defaults for anything not specified.
19
+ */
20
+ export function loadProjectConfig(projectRoot: string): ProjectConfig {
21
+ const configPath = join(projectRoot, CONFIG_FILENAME);
22
+
23
+ if (!existsSync(configPath)) {
24
+ return {};
25
+ }
26
+
27
+ try {
28
+ const raw = readFileSync(configPath, 'utf-8');
29
+ return JSON.parse(raw) as ProjectConfig;
30
+ } catch {
31
+ return {};
32
+ }
33
+ }
34
+
35
+ /** Resolve watch config with defaults applied */
36
+ export function resolveWatchConfig(config: ProjectConfig): Required<WatchConfig> {
37
+ return {
38
+ ...DEFAULT_WATCH,
39
+ ...config.watch,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Resolve the cache directory for a project's SCIP index.
45
+ *
46
+ * Default: ~/.cache/scip-query/projects/<hash>/
47
+ * Override: project config dbPath, or SCIP_QUERY_DB_PATH env var
48
+ *
49
+ * The hash is derived from the absolute project path so each
50
+ * project gets its own isolated index storage.
51
+ */
52
+ export function resolveCacheDir(projectRoot: string, config?: ProjectConfig): string {
53
+ // CLI/env override
54
+ const envOverride = process.env['SCIP_QUERY_CACHE_DIR'];
55
+ if (envOverride) return ensureDir(envOverride);
56
+
57
+ // Project config override
58
+ if (config?.dbPath) return ensureDir(resolve(projectRoot, config.dbPath));
59
+
60
+ // Default: XDG cache dir / fallback to ~/.cache
61
+ const xdgCache = process.env['XDG_CACHE_HOME'];
62
+ const cacheBase = xdgCache || join(homedir(), '.cache');
63
+ const projectHash = createHash('sha256')
64
+ .update(resolve(projectRoot))
65
+ .digest('hex')
66
+ .slice(0, 12);
67
+
68
+ const dir = join(cacheBase, 'scip-query', 'projects', projectHash);
69
+ return ensureDir(dir);
70
+ }
71
+
72
+ /**
73
+ * Resolve all paths for a project's index files.
74
+ */
75
+ export function resolveIndexPaths(projectRoot: string, config?: ProjectConfig): {
76
+ cacheDir: string;
77
+ dbPath: string;
78
+ indexPath: string;
79
+ metaPath: string;
80
+ } {
81
+ const cacheDir = resolveCacheDir(projectRoot, config);
82
+ return {
83
+ cacheDir,
84
+ dbPath: join(cacheDir, 'index.db'),
85
+ indexPath: join(cacheDir, 'index.scip'),
86
+ metaPath: join(cacheDir, 'meta.json'),
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Scaffold a default .scipquery.json in the project root.
92
+ * Does not overwrite an existing config.
93
+ */
94
+ export function initProjectConfig(projectRoot: string, languages: string[]): string {
95
+ const configPath = join(projectRoot, CONFIG_FILENAME);
96
+
97
+ if (existsSync(configPath)) {
98
+ return configPath;
99
+ }
100
+
101
+ const config: ProjectConfig = {
102
+ languages: languages as ProjectConfig['languages'],
103
+ watch: {
104
+ enabled: false,
105
+ debounceMs: 30_000,
106
+ cooldownMs: 60_000,
107
+ },
108
+ };
109
+
110
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
111
+ return configPath;
112
+ }
113
+
114
+ function ensureDir(dir: string): string {
115
+ mkdirSync(dir, { recursive: true });
116
+ return dir;
117
+ }
package/src/db.ts ADDED
@@ -0,0 +1,127 @@
1
+ import Database from 'better-sqlite3';
2
+ import { statSync } from 'node:fs';
3
+ import type { PathFilter } from './gitignore-filter.js';
4
+ import type { ScipQueryConfig } from './types.js';
5
+
6
+ /**
7
+ * Thin wrapper around better-sqlite3 with a pre-configured connection
8
+ * and helper methods for the SCIP SQLite schema.
9
+ *
10
+ * The schema is produced by `scip expt-convert` and is identical
11
+ * regardless of source language (TypeScript, Rust, Python, etc.).
12
+ *
13
+ * Tables:
14
+ * documents — indexed files (id, language, relative_path)
15
+ * global_symbols — all symbols (id, symbol, display_name, kind, documentation)
16
+ * defn_enclosing_ranges — definition locations (document_id, symbol_id, start/end line/char)
17
+ * mentions — references & definitions (chunk_id, symbol_id, role)
18
+ * chunks — code segments (document_id, chunk_index, start/end line, occurrences)
19
+ */
20
+ export class ScipDatabase {
21
+ readonly db: Database.Database;
22
+ readonly config: ScipQueryConfig;
23
+ private pathFilter: PathFilter | null;
24
+
25
+ constructor(config: ScipQueryConfig, pathFilter?: PathFilter) {
26
+ this.config = config;
27
+ this.pathFilter = pathFilter ?? null;
28
+ this.db = new Database(config.dbPath, { readonly: true });
29
+ this.db.pragma('busy_timeout = 5000');
30
+ }
31
+
32
+ /** Attach a gitignore-based path filter for query results */
33
+ setPathFilter(filter: PathFilter): void {
34
+ this.pathFilter = filter;
35
+ }
36
+
37
+ /** Check if a path should be excluded based on .gitignore rules */
38
+ isIgnored(relativePath: string): boolean {
39
+ return this.pathFilter?.isIgnored(relativePath) ?? false;
40
+ }
41
+
42
+ /** Filter an array of paths using the gitignore filter */
43
+ filterPaths(paths: string[]): string[] {
44
+ return this.pathFilter?.filter(paths) ?? paths;
45
+ }
46
+
47
+ /**
48
+ * The local-symbol predicate: only match symbols that are defined
49
+ * in files NOT excluded by gitignore. This replaces the old hardcoded
50
+ * `NOT LIKE 'node_modules/%'` check.
51
+ *
52
+ * Since SQLite can't evaluate JS gitignore rules inline, we use a
53
+ * simpler approach: query broadly, then filter in JS. For queries
54
+ * that need SQL-level filtering, use excludedPathPatterns().
55
+ */
56
+ get localSymbolPredicate(): string {
57
+ // Basic SQL-level exclusions for the most common cases.
58
+ // JS-level gitignore filtering handles the rest post-query.
59
+ return `EXISTS (
60
+ SELECT 1
61
+ FROM defn_enclosing_ranges local_der
62
+ JOIN documents local_d ON local_der.document_id = local_d.id
63
+ WHERE local_der.symbol_id = gs.id
64
+ ${this.pathExclusionsFor('local_d').trimStart()}
65
+ )`;
66
+ }
67
+
68
+ /**
69
+ * SQL WHERE clause fragments to exclude common build/dependency paths.
70
+ * Complements the JS-level gitignore filtering for performance.
71
+ */
72
+ get pathExclusions(): string {
73
+ return this.pathExclusionsFor('d');
74
+ }
75
+
76
+ /** Reusable SQL fragment: filter out synthetic/internal symbol noise */
77
+ get symbolNoise(): string {
78
+ return this.symbolNoiseFor('gs');
79
+ }
80
+
81
+ /** Build SQL path exclusions for one or more document table aliases */
82
+ pathExclusionsFor(...aliases: string[]): string {
83
+ return aliases
84
+ .flatMap((alias) => [
85
+ `AND ${alias}.relative_path NOT LIKE 'node_modules/%'`,
86
+ `AND ${alias}.relative_path NOT LIKE '.git/%'`,
87
+ ])
88
+ .join('\n ');
89
+ }
90
+
91
+ /** Build SQL symbol exclusions for the given global_symbols alias */
92
+ symbolNoiseFor(alias: string): string {
93
+ return `AND ${alias}.symbol NOT LIKE '%().(%' AND ${alias}.symbol NOT LIKE '%typeLiteral%'`;
94
+ }
95
+
96
+ /** Run a raw SQL query and return all rows */
97
+ all<T = Record<string, unknown>>(sql: string, ...params: unknown[]): T[] {
98
+ return this.db.prepare(sql).all(...params) as T[];
99
+ }
100
+
101
+ /** Run a raw SQL query and return the first row */
102
+ get<T = Record<string, unknown>>(sql: string, ...params: unknown[]): T | undefined {
103
+ return this.db.prepare(sql).get(...params) as T | undefined;
104
+ }
105
+
106
+ /** Get the database file size in bytes */
107
+ sizeBytes(): number {
108
+ try {
109
+ return statSync(this.config.dbPath).size;
110
+ } catch {
111
+ return 0;
112
+ }
113
+ }
114
+
115
+ /** Get the last modification time of the database file */
116
+ lastModified(): Date | null {
117
+ try {
118
+ return statSync(this.config.dbPath).mtime;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ close(): void {
125
+ this.db.close();
126
+ }
127
+ }
@@ -0,0 +1,143 @@
1
+ import ignore, { type Ignore } from 'ignore';
2
+ import { readFileSync, existsSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+
5
+ /**
6
+ * Builds a gitignore-based path filter from .gitignore files found
7
+ * in the project directory tree. This replaces hardcoded path exclusions
8
+ * like "node_modules/", "dist/", "target/", "__pycache__/" — instead,
9
+ * we respect whatever the project already ignores.
10
+ *
11
+ * Falls back to sensible defaults if no .gitignore is found.
12
+ */
13
+ export function createGitignoreFilter(projectRoot: string): PathFilter {
14
+ const ig = ignore();
15
+ let loaded = false;
16
+
17
+ // Walk up from project root looking for .gitignore files
18
+ // (nested .gitignore files apply to their subdirectory)
19
+ const gitignorePaths = findGitignoreFiles(projectRoot);
20
+
21
+ for (const gitignorePath of gitignorePaths) {
22
+ try {
23
+ const content = readFileSync(gitignorePath, 'utf-8');
24
+ ig.add(content);
25
+ loaded = true;
26
+ } catch {
27
+ // Skip unreadable files
28
+ }
29
+ }
30
+
31
+ // If no .gitignore found, use universal defaults
32
+ if (!loaded) {
33
+ ig.add(DEFAULT_IGNORES);
34
+ }
35
+
36
+ return {
37
+ isIgnored: (relativePath: string) => ig.ignores(relativePath),
38
+ filter: (paths: string[]) => paths.filter((p) => !ig.ignores(p)),
39
+ };
40
+ }
41
+
42
+ export interface PathFilter {
43
+ /** Returns true if this path should be excluded from results */
44
+ isIgnored: (relativePath: string) => boolean;
45
+ /** Filter an array of paths, keeping only non-ignored ones */
46
+ filter: (paths: string[]) => string[];
47
+ }
48
+
49
+ /**
50
+ * Find all .gitignore files from project root (including nested ones).
51
+ * We look at the root .gitignore and any in immediate subdirectories
52
+ * but don't recursively walk the entire tree (too expensive for large repos).
53
+ */
54
+ function findGitignoreFiles(projectRoot: string): string[] {
55
+ const files: string[] = [];
56
+
57
+ // Root .gitignore
58
+ const rootGitignore = join(projectRoot, '.gitignore');
59
+ if (existsSync(rootGitignore)) {
60
+ files.push(rootGitignore);
61
+ }
62
+
63
+ // Also check parent directories (for monorepo setups where .gitignore
64
+ // is at the repo root but the project is in a subdirectory)
65
+ let dir = dirname(projectRoot);
66
+ let depth = 0;
67
+ while (dir !== dirname(dir) && depth < 5) {
68
+ const parentGitignore = join(dir, '.gitignore');
69
+ if (existsSync(parentGitignore)) {
70
+ files.push(parentGitignore);
71
+ }
72
+ // Stop if we find a .git directory — that's the repo root
73
+ if (existsSync(join(dir, '.git'))) break;
74
+ dir = dirname(dir);
75
+ depth++;
76
+ }
77
+
78
+ return files;
79
+ }
80
+
81
+ /**
82
+ * Universal defaults when no .gitignore exists.
83
+ * Covers build artifacts, dependency directories, and virtual environments
84
+ * across all SCIP-supported languages.
85
+ */
86
+ const DEFAULT_IGNORES = `
87
+ # Dependencies
88
+ node_modules/
89
+ vendor/
90
+ .bundle/
91
+
92
+ # Build output
93
+ dist/
94
+ build/
95
+ out/
96
+ target/
97
+ bin/
98
+ obj/
99
+
100
+ # Python
101
+ __pycache__/
102
+ *.pyc
103
+ *.pyo
104
+ .venv/
105
+ venv/
106
+ .env/
107
+ env/
108
+ *.egg-info/
109
+
110
+ # Rust
111
+ target/
112
+
113
+ # Java / Kotlin / Scala
114
+ *.class
115
+ .gradle/
116
+ .mvn/
117
+
118
+ # C# / .NET
119
+ bin/
120
+ obj/
121
+ packages/
122
+
123
+ # Go
124
+ vendor/
125
+
126
+ # Dart
127
+ .dart_tool/
128
+ build/
129
+
130
+ # PHP
131
+ vendor/
132
+
133
+ # IDE / OS
134
+ .idea/
135
+ .vscode/
136
+ *.swp
137
+ *.swo
138
+ .DS_Store
139
+ Thumbs.db
140
+
141
+ # Type definitions (often noise in queries)
142
+ *.d.ts
143
+ `;
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ // scip-query — Language-agnostic code intelligence powered by SCIP indexes
2
+
3
+ export { ScipDatabase } from './db.js';
4
+ export { createGitignoreFilter } from './gitignore-filter.js';
5
+ export { parseSymbol, shortenSymbol, leafName } from './symbol-parser.js';
6
+ export { reindex, detectLanguages, getIndexerConfig, INDEXER_CONFIGS, isBinaryAvailable, isIndexerInstalled, tryInstallIndexer, tryInstallScipCli } from './reindex/index.js';
7
+ export { loadProjectConfig, resolveIndexPaths, resolveCacheDir, initProjectConfig } from './config.js';
8
+ export { Watcher } from './watch.js';
9
+ export { installSkills, isScipInstalled, getScipVersion, printScipInstallInstructions } from './setup.js';
10
+ export * from './queries/index.js';
11
+ export type * from './types.js';
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * npm postinstall hook. Runs automatically after `npm install -g scip-query`.
4
+ * Installs skills into Claude Code and Codex, checks for scip binary.
5
+ */
6
+ import { postinstall } from './setup.js';
7
+
8
+ postinstall();
@@ -0,0 +1,86 @@
1
+ import type { ScipDatabase } from '../db.js';
2
+ import { findFirstSymbolMatch } from '../query-support.js';
3
+ import type { AffectedResult } from '../types.js';
4
+ import { shortenSymbol } from '../symbol-parser.js';
5
+
6
+ /**
7
+ * Full transitive closure of symbols that could break if a given symbol changes.
8
+ * BFS from the target through the mention graph: depth 1 = direct consumers,
9
+ * depth 2 = consumers of consumers, etc.
10
+ */
11
+ export function affected(
12
+ db: ScipDatabase,
13
+ symbolPattern: string,
14
+ opts: { maxDepth?: number; scope?: string } = {},
15
+ ): AffectedResult[] {
16
+ const { maxDepth = 5, scope } = opts;
17
+
18
+ const target = findFirstSymbolMatch(db, symbolPattern);
19
+ if (!target) return [];
20
+
21
+ const scopeFilter = scope
22
+ ? `AND enc_d.relative_path LIKE '%${scope}%'`
23
+ : '';
24
+
25
+ const results: AffectedResult[] = [];
26
+ const visited = new Set<number>([target.symbolId]);
27
+ let frontier = new Set<number>([target.symbolId]);
28
+
29
+ for (let depth = 1; depth <= maxDepth; depth++) {
30
+ if (frontier.size === 0) break;
31
+
32
+ const placeholders = [...frontier].map(() => '?').join(',');
33
+ const nextFrontier = new Set<number>();
34
+
35
+ // For each symbol in the frontier, find enclosing symbols whose
36
+ // definition ranges contain a reference (role=0) to that frontier symbol.
37
+ const rows = db.all<{
38
+ symbol_id: number;
39
+ symbol: string;
40
+ relative_path: string;
41
+ }>(
42
+ `SELECT DISTINCT
43
+ enc_gs.id AS symbol_id,
44
+ enc_gs.symbol AS symbol,
45
+ enc_d.relative_path AS relative_path
46
+ FROM mentions m
47
+ JOIN chunks c ON m.chunk_id = c.id
48
+ JOIN documents ref_d ON c.document_id = ref_d.id
49
+ JOIN defn_enclosing_ranges enc_der
50
+ ON enc_der.document_id = ref_d.id
51
+ AND c.start_line >= enc_der.start_line
52
+ AND c.end_line <= enc_der.end_line
53
+ JOIN global_symbols enc_gs ON enc_der.symbol_id = enc_gs.id
54
+ JOIN documents enc_d ON enc_der.document_id = enc_d.id
55
+ WHERE m.symbol_id IN (${placeholders})
56
+ AND m.role = 0
57
+ AND enc_gs.id NOT IN (${placeholders})
58
+ ${db.symbolNoiseFor('enc_gs')}
59
+ ${db.pathExclusionsFor('enc_d')}
60
+ ${scopeFilter}`,
61
+ ...[...frontier],
62
+ ...[...frontier],
63
+ );
64
+
65
+ for (const row of rows) {
66
+ if (visited.has(row.symbol_id)) continue;
67
+ if (db.isIgnored(row.relative_path)) continue;
68
+
69
+ visited.add(row.symbol_id);
70
+ nextFrontier.add(row.symbol_id);
71
+
72
+ results.push({
73
+ symbol: row.symbol,
74
+ shortName: shortenSymbol(row.symbol),
75
+ file: row.relative_path,
76
+ depth,
77
+ });
78
+ }
79
+
80
+ frontier = nextFrontier;
81
+ }
82
+
83
+ // Sort by depth then file path
84
+ results.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
85
+ return results;
86
+ }
@@ -0,0 +1,67 @@
1
+ import type { ScipDatabase } from '../db.js';
2
+ import type { BottleneckResult } from '../types.js';
3
+ import { shortenSymbol } from '../symbol-parser.js';
4
+
5
+ /**
6
+ * Find coupling hubs: symbols with both high fan-in (many consumers)
7
+ * AND high fan-out (references many other symbols).
8
+ *
9
+ * These are the most dangerous symbols to change — they sit at the
10
+ * intersection of many dependency paths. Score = fanIn * fanOut.
11
+ */
12
+ export function bottlenecks(
13
+ db: ScipDatabase,
14
+ opts: { limit?: number; scope?: string; minFanIn?: number; minFanOut?: number } = {},
15
+ ): BottleneckResult[] {
16
+ const { limit = 20, scope, minFanIn = 2, minFanOut = 2 } = opts;
17
+ const scopeFilter = scope ? `AND def_d.relative_path LIKE '%${scope}%'` : '';
18
+
19
+ // Use a wrapping query to filter on computed columns
20
+ const rows = db.all<{
21
+ symbol: string;
22
+ defined_in: string;
23
+ fan_in: number;
24
+ fan_out: number;
25
+ }>(
26
+ `SELECT * FROM (
27
+ SELECT
28
+ gs.symbol,
29
+ def_d.relative_path AS defined_in,
30
+ (SELECT COUNT(DISTINCT ref_c.document_id)
31
+ FROM mentions ref_m
32
+ JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
33
+ WHERE ref_m.symbol_id = gs.id AND ref_m.role = 0
34
+ ) AS fan_in,
35
+ (SELECT COUNT(DISTINCT ref_gs.id)
36
+ FROM mentions ref_m
37
+ JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
38
+ JOIN global_symbols ref_gs ON ref_m.symbol_id = ref_gs.id
39
+ JOIN defn_enclosing_ranges ref_der ON ref_gs.id = ref_der.symbol_id
40
+ WHERE ref_c.document_id = def_d.id
41
+ AND ref_m.role = 0
42
+ AND ref_der.document_id != def_d.id
43
+ ) AS fan_out
44
+ FROM global_symbols gs
45
+ JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
46
+ JOIN documents def_d ON der.document_id = def_d.id
47
+ WHERE 1 = 1
48
+ ${db.pathExclusionsFor('def_d')}
49
+ ${db.symbolNoiseFor('gs')}
50
+ ${scopeFilter}
51
+ ) WHERE fan_in >= ? AND fan_out >= ?
52
+ ORDER BY (fan_in * fan_out) DESC
53
+ LIMIT ?`,
54
+ minFanIn, minFanOut, limit,
55
+ );
56
+
57
+ return rows
58
+ .filter((r) => !db.isIgnored(r.defined_in))
59
+ .map((r) => ({
60
+ symbol: r.symbol,
61
+ shortName: shortenSymbol(r.symbol),
62
+ fanIn: r.fan_in,
63
+ fanOut: r.fan_out,
64
+ score: r.fan_in * r.fan_out,
65
+ definedIn: r.defined_in,
66
+ }));
67
+ }