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
@@ -0,0 +1,59 @@
1
+ import type { ScipDatabase } from '../db.js';
2
+ import { buildFileDepGraph } from '../query-support.js';
3
+ import type { DeepChainResult } from '../types.js';
4
+
5
+ /**
6
+ * Find the longest transitive dependency chains between files.
7
+ * A chain A → B → C → D means A depends on B, B on C, C on D.
8
+ *
9
+ * Long chains = high coupling depth = changes at the end ripple through many layers.
10
+ */
11
+ export function deepChains(
12
+ db: ScipDatabase,
13
+ opts: { limit?: number; scope?: string; minDepth?: number } = {},
14
+ ): DeepChainResult[] {
15
+ const { limit = 10, scope, minDepth = 3 } = opts;
16
+ const graph = buildFileDepGraph(db, scope);
17
+
18
+ // DFS to find longest paths (with cycle detection)
19
+ const results: DeepChainResult[] = [];
20
+
21
+ function dfs(node: string, path: string[], visited: Set<string>): void {
22
+ const neighbors = graph.get(node);
23
+ if (!neighbors || neighbors.size === 0) {
24
+ if (path.length >= minDepth) {
25
+ results.push({ chain: [...path], depth: path.length });
26
+ }
27
+ return;
28
+ }
29
+
30
+ let extended = false;
31
+ for (const next of neighbors) {
32
+ if (visited.has(next)) continue; // skip cycles
33
+ visited.add(next);
34
+ path.push(next);
35
+ dfs(next, path, visited);
36
+ path.pop();
37
+ visited.delete(next);
38
+ extended = true;
39
+ }
40
+
41
+ // If no unvisited neighbors, this is a leaf in this path
42
+ if (!extended && path.length >= minDepth) {
43
+ results.push({ chain: [...path], depth: path.length });
44
+ }
45
+ }
46
+
47
+ // Start DFS from each node
48
+ for (const startNode of graph.keys()) {
49
+ const visited = new Set<string>([startNode]);
50
+ dfs(startNode, [startNode], visited);
51
+
52
+ // Early termination if we have enough results
53
+ if (results.length > limit * 10) break;
54
+ }
55
+
56
+ // Sort by depth descending, take top N
57
+ results.sort((a, b) => b.depth - a.depth);
58
+ return results.slice(0, limit);
59
+ }
@@ -0,0 +1,46 @@
1
+ import type { ScipDatabase } from '../db.js';
2
+ import type { DepResult } from '../types.js';
3
+
4
+ /** What internal files does this file depend on? (forward dependencies) */
5
+ export function deps(db: ScipDatabase, filePattern: string): DepResult[] {
6
+ const rows = db.all<{ relative_path: string }>(
7
+ `SELECT DISTINCT d2.relative_path
8
+ FROM mentions m
9
+ JOIN chunks c ON m.chunk_id = c.id
10
+ JOIN documents d1 ON c.document_id = d1.id
11
+ JOIN global_symbols gs ON m.symbol_id = gs.id
12
+ JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
13
+ JOIN documents d2 ON der.document_id = d2.id
14
+ WHERE d1.relative_path LIKE ?
15
+ AND d2.relative_path <> d1.relative_path
16
+ AND ${db.localSymbolPredicate}
17
+ ORDER BY d2.relative_path`,
18
+ `%${filePattern}%`,
19
+ );
20
+
21
+ return rows
22
+ .filter((r) => !db.isIgnored(r.relative_path))
23
+ .map((r) => ({ relativePath: r.relative_path }));
24
+ }
25
+
26
+ /** What files depend on this file/module? (reverse dependencies) */
27
+ export function rdeps(db: ScipDatabase, filePattern: string): DepResult[] {
28
+ const rows = db.all<{ relative_path: string }>(
29
+ `SELECT DISTINCT d1.relative_path
30
+ FROM mentions m
31
+ JOIN chunks c ON m.chunk_id = c.id
32
+ JOIN documents d1 ON c.document_id = d1.id
33
+ JOIN global_symbols gs ON m.symbol_id = gs.id
34
+ JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
35
+ JOIN documents d2 ON der.document_id = d2.id
36
+ WHERE d2.relative_path LIKE ?
37
+ AND d1.relative_path NOT LIKE ?
38
+ ORDER BY d1.relative_path`,
39
+ `%${filePattern}%`,
40
+ `%${filePattern}%`,
41
+ );
42
+
43
+ return rows
44
+ .filter((r) => !db.isIgnored(r.relative_path))
45
+ .map((r) => ({ relativePath: r.relative_path }));
46
+ }
@@ -0,0 +1,204 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import type { ScipDatabase } from '../db.js';
3
+ import { TEST_FILE_PATTERNS, testFileMatchSql } from '../query-support.js';
4
+ import type { DiffImpactResult } from '../types.js';
5
+ import { shortenSymbol } from '../symbol-parser.js';
6
+
7
+ /**
8
+ * Given a git diff, compute the affected symbol set.
9
+ * Finds all symbols defined in changed files, their fan-in,
10
+ * the files that consume them, and test coverage gaps.
11
+ */
12
+ export function diffImpact(
13
+ db: ScipDatabase,
14
+ opts: { base?: string } = {},
15
+ ): DiffImpactResult {
16
+ const { base = 'HEAD' } = opts;
17
+
18
+ // Get changed files from git
19
+ let changedFileLines: string[];
20
+ try {
21
+ const stdout = execFileSync('git', ['diff', '--name-only', base], {
22
+ encoding: 'utf-8',
23
+ cwd: db.config.projectRoot,
24
+ timeout: 10_000,
25
+ });
26
+ changedFileLines = stdout
27
+ .split('\n')
28
+ .map((l) => l.trim())
29
+ .filter((l) => l.length > 0);
30
+ } catch {
31
+ // Not in a git repo or git not available — return empty result
32
+ return {
33
+ changedFiles: [],
34
+ changedSymbols: [],
35
+ affectedConsumers: [],
36
+ uncoveredSymbols: [],
37
+ summary: {
38
+ totalChangedFiles: 0,
39
+ totalChangedSymbols: 0,
40
+ totalAffectedFiles: 0,
41
+ testCoveragePercent: 0,
42
+ },
43
+ };
44
+ }
45
+
46
+ if (changedFileLines.length === 0) {
47
+ return {
48
+ changedFiles: [],
49
+ changedSymbols: [],
50
+ affectedConsumers: [],
51
+ uncoveredSymbols: [],
52
+ summary: {
53
+ totalChangedFiles: 0,
54
+ totalChangedSymbols: 0,
55
+ totalAffectedFiles: 0,
56
+ testCoveragePercent: 0,
57
+ },
58
+ };
59
+ }
60
+
61
+ // Match changed files against the index
62
+ const changedFiles: string[] = [];
63
+ const changedDocIds: number[] = [];
64
+
65
+ for (const file of changedFileLines) {
66
+ const doc = db.get<{ id: number; relative_path: string }>(
67
+ `SELECT id, relative_path FROM documents
68
+ WHERE relative_path LIKE ?
69
+ LIMIT 1`,
70
+ `%${file}`,
71
+ );
72
+ if (doc && !db.isIgnored(doc.relative_path)) {
73
+ changedFiles.push(doc.relative_path);
74
+ changedDocIds.push(doc.id);
75
+ }
76
+ }
77
+
78
+ if (changedDocIds.length === 0) {
79
+ return {
80
+ changedFiles: changedFileLines,
81
+ changedSymbols: [],
82
+ affectedConsumers: [],
83
+ uncoveredSymbols: [],
84
+ summary: {
85
+ totalChangedFiles: changedFileLines.length,
86
+ totalChangedSymbols: 0,
87
+ totalAffectedFiles: 0,
88
+ testCoveragePercent: 0,
89
+ },
90
+ };
91
+ }
92
+
93
+ // Get all symbols defined in changed files
94
+ const docPlaceholders = changedDocIds.map(() => '?').join(',');
95
+ const syms = db.all<{
96
+ symbol_id: number;
97
+ symbol: string;
98
+ relative_path: string;
99
+ }>(
100
+ `SELECT DISTINCT gs.id AS symbol_id, gs.symbol, d.relative_path
101
+ FROM defn_enclosing_ranges der
102
+ JOIN global_symbols gs ON der.symbol_id = gs.id
103
+ JOIN documents d ON der.document_id = d.id
104
+ WHERE der.document_id IN (${docPlaceholders})
105
+ ${db.symbolNoiseFor('gs')}
106
+ ORDER BY d.relative_path`,
107
+ ...changedDocIds,
108
+ );
109
+
110
+ // For each symbol, compute fan-in (distinct referencing documents)
111
+ const testPatternSql = testFileMatchSql('ref_d', TEST_FILE_PATTERNS);
112
+ const changedSymbols: DiffImpactResult['changedSymbols'] = [];
113
+ const consumerMap = new Map<string, Set<string>>(); // file -> set of consumed symbol shortNames
114
+ const uncoveredSymbols: DiffImpactResult['uncoveredSymbols'] = [];
115
+ let coveredCount = 0;
116
+
117
+ for (const sym of syms) {
118
+ // Fan-in: distinct files that reference this symbol
119
+ const fanInRow = db.get<{ fan_in: number }>(
120
+ `SELECT COUNT(DISTINCT c.document_id) AS fan_in
121
+ FROM mentions m
122
+ JOIN chunks c ON m.chunk_id = c.id
123
+ WHERE m.symbol_id = ?
124
+ AND m.role = 0`,
125
+ sym.symbol_id,
126
+ );
127
+
128
+ const fanIn = fanInRow?.fan_in ?? 0;
129
+ const shortName = shortenSymbol(sym.symbol);
130
+
131
+ changedSymbols.push({
132
+ symbol: sym.symbol,
133
+ shortName,
134
+ file: sym.relative_path,
135
+ fanIn,
136
+ });
137
+
138
+ // Collect consumer files (excluding the changed files themselves)
139
+ const consumers = db.all<{ relative_path: string }>(
140
+ `SELECT DISTINCT ref_d.relative_path
141
+ FROM mentions m
142
+ JOIN chunks c ON m.chunk_id = c.id
143
+ JOIN documents ref_d ON c.document_id = ref_d.id
144
+ WHERE m.symbol_id = ?
145
+ AND m.role = 0
146
+ AND ref_d.relative_path NOT IN (${changedFiles.map(() => '?').join(',')})
147
+ ${db.pathExclusionsFor('ref_d')}`,
148
+ sym.symbol_id,
149
+ ...changedFiles,
150
+ );
151
+
152
+ for (const consumer of consumers) {
153
+ if (db.isIgnored(consumer.relative_path)) continue;
154
+ if (!consumerMap.has(consumer.relative_path)) {
155
+ consumerMap.set(consumer.relative_path, new Set());
156
+ }
157
+ consumerMap.get(consumer.relative_path)!.add(shortName);
158
+ }
159
+
160
+ // Check test coverage
161
+ const hasTest = db.get<{ c: number }>(
162
+ `SELECT COUNT(*) AS c
163
+ FROM mentions m
164
+ JOIN chunks c ON m.chunk_id = c.id
165
+ JOIN documents ref_d ON c.document_id = ref_d.id
166
+ WHERE m.symbol_id = ?
167
+ AND m.role = 0
168
+ AND (${testPatternSql})`,
169
+ sym.symbol_id,
170
+ );
171
+
172
+ if (hasTest && hasTest.c > 0) {
173
+ coveredCount++;
174
+ } else {
175
+ uncoveredSymbols.push({
176
+ symbol: sym.symbol,
177
+ shortName,
178
+ file: sym.relative_path,
179
+ });
180
+ }
181
+ }
182
+
183
+ // Build affected consumers list
184
+ const affectedConsumers = [...consumerMap.entries()]
185
+ .map(([file, symbols]) => ({ file, consumedSymbols: symbols.size }))
186
+ .sort((a, b) => b.consumedSymbols - a.consumedSymbols);
187
+
188
+ const totalSymbols = changedSymbols.length;
189
+ const testCoveragePercent =
190
+ totalSymbols > 0 ? Math.round((coveredCount / totalSymbols) * 100) : 0;
191
+
192
+ return {
193
+ changedFiles,
194
+ changedSymbols,
195
+ affectedConsumers,
196
+ uncoveredSymbols,
197
+ summary: {
198
+ totalChangedFiles: changedFiles.length,
199
+ totalChangedSymbols: totalSymbols,
200
+ totalAffectedFiles: affectedConsumers.length,
201
+ testCoveragePercent,
202
+ },
203
+ };
204
+ }
@@ -0,0 +1,86 @@
1
+ import type { ScipDatabase } from '../db.js';
2
+ import type { DocCoverageResult } from '../types.js';
3
+ import { shortenSymbol } from '../symbol-parser.js';
4
+
5
+ /**
6
+ * Check documentation coverage: what percentage of symbols have doc strings?
7
+ * Reports overall stats and lists undocumented symbols.
8
+ */
9
+ export function docCoverage(
10
+ db: ScipDatabase,
11
+ opts: { scope?: string; minLoc?: number; limit?: number } = {},
12
+ ): DocCoverageResult {
13
+ const { scope, minLoc = 3, limit = 50 } = opts;
14
+ const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';
15
+
16
+ // Count all local symbols meeting the threshold
17
+ const totalRow = db.get<{ c: number }>(
18
+ `SELECT COUNT(*) AS c
19
+ FROM global_symbols gs
20
+ JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
21
+ JOIN documents d ON der.document_id = d.id
22
+ WHERE 1 = 1
23
+ ${db.pathExclusionsFor('d')}
24
+ ${db.symbolNoiseFor('gs')}
25
+ AND gs.symbol NOT LIKE '%#%'
26
+ AND (der.end_line - der.start_line + 1) >= ?
27
+ ${scopeFilter}`,
28
+ minLoc,
29
+ );
30
+
31
+ const docRow = db.get<{ c: number }>(
32
+ `SELECT COUNT(*) AS c
33
+ FROM global_symbols gs
34
+ JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
35
+ JOIN documents d ON der.document_id = d.id
36
+ WHERE 1 = 1
37
+ ${db.pathExclusionsFor('d')}
38
+ ${db.symbolNoiseFor('gs')}
39
+ AND gs.symbol NOT LIKE '%#%'
40
+ AND (der.end_line - der.start_line + 1) >= ?
41
+ AND gs.documentation IS NOT NULL
42
+ AND gs.documentation != ''
43
+ ${scopeFilter}`,
44
+ minLoc,
45
+ );
46
+
47
+ const total = totalRow?.c ?? 0;
48
+ const documented = docRow?.c ?? 0;
49
+
50
+ // Get undocumented symbols
51
+ const undocRows = db.all<{
52
+ symbol: string;
53
+ relative_path: string;
54
+ start_line: number;
55
+ }>(
56
+ `SELECT gs.symbol, d.relative_path, der.start_line
57
+ FROM global_symbols gs
58
+ JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
59
+ JOIN documents d ON der.document_id = d.id
60
+ WHERE 1 = 1
61
+ ${db.pathExclusionsFor('d')}
62
+ ${db.symbolNoiseFor('gs')}
63
+ AND gs.symbol NOT LIKE '%#%'
64
+ AND (der.end_line - der.start_line + 1) >= ?
65
+ AND (gs.documentation IS NULL OR gs.documentation = '')
66
+ ${scopeFilter}
67
+ ORDER BY d.relative_path, der.start_line
68
+ LIMIT ?`,
69
+ minLoc, limit,
70
+ );
71
+
72
+ return {
73
+ totalSymbols: total,
74
+ documented,
75
+ undocumented: total - documented,
76
+ coveragePercent: total > 0 ? Math.round((documented / total) * 100) : 0,
77
+ undocumentedSymbols: undocRows
78
+ .filter((r) => !db.isIgnored(r.relative_path))
79
+ .map((r) => ({
80
+ symbol: r.symbol,
81
+ shortName: shortenSymbol(r.symbol),
82
+ relativePath: r.relative_path,
83
+ startLine: r.start_line,
84
+ })),
85
+ };
86
+ }
@@ -0,0 +1,224 @@
1
+ import path from 'node:path';
2
+ import type { ScipDatabase } from '../db.js';
3
+ import { buildFileDepGraph } from '../query-support.js';
4
+ import type { DriftResult, DriftSummary } from '../types.js';
5
+
6
+ /**
7
+ * Detect structural drift using the reference graph, not just import patterns.
8
+ *
9
+ * Three types of drift, each detecting a real problem:
10
+ *
11
+ * 1. **Unused imports** — file depends on a module but never references
12
+ * any of its symbols. Dead dependency, safe to remove.
13
+ *
14
+ * 2. **Layer violations** — file imports from a directory it shouldn't
15
+ * based on the project's directory structure (e.g., a query importing
16
+ * from reindex, a helper importing from CLI). Architectural decay.
17
+ *
18
+ * 3. **Pattern deviations** — file imports something no sibling does,
19
+ * suggesting it's reaching outside its expected scope. Only flagged
20
+ * when the file is the ONLY one in its directory with that dep.
21
+ */
22
+ export function drift(
23
+ db: ScipDatabase,
24
+ opts?: { scope?: string; minDeviation?: number },
25
+ ): DriftSummary {
26
+ const { scope } = opts ?? {};
27
+
28
+ // Build file dep graph (which files depend on which)
29
+ const depGraph = buildFileDepGraph(db, scope);
30
+
31
+ // Build symbol-level reference graph: for each file, which other files'
32
+ // symbols does it actually reference?
33
+ const symbolRefs = buildSymbolRefGraph(db, scope);
34
+
35
+ const results: DriftResult[] = [];
36
+
37
+ // ── Angle 1: Unused imports ──────────────────────────────
38
+ // File depends on module B (via dep graph) but never references
39
+ // any symbol defined in B (via symbol ref graph).
40
+ for (const [file, deps] of depGraph) {
41
+ if (isStructuralRole(path.basename(file))) continue;
42
+
43
+ const referencedFiles = symbolRefs.get(file) ?? new Set<string>();
44
+
45
+ for (const dep of deps) {
46
+ if (!referencedFiles.has(dep)) {
47
+ // This file "depends on" dep but never references its symbols.
48
+ // This can happen when the dep is imported for types only
49
+ // (which don't appear in the mention graph). Skip type-heavy deps.
50
+ if (isLikelyTypeOnlyDep(dep)) continue;
51
+
52
+ results.push({
53
+ file,
54
+ kind: 'unused-import',
55
+ description: `Depends on ${dep} but references none of its symbols`,
56
+ dep,
57
+ });
58
+ }
59
+ }
60
+ }
61
+
62
+ // ── Angle 2: Layer violations ────────────────────────────
63
+ // Detect when a file imports from a directory that represents
64
+ // a different architectural layer. We infer layers from the
65
+ // directory structure: files in the same top-level dir are peers,
66
+ // files in different top-level dirs crossing inward is a violation.
67
+ const layerRules = inferLayerRules(depGraph);
68
+
69
+ for (const [file, deps] of depGraph) {
70
+ if (isStructuralRole(path.basename(file))) continue;
71
+
72
+ const fileLayer = getTopDir(file);
73
+ for (const dep of deps) {
74
+ const depLayer = getTopDir(dep);
75
+ if (fileLayer === depLayer) continue; // same layer, fine
76
+
77
+ const violation = layerRules.get(`${fileLayer}->${depLayer}`);
78
+ if (violation === 'violation') {
79
+ results.push({
80
+ file,
81
+ kind: 'layer-violation',
82
+ description: `Imports from ${depLayer}/ (${dep}) — may cross architectural boundary`,
83
+ dep,
84
+ detail: `${fileLayer}/ should not depend on ${depLayer}/`,
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ // ── Angle 3: Unique deps (pattern deviation) ─────────────
91
+ // If a file is the ONLY one in its directory that depends on a
92
+ // particular module, that dependency is unusual and worth flagging.
93
+ const dirToFiles = new Map<string, string[]>();
94
+ for (const file of depGraph.keys()) {
95
+ const dir = path.dirname(file);
96
+ if (!dirToFiles.has(dir)) dirToFiles.set(dir, []);
97
+ dirToFiles.get(dir)!.push(file);
98
+ }
99
+
100
+ for (const [dir, files] of dirToFiles) {
101
+ if (files.length < 3) continue;
102
+
103
+ // Count dep frequency across siblings
104
+ const depFreq = new Map<string, number>();
105
+ for (const file of files) {
106
+ if (isStructuralRole(path.basename(file))) continue;
107
+ for (const dep of depGraph.get(file) ?? []) {
108
+ depFreq.set(dep, (depFreq.get(dep) ?? 0) + 1);
109
+ }
110
+ }
111
+
112
+ for (const file of files) {
113
+ if (isStructuralRole(path.basename(file))) continue;
114
+ for (const dep of depGraph.get(file) ?? []) {
115
+ if ((depFreq.get(dep) ?? 0) === 1) {
116
+ // This file is the only one in its dir that depends on this module
117
+ // Skip if dep is in the same directory (sibling imports are normal)
118
+ if (path.dirname(dep) === dir) continue;
119
+
120
+ results.push({
121
+ file,
122
+ kind: 'pattern-deviation',
123
+ description: `Only file in ${dir}/ that depends on ${dep}`,
124
+ dep,
125
+ });
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ return {
132
+ results,
133
+ unusedImports: results.filter((r) => r.kind === 'unused-import').length,
134
+ layerViolations: results.filter((r) => r.kind === 'layer-violation').length,
135
+ patternDeviations: results.filter((r) => r.kind === 'pattern-deviation').length,
136
+ };
137
+ }
138
+
139
+ // ── Helpers ────────────────────────────────────────────────
140
+
141
+ /**
142
+ * Build a map of file → set of files whose symbols it references.
143
+ * This is more precise than the dep graph because it uses actual
144
+ * symbol mentions, not just import statements.
145
+ */
146
+ function buildSymbolRefGraph(
147
+ db: ScipDatabase,
148
+ scope?: string,
149
+ ): Map<string, Set<string>> {
150
+ const scopeFilter = scope ? `AND d1.relative_path LIKE '%${scope}%'` : '';
151
+
152
+ const rows = db.all<{ from_file: string; to_file: string }>(
153
+ `SELECT DISTINCT d1.relative_path AS from_file, d2.relative_path AS to_file
154
+ FROM mentions m
155
+ JOIN chunks c ON m.chunk_id = c.id
156
+ JOIN documents d1 ON c.document_id = d1.id
157
+ JOIN global_symbols gs ON m.symbol_id = gs.id
158
+ JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
159
+ JOIN documents d2 ON der.document_id = d2.id
160
+ WHERE d1.id != d2.id
161
+ AND m.role = 0
162
+ ${db.pathExclusionsFor('d1', 'd2')}
163
+ ${scopeFilter}`,
164
+ );
165
+
166
+ const graph = new Map<string, Set<string>>();
167
+ for (const r of rows) {
168
+ if (db.isIgnored(r.from_file) || db.isIgnored(r.to_file)) continue;
169
+ if (!graph.has(r.from_file)) graph.set(r.from_file, new Set());
170
+ graph.get(r.from_file)!.add(r.to_file);
171
+ }
172
+ return graph;
173
+ }
174
+
175
+ /**
176
+ * Infer layer boundary rules from the dependency graph.
177
+ * If directory A never depends on directory B across the entire codebase,
178
+ * then a new A→B dependency is a violation.
179
+ */
180
+ function inferLayerRules(
181
+ depGraph: Map<string, Set<string>>,
182
+ ): Map<string, 'ok' | 'violation'> {
183
+ const layerEdges = new Map<string, number>();
184
+ const layerSet = new Set<string>();
185
+
186
+ for (const [file, deps] of depGraph) {
187
+ const fromLayer = getTopDir(file);
188
+ layerSet.add(fromLayer);
189
+ for (const dep of deps) {
190
+ const toLayer = getTopDir(dep);
191
+ if (fromLayer === toLayer) continue;
192
+ layerSet.add(toLayer);
193
+ const key = `${fromLayer}->${toLayer}`;
194
+ layerEdges.set(key, (layerEdges.get(key) ?? 0) + 1);
195
+ }
196
+ }
197
+
198
+ // An edge that appears only 1-2 times across the whole codebase
199
+ // is likely a violation (anomalous cross-layer dep).
200
+ // Edges that appear many times are established patterns.
201
+ const rules = new Map<string, 'ok' | 'violation'>();
202
+ for (const [edge, count] of layerEdges) {
203
+ rules.set(edge, count <= 2 ? 'violation' : 'ok');
204
+ }
205
+
206
+ return rules;
207
+ }
208
+
209
+ function getTopDir(filePath: string): string {
210
+ const parts = filePath.split('/');
211
+ return parts[0] ?? filePath;
212
+ }
213
+
214
+ function isLikelyTypeOnlyDep(dep: string): boolean {
215
+ return dep.includes('types') || dep.endsWith('.d.ts');
216
+ }
217
+
218
+ function isStructuralRole(basename: string): boolean {
219
+ if (basename === 'index.ts' || basename === 'index.js') return true;
220
+ if (basename === 'cli.ts' || basename === 'main.ts' || basename === 'main.rs') return true;
221
+ if (basename.includes('worker.') || basename.includes('postinstall.')) return true;
222
+ if (basename === 'health.ts' || basename === 'health.js') return true;
223
+ return false;
224
+ }