scip-query 0.1.0 → 0.2.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 (198) hide show
  1. package/dist/{chunk-CM454WL3.js → chunk-2CKGIR6G.js} +2 -2
  2. package/dist/{chunk-5FGUEU7N.js → chunk-4EXL2CUA.js} +4 -4
  3. package/dist/chunk-4EXL2CUA.js.map +1 -0
  4. package/dist/{chunk-TDNNOR6D.js → chunk-4PDAL6IL.js} +6 -6
  5. package/dist/chunk-4PDAL6IL.js.map +1 -0
  6. package/dist/{chunk-BP2ATLK2.js → chunk-4XHWPRAX.js} +3 -3
  7. package/dist/chunk-4XHWPRAX.js.map +1 -0
  8. package/dist/{chunk-2QZ23IBN.js → chunk-5RMYT5WH.js} +3 -3
  9. package/dist/chunk-5RMYT5WH.js.map +1 -0
  10. package/dist/{chunk-7OZPA5OO.js → chunk-63G7IQTD.js} +13 -13
  11. package/dist/{chunk-XFXDXEUN.js → chunk-74RFWB5T.js} +2 -2
  12. package/dist/{chunk-XFXDXEUN.js.map → chunk-74RFWB5T.js.map} +1 -1
  13. package/dist/{chunk-JKP5GH6T.js → chunk-7LLPRPR5.js} +2 -2
  14. package/dist/{chunk-BFSCMC22.js → chunk-7PBOG4YE.js} +2 -2
  15. package/dist/{chunk-BFSCMC22.js.map → chunk-7PBOG4YE.js.map} +1 -1
  16. package/dist/{chunk-36OMT7ZJ.js → chunk-7RLE5EWE.js} +3 -3
  17. package/dist/{chunk-36OMT7ZJ.js.map → chunk-7RLE5EWE.js.map} +1 -1
  18. package/dist/{chunk-UNTPVD36.js → chunk-7UCKSQRS.js} +3 -3
  19. package/dist/{chunk-UNTPVD36.js.map → chunk-7UCKSQRS.js.map} +1 -1
  20. package/dist/{chunk-GJFURBEW.js → chunk-BNN2RKD2.js} +3 -3
  21. package/dist/chunk-BNN2RKD2.js.map +1 -0
  22. package/dist/{chunk-FFSWWE5O.js → chunk-BOVXCR46.js} +2 -2
  23. package/dist/{chunk-FFSWWE5O.js.map → chunk-BOVXCR46.js.map} +1 -1
  24. package/dist/{chunk-6VJ6Q7IE.js → chunk-D567NFIF.js} +3 -3
  25. package/dist/{chunk-6VJ6Q7IE.js.map → chunk-D567NFIF.js.map} +1 -1
  26. package/dist/{chunk-GTILYBH6.js → chunk-DGUPQSOR.js} +5 -5
  27. package/dist/chunk-DGUPQSOR.js.map +1 -0
  28. package/dist/{chunk-TBP6BICL.js → chunk-EQYLEQCW.js} +2 -2
  29. package/dist/{chunk-TBP6BICL.js.map → chunk-EQYLEQCW.js.map} +1 -1
  30. package/dist/{chunk-VZ7AMAFL.js → chunk-H2MDONBU.js} +2 -2
  31. package/dist/{chunk-BEPIEVLR.js → chunk-HB7MRLLL.js} +2 -2
  32. package/dist/{chunk-BEPIEVLR.js.map → chunk-HB7MRLLL.js.map} +1 -1
  33. package/dist/{chunk-KVSW5KYP.js → chunk-HDSRORNV.js} +4 -4
  34. package/dist/chunk-HDSRORNV.js.map +1 -0
  35. package/dist/{chunk-5WTJAXY2.js → chunk-HMLMH7VZ.js} +2 -2
  36. package/dist/{chunk-Z73NYSBZ.js → chunk-HPFZLISB.js} +2 -2
  37. package/dist/{chunk-3ZZJVBIO.js → chunk-HZBC7PPD.js} +4 -4
  38. package/dist/chunk-HZBC7PPD.js.map +1 -0
  39. package/dist/{chunk-6NBLIDF4.js → chunk-ITZ3DDOG.js} +2 -2
  40. package/dist/chunk-ITZ3DDOG.js.map +1 -0
  41. package/dist/{chunk-YZAA4LYG.js → chunk-KPPHZCZJ.js} +5 -5
  42. package/dist/chunk-KPPHZCZJ.js.map +1 -0
  43. package/dist/{chunk-T6ARFSBZ.js → chunk-MCUX5LA7.js} +6 -6
  44. package/dist/chunk-MCUX5LA7.js.map +1 -0
  45. package/dist/{chunk-ZJRYBOEE.js → chunk-NHBZIL2J.js} +4 -4
  46. package/dist/{chunk-ZJRYBOEE.js.map → chunk-NHBZIL2J.js.map} +1 -1
  47. package/dist/{chunk-VRUJH4BO.js → chunk-NWCE4CIC.js} +4 -4
  48. package/dist/chunk-NWCE4CIC.js.map +1 -0
  49. package/dist/{chunk-LUSIFBXO.js → chunk-OVPLOMPY.js} +4 -4
  50. package/dist/{chunk-LUSIFBXO.js.map → chunk-OVPLOMPY.js.map} +1 -1
  51. package/dist/{chunk-TSPZOMHC.js → chunk-UJQN5N3I.js} +2 -2
  52. package/dist/{chunk-NDSQYIWT.js → chunk-W4ALF422.js} +2 -2
  53. package/dist/{chunk-3E2X7RIE.js → chunk-Z4GHE2HD.js} +5 -5
  54. package/dist/chunk-Z4GHE2HD.js.map +1 -0
  55. package/dist/{chunk-LB7OS35Q.js → chunk-Z6YZJ36C.js} +2 -2
  56. package/dist/{chunk-EMDQWNYR.js → chunk-ZK6GXM3J.js} +7 -7
  57. package/dist/chunk-ZK6GXM3J.js.map +1 -0
  58. package/dist/{chunk-FUHJCHS4.js → chunk-ZOGY2V3N.js} +3 -3
  59. package/dist/chunk-ZOGY2V3N.js.map +1 -0
  60. package/dist/{chunk-MBVNHJVN.js → chunk-ZQIIPFD7.js} +2 -2
  61. package/dist/cli.js +52 -52
  62. package/dist/cli.js.map +1 -1
  63. package/dist/{db-BxaevAyc.d.ts → db-BNVVZSfP.d.ts} +1 -1
  64. package/dist/index.d.ts +2 -2
  65. package/dist/index.js +35 -35
  66. package/dist/queries/affected.d.ts +1 -1
  67. package/dist/queries/affected.js +2 -2
  68. package/dist/queries/bottlenecks.d.ts +1 -1
  69. package/dist/queries/bottlenecks.js +1 -1
  70. package/dist/queries/by-kind.d.ts +1 -1
  71. package/dist/queries/call-graph.d.ts +1 -1
  72. package/dist/queries/call-graph.js +2 -2
  73. package/dist/queries/change-surface.d.ts +1 -1
  74. package/dist/queries/change-surface.js +2 -2
  75. package/dist/queries/code.d.ts +1 -1
  76. package/dist/queries/code.js +2 -2
  77. package/dist/queries/complexity-hotspots.d.ts +1 -1
  78. package/dist/queries/complexity-hotspots.js +2 -2
  79. package/dist/queries/complexity.d.ts +1 -1
  80. package/dist/queries/complexity.js +2 -2
  81. package/dist/queries/convergence.d.ts +1 -1
  82. package/dist/queries/convergence.js +2 -2
  83. package/dist/queries/coupling.d.ts +1 -1
  84. package/dist/queries/coupling.js +1 -1
  85. package/dist/queries/cycles.d.ts +1 -1
  86. package/dist/queries/cycles.js +2 -2
  87. package/dist/queries/dataflow.d.ts +1 -1
  88. package/dist/queries/dataflow.js +2 -2
  89. package/dist/queries/dead.d.ts +1 -1
  90. package/dist/queries/dead.js +2 -2
  91. package/dist/queries/deep-chains.d.ts +1 -1
  92. package/dist/queries/deep-chains.js +2 -2
  93. package/dist/queries/deps.d.ts +1 -1
  94. package/dist/queries/diff-impact.d.ts +1 -1
  95. package/dist/queries/diff-impact.js +2 -2
  96. package/dist/queries/doc-coverage.d.ts +1 -1
  97. package/dist/queries/drift.d.ts +1 -1
  98. package/dist/queries/drift.js +2 -2
  99. package/dist/queries/extract-candidates.d.ts +1 -1
  100. package/dist/queries/extract-candidates.js +2 -2
  101. package/dist/queries/fan.d.ts +1 -1
  102. package/dist/queries/fan.js +1 -1
  103. package/dist/queries/files.d.ts +1 -1
  104. package/dist/queries/health.d.ts +1 -1
  105. package/dist/queries/health.js +14 -14
  106. package/dist/queries/hierarchy.d.ts +1 -1
  107. package/dist/queries/hotspots.d.ts +1 -1
  108. package/dist/queries/hotspots.js +1 -1
  109. package/dist/queries/imports.d.ts +1 -1
  110. package/dist/queries/imports.js +1 -1
  111. package/dist/queries/index.d.ts +1 -1
  112. package/dist/queries/index.js +35 -35
  113. package/dist/queries/isolated.d.ts +1 -1
  114. package/dist/queries/isolated.js +2 -2
  115. package/dist/queries/members.d.ts +1 -1
  116. package/dist/queries/methods.d.ts +1 -1
  117. package/dist/queries/outline.d.ts +1 -1
  118. package/dist/queries/passthrough-candidates.d.ts +1 -1
  119. package/dist/queries/passthrough-candidates.js +2 -2
  120. package/dist/queries/redundant-reexports.d.ts +1 -1
  121. package/dist/queries/redundant-reexports.js +1 -1
  122. package/dist/queries/refs.d.ts +1 -1
  123. package/dist/queries/refs.js +1 -1
  124. package/dist/queries/similar-chains.d.ts +1 -1
  125. package/dist/queries/similar-chains.js +2 -2
  126. package/dist/queries/similar-files.d.ts +1 -1
  127. package/dist/queries/similar-files.js +2 -2
  128. package/dist/queries/similar-signatures.d.ts +1 -1
  129. package/dist/queries/similar.d.ts +1 -1
  130. package/dist/queries/similar.js +2 -2
  131. package/dist/queries/slice.d.ts +1 -1
  132. package/dist/queries/slice.js +2 -2
  133. package/dist/queries/stale-abstractions.d.ts +1 -1
  134. package/dist/queries/stale-abstractions.js +2 -2
  135. package/dist/queries/stats.d.ts +1 -1
  136. package/dist/queries/stats.js +1 -1
  137. package/dist/queries/surface.d.ts +1 -1
  138. package/dist/queries/surface.js +1 -1
  139. package/dist/queries/symbols.d.ts +1 -1
  140. package/dist/queries/system.d.ts +1 -1
  141. package/dist/queries/test-coverage.d.ts +1 -1
  142. package/dist/queries/test-coverage.js +2 -2
  143. package/dist/queries/trace.d.ts +1 -1
  144. package/dist/queries/trace.js +1 -1
  145. package/dist/queries/wrapper-candidates.d.ts +1 -1
  146. package/dist/queries/wrapper-candidates.js +2 -2
  147. package/package.json +1 -1
  148. package/src/queries/affected.ts +1 -1
  149. package/src/queries/bottlenecks.ts +2 -2
  150. package/src/queries/call-graph.ts +1 -1
  151. package/src/queries/change-surface.ts +2 -2
  152. package/src/queries/complexity-hotspots.ts +5 -5
  153. package/src/queries/complexity.ts +1 -1
  154. package/src/queries/coupling.ts +3 -3
  155. package/src/queries/dataflow.ts +4 -4
  156. package/src/queries/dead.ts +2 -2
  157. package/src/queries/diff-impact.ts +3 -3
  158. package/src/queries/drift.ts +1 -1
  159. package/src/queries/fan.ts +4 -4
  160. package/src/queries/hotspots.ts +1 -1
  161. package/src/queries/imports.ts +1 -1
  162. package/src/queries/isolated.ts +2 -2
  163. package/src/queries/redundant-reexports.ts +4 -4
  164. package/src/queries/refs.ts +1 -1
  165. package/src/queries/slice.ts +2 -2
  166. package/src/queries/stale-abstractions.ts +1 -1
  167. package/src/queries/stats.ts +1 -1
  168. package/src/queries/surface.ts +1 -1
  169. package/src/queries/test-coverage.ts +2 -2
  170. package/src/queries/trace.ts +1 -1
  171. package/src/queries/wrapper-candidates.ts +4 -4
  172. package/src/query-support.ts +2 -2
  173. package/src/types.ts +1 -1
  174. package/dist/chunk-2QZ23IBN.js.map +0 -1
  175. package/dist/chunk-3E2X7RIE.js.map +0 -1
  176. package/dist/chunk-3ZZJVBIO.js.map +0 -1
  177. package/dist/chunk-5FGUEU7N.js.map +0 -1
  178. package/dist/chunk-6NBLIDF4.js.map +0 -1
  179. package/dist/chunk-BP2ATLK2.js.map +0 -1
  180. package/dist/chunk-EMDQWNYR.js.map +0 -1
  181. package/dist/chunk-FUHJCHS4.js.map +0 -1
  182. package/dist/chunk-GJFURBEW.js.map +0 -1
  183. package/dist/chunk-GTILYBH6.js.map +0 -1
  184. package/dist/chunk-KVSW5KYP.js.map +0 -1
  185. package/dist/chunk-T6ARFSBZ.js.map +0 -1
  186. package/dist/chunk-TDNNOR6D.js.map +0 -1
  187. package/dist/chunk-VRUJH4BO.js.map +0 -1
  188. package/dist/chunk-YZAA4LYG.js.map +0 -1
  189. /package/dist/{chunk-CM454WL3.js.map → chunk-2CKGIR6G.js.map} +0 -0
  190. /package/dist/{chunk-7OZPA5OO.js.map → chunk-63G7IQTD.js.map} +0 -0
  191. /package/dist/{chunk-JKP5GH6T.js.map → chunk-7LLPRPR5.js.map} +0 -0
  192. /package/dist/{chunk-VZ7AMAFL.js.map → chunk-H2MDONBU.js.map} +0 -0
  193. /package/dist/{chunk-5WTJAXY2.js.map → chunk-HMLMH7VZ.js.map} +0 -0
  194. /package/dist/{chunk-Z73NYSBZ.js.map → chunk-HPFZLISB.js.map} +0 -0
  195. /package/dist/{chunk-TSPZOMHC.js.map → chunk-UJQN5N3I.js.map} +0 -0
  196. /package/dist/{chunk-NDSQYIWT.js.map → chunk-W4ALF422.js.map} +0 -0
  197. /package/dist/{chunk-LB7OS35Q.js.map → chunk-Z6YZJ36C.js.map} +0 -0
  198. /package/dist/{chunk-MBVNHJVN.js.map → chunk-ZQIIPFD7.js.map} +0 -0
@@ -51,7 +51,7 @@ export function dead(db: ScipDatabase, opts: DeadOptions = {}): DeadSummary {
51
51
  gs.symbol,
52
52
  (SELECT COUNT(*) FROM mentions m2
53
53
  JOIN chunks c2 ON m2.chunk_id = c2.id
54
- WHERE m2.symbol_id = gs.id AND m2.role = 0 AND c2.document_id = d.id
54
+ WHERE m2.symbol_id = gs.id AND m2.role != 1 AND c2.document_id = d.id
55
55
  ) AS same_file_refs
56
56
  FROM global_symbols gs
57
57
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
@@ -69,7 +69,7 @@ export function dead(db: ScipDatabase, opts: DeadOptions = {}): DeadSummary {
69
69
  JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
70
70
  JOIN documents ref_d ON ref_c.document_id = ref_d.id
71
71
  WHERE ref_m.symbol_id = gs.id
72
- AND ref_m.role = 0
72
+ AND ref_m.role != 1
73
73
  AND ref_d.id != d.id
74
74
  ${barrelExclusions}
75
75
  )
@@ -121,7 +121,7 @@ export function diffImpact(
121
121
  FROM mentions m
122
122
  JOIN chunks c ON m.chunk_id = c.id
123
123
  WHERE m.symbol_id = ?
124
- AND m.role = 0`,
124
+ AND m.role != 1`,
125
125
  sym.symbol_id,
126
126
  );
127
127
 
@@ -142,7 +142,7 @@ export function diffImpact(
142
142
  JOIN chunks c ON m.chunk_id = c.id
143
143
  JOIN documents ref_d ON c.document_id = ref_d.id
144
144
  WHERE m.symbol_id = ?
145
- AND m.role = 0
145
+ AND m.role != 1
146
146
  AND ref_d.relative_path NOT IN (${changedFiles.map(() => '?').join(',')})
147
147
  ${db.pathExclusionsFor('ref_d')}`,
148
148
  sym.symbol_id,
@@ -164,7 +164,7 @@ export function diffImpact(
164
164
  JOIN chunks c ON m.chunk_id = c.id
165
165
  JOIN documents ref_d ON c.document_id = ref_d.id
166
166
  WHERE m.symbol_id = ?
167
- AND m.role = 0
167
+ AND m.role != 1
168
168
  AND (${testPatternSql})`,
169
169
  sym.symbol_id,
170
170
  );
@@ -158,7 +158,7 @@ function buildSymbolRefGraph(
158
158
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
159
159
  JOIN documents d2 ON der.document_id = d2.id
160
160
  WHERE d1.id != d2.id
161
- AND m.role = 0
161
+ AND m.role != 1
162
162
  ${db.pathExclusionsFor('d1', 'd2')}
163
163
  ${scopeFilter}`,
164
164
  );
@@ -19,7 +19,7 @@ export function fanIn(
19
19
  JOIN chunks c ON m.chunk_id = c.id
20
20
  JOIN global_symbols gs ON m.symbol_id = gs.id
21
21
  WHERE gs.symbol LIKE ?
22
- AND m.role = 0
22
+ AND m.role != 1
23
23
  GROUP BY gs.id
24
24
  ORDER BY file_count DESC`,
25
25
  `%${symbolPattern}%`,
@@ -51,7 +51,7 @@ export function fanOut(
51
51
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
52
52
  JOIN documents def_d ON der.document_id = def_d.id
53
53
  WHERE d.relative_path LIKE ?
54
- AND m.role = 0
54
+ AND m.role != 1
55
55
  AND def_d.id != d.id
56
56
  GROUP BY d.id
57
57
  ORDER BY symbol_count DESC`,
@@ -88,7 +88,7 @@ export function topFanIn(
88
88
  JOIN global_symbols gs ON m.symbol_id = gs.id
89
89
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
90
90
  JOIN documents def_d ON der.document_id = def_d.id
91
- WHERE m.role = 0
91
+ WHERE m.role != 1
92
92
  ${db.pathExclusionsFor('def_d')}
93
93
  ${db.symbolNoiseFor('gs')}
94
94
  ${scopeFilter}
@@ -128,7 +128,7 @@ export function topFanOut(
128
128
  JOIN global_symbols gs ON m.symbol_id = gs.id
129
129
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
130
130
  JOIN documents def_d ON der.document_id = def_d.id
131
- WHERE m.role = 0
131
+ WHERE m.role != 1
132
132
  AND def_d.id != d.id
133
133
  ${db.pathExclusionsFor('d')}
134
134
  ${db.symbolNoiseFor('gs')}
@@ -31,7 +31,7 @@ export function hotspots(
31
31
  JOIN global_symbols gs ON m.symbol_id = gs.id
32
32
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
33
33
  JOIN documents def_d ON der.document_id = def_d.id
34
- WHERE m.role = 0
34
+ WHERE m.role != 1
35
35
  ${db.pathExclusionsFor('def_d')}
36
36
  ${db.symbolNoiseFor('gs')}
37
37
  ${scopeFilter}
@@ -80,7 +80,7 @@ export function unusedImports(db: ScipDatabase, filePattern: string): UnusedImpo
80
80
  FROM mentions ref_m
81
81
  JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
82
82
  WHERE ref_m.symbol_id = gs.id
83
- AND ref_m.role = 0
83
+ AND ref_m.role != 1
84
84
  AND ref_c.document_id = d.id
85
85
  )
86
86
  ORDER BY d.relative_path, gs.symbol`,
@@ -42,13 +42,13 @@ export function isolated(
42
42
  AND NOT EXISTS (
43
43
  SELECT 1 FROM mentions m
44
44
  JOIN chunks c ON m.chunk_id = c.id
45
- WHERE m.symbol_id = gs.id AND m.role = 0 AND c.document_id != d.id
45
+ WHERE m.symbol_id = gs.id AND m.role != 1 AND c.document_id != d.id
46
46
  )
47
47
  -- No same-file references either
48
48
  AND NOT EXISTS (
49
49
  SELECT 1 FROM mentions m
50
50
  JOIN chunks c ON m.chunk_id = c.id
51
- WHERE m.symbol_id = gs.id AND m.role = 0 AND c.document_id = d.id
51
+ WHERE m.symbol_id = gs.id AND m.role != 1 AND c.document_id = d.id
52
52
  )
53
53
  ORDER BY loc DESC, d.relative_path`,
54
54
  minLoc,
@@ -48,7 +48,7 @@ export function redundantReexports(
48
48
  JOIN global_symbols gs ON m.symbol_id = gs.id
49
49
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
50
50
  JOIN documents orig_d ON der.document_id = orig_d.id
51
- WHERE m.role = 0
51
+ WHERE m.role != 1
52
52
  AND (barrel_d.relative_path LIKE '%/index.ts'
53
53
  OR barrel_d.relative_path LIKE '%/index.js'
54
54
  OR barrel_d.relative_path = 'index.ts'
@@ -109,20 +109,20 @@ export function redundantReexports(
109
109
  FROM mentions barrel_m
110
110
  JOIN chunks barrel_c ON barrel_m.chunk_id = barrel_c.id
111
111
  WHERE barrel_c.document_id = consumer_d.id
112
- AND barrel_m.role = 0
112
+ AND barrel_m.role != 1
113
113
  AND barrel_m.symbol_id IN (
114
114
  SELECT m2.symbol_id
115
115
  FROM mentions m2
116
116
  JOIN chunks c2 ON m2.chunk_id = c2.id
117
117
  WHERE c2.document_id = ?
118
- AND m2.role = 0
118
+ AND m2.role != 1
119
119
  )
120
120
  ) THEN 1 ELSE 0 END) AS uses_barrel
121
121
  FROM mentions ref_m
122
122
  JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
123
123
  JOIN documents consumer_d ON ref_c.document_id = consumer_d.id
124
124
  WHERE ref_m.symbol_id = ?
125
- AND ref_m.role = 0
125
+ AND ref_m.role != 1
126
126
  AND consumer_d.id != ?
127
127
  AND consumer_d.id != ?
128
128
  ${db.pathExclusionsFor('consumer_d')}
@@ -13,7 +13,7 @@ export function refs(db: ScipDatabase, symbolPattern: string): RefResult[] {
13
13
  JOIN global_symbols gs ON m.symbol_id = gs.id
14
14
  WHERE gs.symbol LIKE ?
15
15
  AND ${db.localSymbolPredicate}
16
- AND m.role = 0
16
+ AND m.role != 1
17
17
  ORDER BY d.relative_path, c.start_line`,
18
18
  `%${symbolPattern}%`,
19
19
  );
@@ -113,7 +113,7 @@ function forwardSlice(db: ScipDatabase, match: SymbolMatch): SliceResult {
113
113
  JOIN global_symbols enc_gs ON enc_der.symbol_id = enc_gs.id
114
114
  JOIN documents enc_d ON enc_der.document_id = enc_d.id
115
115
  -- Find other symbols referenced within that enclosing function
116
- JOIN mentions out_m ON out_m.role = 0
116
+ JOIN mentions out_m ON out_m.role != 1
117
117
  JOIN chunks out_c ON out_m.chunk_id = out_c.id
118
118
  AND out_c.document_id = enc_der.document_id
119
119
  AND out_c.start_line >= enc_der.start_line
@@ -121,7 +121,7 @@ function forwardSlice(db: ScipDatabase, match: SymbolMatch): SliceResult {
121
121
  JOIN global_symbols out_gs ON out_m.symbol_id = out_gs.id
122
122
  JOIN defn_enclosing_ranges out_der ON out_gs.id = out_der.symbol_id
123
123
  JOIN documents out_d ON out_der.document_id = out_d.id
124
- WHERE ref_m.symbol_id = ? AND ref_m.role = 0
124
+ WHERE ref_m.symbol_id = ? AND ref_m.role != 1
125
125
  AND out_gs.id != ? AND out_gs.id != enc_gs.id
126
126
  AND out_d.id != ref_d.id
127
127
  ${db.symbolNoiseFor('out_gs')}
@@ -37,7 +37,7 @@ export function staleAbstractions(
37
37
  FROM mentions ref_m
38
38
  JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
39
39
  WHERE ref_m.symbol_id = gs.id
40
- AND ref_m.role = 0
40
+ AND ref_m.role != 1
41
41
  AND ref_c.document_id != der.document_id
42
42
  ) AS consumers
43
43
  FROM global_symbols gs
@@ -8,7 +8,7 @@ export function stats(db: ScipDatabase): StatsResult {
8
8
  'SELECT COUNT(*) as c FROM mentions WHERE role = 1',
9
9
  )!.c;
10
10
  const references = db.get<{ c: number }>(
11
- 'SELECT COUNT(*) as c FROM mentions WHERE role = 0',
11
+ 'SELECT COUNT(*) as c FROM mentions WHERE role != 1',
12
12
  )!.c;
13
13
 
14
14
  return {
@@ -18,7 +18,7 @@ export function surface(db: ScipDatabase, modulePattern: string): SurfaceResult[
18
18
  WHERE d2.relative_path LIKE ?
19
19
  AND d1.relative_path NOT LIKE ?
20
20
  AND ${db.localSymbolPredicate}
21
- AND m.role = 0
21
+ AND m.role != 1
22
22
  ORDER BY d1.relative_path`,
23
23
  `%${modulePattern}%`,
24
24
  `%${modulePattern}%`,
@@ -40,7 +40,7 @@ export function testCoverage(
40
40
  JOIN chunks c ON m.chunk_id = c.id
41
41
  JOIN documents ref_d ON c.document_id = ref_d.id
42
42
  WHERE m.symbol_id = ?
43
- AND m.role = 0
43
+ AND m.role != 1
44
44
  AND (${testPatternSql})
45
45
  ORDER BY ref_d.relative_path`,
46
46
  s.id,
@@ -90,7 +90,7 @@ export function testCoverageSummary(
90
90
  `SELECT COUNT(*) AS c FROM mentions m
91
91
  JOIN chunks c ON m.chunk_id = c.id
92
92
  JOIN documents ref_d ON c.document_id = ref_d.id
93
- WHERE m.symbol_id = ? AND m.role = 0 AND (${testRefSql})`,
93
+ WHERE m.symbol_id = ? AND m.role != 1 AND (${testRefSql})`,
94
94
  s.id,
95
95
  );
96
96
  if (hasTest && hasTest.c > 0) covered++;
@@ -42,7 +42,7 @@ export function trace(db: ScipDatabase, symbolPattern: string): TraceResult {
42
42
  WHERE gs.symbol LIKE ?
43
43
  AND ${db.localSymbolPredicate}
44
44
  ${db.symbolNoise}
45
- AND m.role = 0
45
+ AND m.role != 1
46
46
  ORDER BY d.relative_path`,
47
47
  `%${symbolPattern}%`,
48
48
  );
@@ -46,7 +46,7 @@ export function wrapperCandidates(
46
46
  AND ref_c.end_line <= caller_der.end_line
47
47
  JOIN global_symbols caller_gs ON caller_der.symbol_id = caller_gs.id
48
48
  WHERE ref_m.symbol_id = gs.id
49
- AND ref_m.role = 0
49
+ AND ref_m.role != 1
50
50
  AND ref_c.document_id != der.document_id
51
51
  LIMIT 1
52
52
  ) AS caller_symbol,
@@ -63,11 +63,11 @@ export function wrapperCandidates(
63
63
  AND ref_c2.start_line >= caller_der2.start_line
64
64
  AND ref_c2.end_line <= caller_der2.end_line
65
65
  WHERE ref_m2.symbol_id = gs.id
66
- AND ref_m2.role = 0
66
+ AND ref_m2.role != 1
67
67
  AND ref_c2.document_id != der.document_id
68
68
  LIMIT 1
69
69
  )
70
- AND caller_ref_m.role = 0
70
+ AND caller_ref_m.role != 1
71
71
  ) AS caller_fan_in
72
72
  FROM global_symbols gs
73
73
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
@@ -87,7 +87,7 @@ export function wrapperCandidates(
87
87
  FROM mentions ref_m
88
88
  JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id
89
89
  WHERE ref_m.symbol_id = gs.id
90
- AND ref_m.role = 0
90
+ AND ref_m.role != 1
91
91
  AND ref_c.document_id != der.document_id
92
92
  ) = 1
93
93
  ) WHERE caller_symbol IS NOT NULL AND caller_fan_in > 3
@@ -68,7 +68,7 @@ export function buildFileDepGraph(
68
68
  JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id
69
69
  JOIN documents d2 ON der.document_id = d2.id
70
70
  WHERE d1.id != d2.id
71
- AND m.role = 0
71
+ AND m.role != 1
72
72
  ${db.pathExclusionsFor('d1', 'd2')}
73
73
  ${scopeFilter}`,
74
74
  );
@@ -187,7 +187,7 @@ export function getCalleeRowsForSymbol(
187
187
  WHERE c.document_id = ?
188
188
  AND c.start_line >= ?
189
189
  AND c.end_line <= ?
190
- AND m.role = 0
190
+ AND m.role != 1
191
191
  AND callee_gs.id != ?
192
192
  ${db.symbolNoiseFor('callee_gs')}
193
193
  ${db.pathExclusionsFor('callee_d')}
package/src/types.ts CHANGED
@@ -457,7 +457,7 @@ export interface DataflowResult {
457
457
  relativePath: string;
458
458
  /** Where the symbol is defined (role=1) */
459
459
  definitionSites: Array<{ file: string; line: number }>;
460
- /** Where the symbol is referenced (role=0) */
460
+ /** Where the symbol is referenced (role!=1) */
461
461
  usageSites: Array<{ file: string; line: number; enclosingSymbol: string; enclosingShort: string }>;
462
462
  /** Symbols that appear in the same function that defines this symbol (producers/inputs) */
463
463
  producers: Array<{ symbol: string; shortName: string; file: string }>;
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/bottlenecks.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { BottleneckResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find coupling hubs: symbols with both high fan-in (many consumers)\n * AND high fan-out (references many other symbols).\n *\n * These are the most dangerous symbols to change — they sit at the\n * intersection of many dependency paths. Score = fanIn * fanOut.\n */\nexport function bottlenecks(\n db: ScipDatabase,\n opts: { limit?: number; scope?: string; minFanIn?: number; minFanOut?: number } = {},\n): BottleneckResult[] {\n const { limit = 20, scope, minFanIn = 2, minFanOut = 2 } = opts;\n const scopeFilter = scope ? `AND def_d.relative_path LIKE '%${scope}%'` : '';\n\n // Use a wrapping query to filter on computed columns\n const rows = db.all<{\n symbol: string;\n defined_in: string;\n fan_in: number;\n fan_out: number;\n }>(\n `SELECT * FROM (\n SELECT\n gs.symbol,\n def_d.relative_path AS defined_in,\n (SELECT COUNT(DISTINCT ref_c.document_id)\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n WHERE ref_m.symbol_id = gs.id AND ref_m.role = 0\n ) AS fan_in,\n (SELECT COUNT(DISTINCT ref_gs.id)\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n JOIN global_symbols ref_gs ON ref_m.symbol_id = ref_gs.id\n JOIN defn_enclosing_ranges ref_der ON ref_gs.id = ref_der.symbol_id\n WHERE ref_c.document_id = def_d.id\n AND ref_m.role = 0\n AND ref_der.document_id != def_d.id\n ) AS fan_out\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents def_d ON der.document_id = def_d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('def_d')}\n ${db.symbolNoiseFor('gs')}\n ${scopeFilter}\n ) WHERE fan_in >= ? AND fan_out >= ?\n ORDER BY (fan_in * fan_out) DESC\n LIMIT ?`,\n minFanIn, minFanOut, limit,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.defined_in))\n .map((r) => ({\n symbol: r.symbol,\n shortName: shortenSymbol(r.symbol),\n fanIn: r.fan_in,\n fanOut: r.fan_out,\n score: r.fan_in * r.fan_out,\n definedIn: r.defined_in,\n }));\n}\n"],"mappings":";;;;;AAWO,SAAS,YACd,IACA,OAAkF,CAAC,GAC/D;AACpB,QAAM,EAAE,QAAQ,IAAI,OAAO,WAAW,GAAG,YAAY,EAAE,IAAI;AAC3D,QAAM,cAAc,QAAQ,kCAAkC,KAAK,OAAO;AAG1E,QAAM,OAAO,GAAG;AAAA,IAMd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAsBM,GAAG,kBAAkB,OAAO,CAAC;AAAA,UAC7B,GAAG,eAAe,IAAI,CAAC;AAAA,UACvB,WAAW;AAAA;AAAA;AAAA;AAAA,IAIjB;AAAA,IAAU;AAAA,IAAW;AAAA,EACvB;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,UAAU,CAAC,EACzC,IAAI,CAAC,OAAO;AAAA,IACX,QAAQ,EAAE;AAAA,IACV,WAAW,cAAc,EAAE,MAAM;AAAA,IACjC,OAAO,EAAE;AAAA,IACT,QAAQ,EAAE;AAAA,IACV,OAAO,EAAE,SAAS,EAAE;AAAA,IACpB,WAAW,EAAE;AAAA,EACf,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/redundant-reexports.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { RedundantReexport } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find barrel re-exports that no consumer actually imports through.\n *\n * If `queries/index.ts` re-exports `byKind` from `by-kind.ts`, but every\n * consumer of `byKind` imports it directly from `by-kind.ts` (not through\n * `index.ts`), the re-export in the barrel is dead weight.\n *\n * Algorithm:\n * 1. Find all barrel files (index.ts / index.js)\n * 2. For each barrel, find symbols it re-exports (defined elsewhere, referenced in barrel with role=0)\n * 3. For each re-exported symbol, count consumers through the barrel vs direct from the source\n * 4. If zero consumers go through the barrel, the re-export is redundant\n */\nexport function redundantReexports(\n db: ScipDatabase,\n opts: { scope?: string; limit?: number } = {},\n): RedundantReexport[] {\n const { scope, limit } = opts;\n\n const scopeFilter = scope ? `AND barrel_d.relative_path LIKE '%${scope}%'` : '';\n\n // Step 1 + 2: Find all barrel files and symbols they re-export.\n // A re-export is a symbol that:\n // - is mentioned in a barrel file with role=0 (reference/import)\n // - has its definition (defn_enclosing_ranges) in a DIFFERENT file\n const reexportRows = db.all<{\n barrel_doc_id: number;\n barrel_path: string;\n symbol_id: number;\n symbol: string;\n original_doc_id: number;\n original_path: string;\n }>(\n `SELECT DISTINCT\n barrel_d.id AS barrel_doc_id,\n barrel_d.relative_path AS barrel_path,\n gs.id AS symbol_id,\n gs.symbol AS symbol,\n orig_d.id AS original_doc_id,\n orig_d.relative_path AS original_path\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents barrel_d ON c.document_id = barrel_d.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents orig_d ON der.document_id = orig_d.id\n WHERE m.role = 0\n AND (barrel_d.relative_path LIKE '%/index.ts'\n OR barrel_d.relative_path LIKE '%/index.js'\n OR barrel_d.relative_path = 'index.ts'\n OR barrel_d.relative_path = 'index.js')\n AND orig_d.id != barrel_d.id\n ${db.pathExclusionsFor('barrel_d', 'orig_d')}\n ${db.symbolNoiseFor('gs')}\n -- Only function-level symbols (ending with ().), not module-level\n AND gs.symbol LIKE '%().'\n ${scopeFilter}\n ORDER BY barrel_d.relative_path, gs.symbol`,\n );\n\n const results: RedundantReexport[] = [];\n\n for (const row of reexportRows) {\n if (db.isIgnored(row.barrel_path) || db.isIgnored(row.original_path)) continue;\n\n // Step 3: Count consumers that reference this symbol through the barrel\n // A \"barrel consumer\" is a file (other than the barrel itself and the original file)\n // that mentions this symbol AND also mentions something from the barrel document.\n // More precisely: count distinct files that reference this symbol AND whose\n // chunk is in a document that also has a role=0 mention pointing to the barrel file's symbols.\n //\n // Simpler approach: count distinct documents that reference this symbol (role=0)\n // grouped by whether the reference chunk is in a file that imports from the barrel\n // or from the original.\n //\n // Actually, the most reliable approach with SCIP data: count how many distinct\n // consumer documents reference this symbol_id with role=0, excluding the barrel\n // and the original file themselves. Then check if those consumers also reference\n // ANY symbol through a mention in the barrel doc vs the original doc.\n //\n // Simplest correct approach: In SCIP, when file A does `import { foo } from './bar/index'`,\n // the mention of `foo` in file A points to the same global symbol regardless of import path.\n // SCIP doesn't track import provenance. BUT the barrel file itself contains mentions\n // (role=0 references) of the re-exported symbols. So we can check:\n // - barrelConsumers: files that mention both this symbol AND any symbol whose definition\n // is in the barrel (i.e., they import the barrel)\n // - directConsumers: files that mention this symbol but don't import the barrel\n //\n // Even simpler: check if the barrel document is in the deps of the consumer.\n // A consumer \"goes through the barrel\" if it has ANY role=0 mention pointing to a\n // chunk in the barrel file. Otherwise it goes direct.\n\n const consumerCounts = db.get<{\n barrel_consumers: number;\n direct_consumers: number;\n }>(\n `SELECT\n SUM(CASE WHEN uses_barrel = 1 THEN 1 ELSE 0 END) AS barrel_consumers,\n SUM(CASE WHEN uses_barrel = 0 THEN 1 ELSE 0 END) AS direct_consumers\n FROM (\n SELECT\n consumer_d.id AS consumer_doc_id,\n MAX(CASE WHEN EXISTS (\n SELECT 1\n FROM mentions barrel_m\n JOIN chunks barrel_c ON barrel_m.chunk_id = barrel_c.id\n WHERE barrel_c.document_id = consumer_d.id\n AND barrel_m.role = 0\n AND barrel_m.symbol_id IN (\n SELECT m2.symbol_id\n FROM mentions m2\n JOIN chunks c2 ON m2.chunk_id = c2.id\n WHERE c2.document_id = ?\n AND m2.role = 0\n )\n ) THEN 1 ELSE 0 END) AS uses_barrel\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n JOIN documents consumer_d ON ref_c.document_id = consumer_d.id\n WHERE ref_m.symbol_id = ?\n AND ref_m.role = 0\n AND consumer_d.id != ?\n AND consumer_d.id != ?\n ${db.pathExclusionsFor('consumer_d')}\n GROUP BY consumer_d.id\n )`,\n row.barrel_doc_id, // for the inner subquery checking barrel mentions\n row.symbol_id, // the re-exported symbol\n row.barrel_doc_id, // exclude the barrel itself\n row.original_doc_id, // exclude the original file\n );\n\n const barrelConsumers = consumerCounts?.barrel_consumers ?? 0;\n const directConsumers = consumerCounts?.direct_consumers ?? 0;\n\n // In TypeScript, `import * as X from './barrel'` resolves all references\n // directly to the source file — the barrel is transparent to SCIP.\n // This means barrelConsumers is always 0 for namespace imports.\n //\n // We can only confidently report symbols with 0 consumers EVERYWHERE\n // (both barrel and direct). These are truly dead re-exports.\n //\n // Symbols with directConsumers > 0 but barrelConsumers === 0 might be\n // consumed through a namespace import — we can't tell, so we skip them.\n if (barrelConsumers === 0 && directConsumers === 0) {\n results.push({\n barrelFile: row.barrel_path,\n symbol: row.symbol,\n shortName: shortenSymbol(row.symbol),\n originalFile: row.original_path,\n barrelConsumers,\n directConsumers,\n });\n }\n }\n\n // Sort: symbols with the most direct consumers first (biggest cleanup wins),\n // then by barrel file path for stable output\n results.sort((a, b) =>\n b.directConsumers - a.directConsumers\n || a.barrelFile.localeCompare(b.barrelFile)\n || a.shortName.localeCompare(b.shortName),\n );\n\n return limit ? results.slice(0, limit) : results;\n}\n"],"mappings":";;;;;AAiBO,SAAS,mBACd,IACA,OAA2C,CAAC,GACvB;AACrB,QAAM,EAAE,OAAO,MAAM,IAAI;AAEzB,QAAM,cAAc,QAAQ,qCAAqC,KAAK,OAAO;AAM7E,QAAM,eAAe,GAAG;AAAA,IAQtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAmBI,GAAG,kBAAkB,YAAY,QAAQ,CAAC;AAAA,QAC1C,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA;AAAA,QAGvB,WAAW;AAAA;AAAA,EAEjB;AAEA,QAAM,UAA+B,CAAC;AAEtC,aAAW,OAAO,cAAc;AAC9B,QAAI,GAAG,UAAU,IAAI,WAAW,KAAK,GAAG,UAAU,IAAI,aAAa,EAAG;AA6BtE,UAAM,iBAAiB,GAAG;AAAA,MAIxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YA2BM,GAAG,kBAAkB,YAAY,CAAC;AAAA;AAAA;AAAA,MAGxC,IAAI;AAAA;AAAA,MACJ,IAAI;AAAA;AAAA,MACJ,IAAI;AAAA;AAAA,MACJ,IAAI;AAAA;AAAA,IACN;AAEA,UAAM,kBAAkB,gBAAgB,oBAAoB;AAC5D,UAAM,kBAAkB,gBAAgB,oBAAoB;AAW5D,QAAI,oBAAoB,KAAK,oBAAoB,GAAG;AAClD,cAAQ,KAAK;AAAA,QACX,YAAY,IAAI;AAAA,QAChB,QAAQ,IAAI;AAAA,QACZ,WAAW,cAAc,IAAI,MAAM;AAAA,QACnC,cAAc,IAAI;AAAA,QAClB;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAIA,UAAQ;AAAA,IAAK,CAAC,GAAG,MACf,EAAE,kBAAkB,EAAE,mBACnB,EAAE,WAAW,cAAc,EAAE,UAAU,KACvC,EAAE,UAAU,cAAc,EAAE,SAAS;AAAA,EAC1C;AAEA,SAAO,QAAQ,QAAQ,MAAM,GAAG,KAAK,IAAI;AAC3C;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/test-coverage.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { TEST_FILE_PATTERNS, testFileExclusionSql, testFileMatchSql } from '../query-support.js';\nimport type { TestCoverageResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Check if a symbol is referenced by any test file.\n * Reports which test files cover (reference) each matching symbol.\n */\nexport function testCoverage(\n db: ScipDatabase,\n symbolPattern: string,\n): TestCoverageResult[] {\n // Find matching symbols\n const syms = db.all<{\n id: number;\n symbol: string;\n relative_path: string;\n }>(\n `SELECT gs.id, gs.symbol, d.relative_path\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE gs.symbol LIKE ?\n ${db.pathExclusionsFor('d')}\n ${db.symbolNoiseFor('gs')}\n ORDER BY d.relative_path`,\n `%${symbolPattern}%`,\n );\n\n const testPatternSql = testFileMatchSql('ref_d', TEST_FILE_PATTERNS);\n\n return syms\n .filter((s) => !db.isIgnored(s.relative_path))\n .map((s) => {\n // Find test files that reference this symbol\n const testFiles = db.all<{ relative_path: string }>(\n `SELECT DISTINCT ref_d.relative_path\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents ref_d ON c.document_id = ref_d.id\n WHERE m.symbol_id = ?\n AND m.role = 0\n AND (${testPatternSql})\n ORDER BY ref_d.relative_path`,\n s.id,\n ).map((r) => r.relative_path);\n\n return {\n symbol: s.symbol,\n shortName: shortenSymbol(s.symbol),\n definedIn: s.relative_path,\n testFiles,\n covered: testFiles.length > 0,\n };\n });\n}\n\n/**\n * Summary: what percentage of symbols in scope are referenced by test files?\n */\nexport function testCoverageSummary(\n db: ScipDatabase,\n opts: { scope?: string; minLoc?: number } = {},\n): { total: number; covered: number; uncovered: number; percent: number } {\n const { scope, minLoc = 3 } = opts;\n const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';\n const testPatternSql = testFileExclusionSql('d');\n\n const symbols = db.all<{ id: number }>(\n `SELECT gs.id\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('d')}\n AND ${testPatternSql}\n ${db.symbolNoiseFor('gs')}\n AND gs.symbol NOT LIKE '%#%'\n AND (der.end_line - der.start_line + 1) >= ?\n ${scopeFilter}`,\n minLoc,\n );\n\n const testRefSql = testFileMatchSql('ref_d', TEST_FILE_PATTERNS);\n\n let covered = 0;\n for (const s of symbols) {\n const hasTest = db.get<{ c: number }>(\n `SELECT COUNT(*) AS c FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents ref_d ON c.document_id = ref_d.id\n WHERE m.symbol_id = ? AND m.role = 0 AND (${testRefSql})`,\n s.id,\n );\n if (hasTest && hasTest.c > 0) covered++;\n }\n\n const total = symbols.length;\n return {\n total,\n covered,\n uncovered: total - covered,\n percent: total > 0 ? Math.round((covered / total) * 100) : 0,\n };\n}\n"],"mappings":";;;;;;;;;;AASO,SAAS,aACd,IACA,eACsB;AAEtB,QAAM,OAAO,GAAG;AAAA,IAKd;AAAA;AAAA;AAAA;AAAA;AAAA,QAKI,GAAG,kBAAkB,GAAG,CAAC;AAAA,QACzB,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA,IAE3B,IAAI,aAAa;AAAA,EACnB;AAEA,QAAM,iBAAiB,iBAAiB,SAAS,kBAAkB;AAEnE,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,MAAM;AAEV,UAAM,YAAY,GAAG;AAAA,MACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAMS,cAAc;AAAA;AAAA,MAEvB,EAAE;AAAA,IACJ,EAAE,IAAI,CAAC,MAAM,EAAE,aAAa;AAE5B,WAAO;AAAA,MACL,QAAQ,EAAE;AAAA,MACV,WAAW,cAAc,EAAE,MAAM;AAAA,MACjC,WAAW,EAAE;AAAA,MACb;AAAA,MACA,SAAS,UAAU,SAAS;AAAA,IAC9B;AAAA,EACF,CAAC;AACL;AAKO,SAAS,oBACd,IACA,OAA4C,CAAC,GAC2B;AACxE,QAAM,EAAE,OAAO,SAAS,EAAE,IAAI;AAC9B,QAAM,cAAc,QAAQ,8BAA8B,KAAK,OAAO;AACtE,QAAM,iBAAiB,qBAAqB,GAAG;AAE/C,QAAM,UAAU,GAAG;AAAA,IACjB;AAAA;AAAA;AAAA;AAAA;AAAA,QAKI,GAAG,kBAAkB,GAAG,CAAC;AAAA,YACrB,cAAc;AAAA,QAClB,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA;AAAA,QAGvB,WAAW;AAAA,IACf;AAAA,EACF;AAEA,QAAM,aAAa,iBAAiB,SAAS,kBAAkB;AAE/D,MAAI,UAAU;AACd,aAAW,KAAK,SAAS;AACvB,UAAM,UAAU,GAAG;AAAA,MACjB;AAAA;AAAA;AAAA,mDAG6C,UAAU;AAAA,MACvD,EAAE;AAAA,IACJ;AACA,QAAI,WAAW,QAAQ,IAAI,EAAG;AAAA,EAChC;AAEA,QAAM,QAAQ,QAAQ;AACtB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW,QAAQ;AAAA,IACnB,SAAS,QAAQ,IAAI,KAAK,MAAO,UAAU,QAAS,GAAG,IAAI;AAAA,EAC7D;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/dead.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { TEST_SUPPORT_PATH_PATTERNS, testFileExclusionSql } from '../query-support.js';\nimport type { DeadOptions, DeadSymbolResult, DeadSummary } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find dead exports: symbols defined locally with no cross-file references.\n * Language-agnostic — works with any SCIP index.\n */\nexport function dead(db: ScipDatabase, opts: DeadOptions = {}): DeadSummary {\n const {\n scope,\n minLoc = 1,\n includeTests = false,\n skipBarrels = false,\n includeMembers = false,\n } = opts;\n\n const params: unknown[] = [minLoc];\n let testFileExclusions = '';\n let memberExclusion = '';\n\n if (scope) {\n params.push(`%${scope}%`);\n }\n\n if (!includeTests) {\n testFileExclusions = `\n AND ${testFileExclusionSql('d', TEST_SUPPORT_PATH_PATTERNS)}\n `;\n }\n\n if (!includeMembers) {\n memberExclusion = `AND gs.symbol NOT LIKE '%#%'`;\n }\n\n // Barrel file exclusion for the NOT EXISTS subquery\n const barrelExclusions = skipBarrels\n ? `AND ref_d.relative_path NOT LIKE '%/index.ts'\n AND ref_d.relative_path NOT LIKE '%/index.js'\n AND ref_d.relative_path NOT LIKE '%/mod.rs'\n AND ref_d.relative_path NOT LIKE '%/__init__.py'`\n : '';\n\n const sql = `\n SELECT\n d.relative_path,\n der.start_line,\n der.end_line,\n (der.end_line - der.start_line + 1) AS loc,\n gs.symbol,\n (SELECT COUNT(*) FROM mentions m2\n JOIN chunks c2 ON m2.chunk_id = c2.id\n WHERE m2.symbol_id = gs.id AND m2.role = 0 AND c2.document_id = d.id\n ) AS same_file_refs\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('d')}\n ${db.symbolNoiseFor('gs')}\n AND (der.end_line - der.start_line + 1) >= ?\n ${scope ? 'AND d.relative_path LIKE ?' : ''}\n ${testFileExclusions}\n ${memberExclusion}\n AND NOT EXISTS (\n SELECT 1\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n JOIN documents ref_d ON ref_c.document_id = ref_d.id\n WHERE ref_m.symbol_id = gs.id\n AND ref_m.role = 0\n AND ref_d.id != d.id\n ${barrelExclusions}\n )\n ORDER BY (der.end_line - der.start_line + 1) DESC, d.relative_path, der.start_line\n `;\n\n const rows = db.all<{\n relative_path: string;\n start_line: number;\n end_line: number;\n loc: number;\n symbol: string;\n same_file_refs: number;\n }>(sql, ...params);\n\n let deadCodeCount = 0;\n let fileInternalCount = 0;\n let totalLoc = 0;\n\n const symbols: DeadSymbolResult[] = rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => {\n // dead-code: zero references anywhere (not even in same file) — safe to delete\n // file-internal: referenced within same file but never cross-file —\n // may be a private helper (fine) or a forgotten export (needs review)\n const kind = r.same_file_refs === 0 ? 'dead-code' : 'file-internal';\n if (kind === 'dead-code') deadCodeCount++;\n else fileInternalCount++;\n totalLoc += r.loc;\n\n return {\n relativePath: r.relative_path,\n startLine: r.start_line,\n endLine: r.end_line,\n loc: r.loc,\n symbol: r.symbol,\n shortName: shortenSymbol(r.symbol),\n sameFileRefs: r.same_file_refs,\n kind,\n };\n });\n\n return {\n symbols,\n totalCount: symbols.length,\n deadCodeCount,\n fileInternalCount,\n totalLoc,\n };\n}\n"],"mappings":";;;;;;;;;AASO,SAAS,KAAK,IAAkB,OAAoB,CAAC,GAAgB;AAC1E,QAAM;AAAA,IACJ;AAAA,IACA,SAAS;AAAA,IACT,eAAe;AAAA,IACf,cAAc;AAAA,IACd,iBAAiB;AAAA,EACnB,IAAI;AAEJ,QAAM,SAAoB,CAAC,MAAM;AACjC,MAAI,qBAAqB;AACzB,MAAI,kBAAkB;AAEtB,MAAI,OAAO;AACT,WAAO,KAAK,IAAI,KAAK,GAAG;AAAA,EAC1B;AAEA,MAAI,CAAC,cAAc;AACjB,yBAAqB;AAAA,YACb,qBAAqB,KAAK,0BAA0B,CAAC;AAAA;AAAA,EAE/D;AAEA,MAAI,CAAC,gBAAgB;AACnB,sBAAkB;AAAA,EACpB;AAGA,QAAM,mBAAmB,cACrB;AAAA;AAAA;AAAA,2DAIA;AAEJ,QAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAeN,GAAG,kBAAkB,GAAG,CAAC;AAAA,QACzB,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA,QAEvB,QAAQ,+BAA+B,EAAE;AAAA,QACzC,kBAAkB;AAAA,QAClB,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YASX,gBAAgB;AAAA;AAAA;AAAA;AAK1B,QAAM,OAAO,GAAG,IAOb,KAAK,GAAG,MAAM;AAEjB,MAAI,gBAAgB;AACpB,MAAI,oBAAoB;AACxB,MAAI,WAAW;AAEf,QAAM,UAA8B,KACjC,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,MAAM;AAIV,UAAM,OAAO,EAAE,mBAAmB,IAAI,cAAc;AACpD,QAAI,SAAS,YAAa;AAAA,QACrB;AACL,gBAAY,EAAE;AAEd,WAAO;AAAA,MACL,cAAc,EAAE;AAAA,MAChB,WAAW,EAAE;AAAA,MACb,SAAS,EAAE;AAAA,MACX,KAAK,EAAE;AAAA,MACP,QAAQ,EAAE;AAAA,MACV,WAAW,cAAc,EAAE,MAAM;AAAA,MACjC,cAAc,EAAE;AAAA,MAChB;AAAA,IACF;AAAA,EACF,CAAC;AAEH,SAAO;AAAA,IACL;AAAA,IACA,YAAY,QAAQ;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/refs.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { RefResult } from '../types.js';\n\nexport function refs(db: ScipDatabase, symbolPattern: string): RefResult[] {\n const rows = db.all<{\n relative_path: string;\n start_line: number;\n }>(\n `SELECT DISTINCT d.relative_path, c.start_line\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n WHERE gs.symbol LIKE ?\n AND ${db.localSymbolPredicate}\n AND m.role = 0\n ORDER BY d.relative_path, c.start_line`,\n `%${symbolPattern}%`,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({\n relativePath: r.relative_path,\n line: r.start_line,\n }));\n}\n"],"mappings":";AAGO,SAAS,KAAK,IAAkB,eAAoC;AACzE,QAAM,OAAO,GAAG;AAAA,IAId;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAMQ,GAAG,oBAAoB;AAAA;AAAA;AAAA,IAG/B,IAAI,aAAa;AAAA,EACnB;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO;AAAA,IACX,cAAc,EAAE;AAAA,IAChB,MAAM,EAAE;AAAA,EACV,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/complexity.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { ScipDatabase } from '../db.js';\nimport { findFirstSymbolMatch, getCalleeRowsForSymbol } from '../query-support.js';\nimport type { ComplexityResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Per-symbol complexity analysis combining source-level branch counting\n * with index-level metrics (fan-in, fan-out, callee count).\n *\n * Branch counting uses language-aware regex. The language is read from\n * the SCIP documents table, so it works for any indexed language.\n */\nexport function complexity(\n db: ScipDatabase,\n symbolPattern: string,\n): ComplexityResult | null {\n const match = findFirstSymbolMatch(db, symbolPattern);\n if (!match) return null;\n\n // Get language\n const doc = db.get<{ language: string | null }>(\n `SELECT language FROM documents WHERE relative_path = ?`,\n match.relativePath,\n );\n const language = doc?.language ?? 'unknown';\n\n // Read source for branch counting\n const filePath = join(db.config.projectRoot, match.relativePath);\n let source = '';\n try {\n const lines = readFileSync(filePath, 'utf-8').split('\\n');\n source = lines.slice(match.startLine, match.endLine + 1).join('\\n');\n } catch {\n // If we can't read the file, just skip branch counting\n }\n\n const branches = countBranches(source, language);\n const loc = match.endLine - match.startLine + 1;\n\n // Callee count\n const callees = getCalleeRowsForSymbol(db, match);\n const uniqueCallees = new Set(callees.map((c) => c.symbol));\n\n // Fan-in\n const fanInRow = db.get<{ c: number }>(\n `SELECT COUNT(DISTINCT c.document_id) AS c\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n WHERE m.symbol_id = ? AND m.role = 0`,\n match.symbolId,\n );\n\n // Fan-out (callees in other files)\n const fanOut = new Set(\n callees.filter((c) => c.file !== match.relativePath).map((c) => c.symbol),\n ).size;\n\n return {\n symbol: match.symbol,\n shortName: shortenSymbol(match.symbol),\n relativePath: match.relativePath,\n startLine: match.startLine,\n endLine: match.endLine,\n loc,\n branches,\n cyclomaticEstimate: branches + 1,\n calleeCount: uniqueCallees.size,\n fanIn: fanInRow?.c ?? 0,\n fanOut,\n };\n}\n\n/**\n * Count branch points in source code using language-aware regex.\n * Works across all SCIP-supported languages.\n */\nfunction countBranches(source: string, language: string): number {\n // Strip comments and strings to avoid false positives\n const stripped = stripCommentsAndStrings(source);\n let count = 0;\n\n // Universal branch keywords (work across most C-family languages)\n const universalPatterns = [\n /\\bif\\b/g,\n /\\belse\\s+if\\b/g,\n /\\belse\\b/g,\n /\\bfor\\b/g,\n /\\bwhile\\b/g,\n /\\bswitch\\b/g,\n /\\bcase\\b/g,\n /\\bcatch\\b/g,\n /\\?\\s*[^?]/g, // ternary (but not ??)\n /&&/g,\n /\\|\\|/g,\n ];\n\n for (const pattern of universalPatterns) {\n const matches = stripped.match(pattern);\n if (matches) count += matches.length;\n }\n\n // Language-specific patterns\n if (language === 'python') {\n const pyPatterns = [/\\belif\\b/g, /\\bexcept\\b/g, /\\bfinally\\b/g];\n for (const p of pyPatterns) {\n const m = stripped.match(p);\n if (m) count += m.length;\n }\n } else if (language === 'rust') {\n const rustPatterns = [/\\bmatch\\b/g, /=>/g, /\\bloop\\b/g];\n for (const p of rustPatterns) {\n const m = stripped.match(p);\n if (m) count += m.length;\n }\n } else if (language === 'ruby') {\n const rubyPatterns = [/\\belsif\\b/g, /\\bunless\\b/g, /\\brescue\\b/g, /\\bwhen\\b/g];\n for (const p of rubyPatterns) {\n const m = stripped.match(p);\n if (m) count += m.length;\n }\n } else if (language === 'go') {\n const goPatterns = [/\\bselect\\b/g, /\\bdefer\\b/g];\n for (const p of goPatterns) {\n const m = stripped.match(p);\n if (m) count += m.length;\n }\n }\n\n return count;\n}\n\n/**\n * Rough strip of comments and string literals to reduce false positives\n * in branch counting. Not perfect but good enough for estimation.\n */\nfunction stripCommentsAndStrings(source: string): string {\n return source\n // Block comments\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n // Line comments\n .replace(/\\/\\/.*/g, '')\n // Python/Ruby line comments\n .replace(/#.*/g, '')\n // Double-quoted strings\n .replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, '\"\"')\n // Single-quoted strings\n .replace(/'(?:[^'\\\\]|\\\\.)*'/g, \"''\")\n // Template literals\n .replace(/`(?:[^`\\\\]|\\\\.)*`/g, '``');\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/complexity-hotspots.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { testFileExclusionSql } from '../query-support.js';\nimport type { ComplexityHotspot } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find complexity hotspots: symbols with a composite score based on\n * LOC, fan-in, fan-out, and callee count.\n *\n * Score = (loc / 50) * (fanIn / 5) * max(fanOut / 5, 1)\n *\n * High scores indicate symbols that are large, widely depended upon,\n * AND reach out to many other modules — the riskiest code to change.\n */\nexport function complexityHotspots(\n db: ScipDatabase,\n opts?: { scope?: string; minLoc?: number; limit?: number },\n): ComplexityHotspot[] {\n const { scope, minLoc = 10, limit = 30 } = opts ?? {};\n const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';\n\n const rows = db.all<{\n symbol: string;\n file: string;\n start_line: number;\n end_line: number;\n loc: number;\n fan_in: number;\n fan_out: number;\n callee_count: number;\n }>(\n `SELECT\n gs.symbol,\n d.relative_path AS file,\n der.start_line,\n der.end_line,\n (der.end_line - der.start_line + 1) AS loc,\n -- fanIn: distinct files that reference this symbol\n (SELECT COUNT(DISTINCT ref_c.document_id)\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n WHERE ref_m.symbol_id = gs.id AND ref_m.role = 0\n ) AS fan_in,\n -- fanOut: distinct symbols referenced within this definition range\n -- that are defined in different files\n (SELECT COUNT(DISTINCT out_gs.id)\n FROM mentions out_m\n JOIN chunks out_c ON out_m.chunk_id = out_c.id\n JOIN global_symbols out_gs ON out_m.symbol_id = out_gs.id\n JOIN defn_enclosing_ranges out_der ON out_gs.id = out_der.symbol_id\n WHERE out_c.document_id = der.document_id\n AND out_c.start_line >= der.start_line\n AND out_c.end_line <= der.end_line\n AND out_m.role = 0\n AND out_gs.id != gs.id\n AND out_der.document_id != der.document_id\n ) AS fan_out,\n -- calleeCount: total distinct callees within definition range\n (SELECT COUNT(DISTINCT callee_gs.id)\n FROM mentions callee_m\n JOIN chunks callee_c ON callee_m.chunk_id = callee_c.id\n JOIN global_symbols callee_gs ON callee_m.symbol_id = callee_gs.id\n WHERE callee_c.document_id = der.document_id\n AND callee_c.start_line >= der.start_line\n AND callee_c.end_line <= der.end_line\n AND callee_m.role = 0\n AND callee_gs.id != gs.id\n ) AS callee_count\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('d')}\n AND ${testFileExclusionSql('d')}\n ${db.symbolNoiseFor('gs')}\n AND (der.end_line - der.start_line + 1) >= ?\n ${scopeFilter}\n ORDER BY (\n CAST((der.end_line - der.start_line + 1) AS REAL) / 50.0\n * CAST((SELECT COUNT(DISTINCT ref_c2.document_id)\n FROM mentions ref_m2\n JOIN chunks ref_c2 ON ref_m2.chunk_id = ref_c2.id\n WHERE ref_m2.symbol_id = gs.id AND ref_m2.role = 0\n ) AS REAL) / 5.0\n * MAX(CAST((SELECT COUNT(DISTINCT out_gs2.id)\n FROM mentions out_m2\n JOIN chunks out_c2 ON out_m2.chunk_id = out_c2.id\n JOIN global_symbols out_gs2 ON out_m2.symbol_id = out_gs2.id\n JOIN defn_enclosing_ranges out_der2 ON out_gs2.id = out_der2.symbol_id\n WHERE out_c2.document_id = der.document_id\n AND out_c2.start_line >= der.start_line\n AND out_c2.end_line <= der.end_line\n AND out_m2.role = 0\n AND out_gs2.id != gs.id\n AND out_der2.document_id != der.document_id\n ) AS REAL) / 5.0, 1.0)\n ) DESC\n LIMIT ?`,\n minLoc, limit,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.file))\n .map((r) => ({\n symbol: r.symbol,\n shortName: shortenSymbol(r.symbol),\n file: r.file,\n startLine: r.start_line,\n endLine: r.end_line,\n loc: r.loc,\n fanIn: r.fan_in,\n fanOut: r.fan_out,\n calleeCount: r.callee_count,\n score:\n Math.round(\n (r.loc / 50) * (r.fan_in / 5) * Math.max(r.fan_out / 5, 1) * 100,\n ) / 100,\n }));\n}\n"],"mappings":";;;;;;;;AAcO,SAAS,mBACd,IACA,MACqB;AACrB,QAAM,EAAE,OAAO,SAAS,IAAI,QAAQ,GAAG,IAAI,QAAQ,CAAC;AACpD,QAAM,cAAc,QAAQ,8BAA8B,KAAK,OAAO;AAEtE,QAAM,OAAO,GAAG;AAAA,IAUd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAyCI,GAAG,kBAAkB,GAAG,CAAC;AAAA,YACrB,qBAAqB,GAAG,CAAC;AAAA,QAC7B,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA,QAEvB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAsBf;AAAA,IAAQ;AAAA,EACV;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,EACnC,IAAI,CAAC,OAAO;AAAA,IACX,QAAQ,EAAE;AAAA,IACV,WAAW,cAAc,EAAE,MAAM;AAAA,IACjC,MAAM,EAAE;AAAA,IACR,WAAW,EAAE;AAAA,IACb,SAAS,EAAE;AAAA,IACX,KAAK,EAAE;AAAA,IACP,OAAO,EAAE;AAAA,IACT,QAAQ,EAAE;AAAA,IACV,aAAa,EAAE;AAAA,IACf,OACE,KAAK;AAAA,MACF,EAAE,MAAM,MAAO,EAAE,SAAS,KAAK,KAAK,IAAI,EAAE,UAAU,GAAG,CAAC,IAAI;AAAA,IAC/D,IAAI;AAAA,EACR,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/query-support.ts"],"sourcesContent":["import type { ScipDatabase } from './db.js';\n\nexport interface SymbolLocation {\n documentId: number;\n startLine: number;\n endLine: number;\n symbolId: number;\n}\n\nexport interface SymbolMatch extends SymbolLocation {\n symbol: string;\n relativePath: string;\n}\n\nexport interface CalleeRow {\n symbol: string;\n file: string;\n chunkId: number;\n}\n\nexport const TEST_FILE_PATTERNS = [\n '%/__tests__/%',\n '%.test.%',\n '%.spec.%',\n '%/test/%',\n '%/tests/%',\n '%_test.%',\n '%_spec.%',\n '%/test_%.%',\n '%/spec_%.%',\n] as const;\n\nexport const TEST_SUPPORT_PATH_PATTERNS = [\n '%/test-utils/%',\n] as const;\n\nexport function testFileMatchSql(\n alias: string,\n patterns: readonly string[] = TEST_FILE_PATTERNS,\n): string {\n return `(${patterns.map((pattern) => `${alias}.relative_path LIKE '${pattern}'`).join(' OR ')})`;\n}\n\nexport function testFileExclusionSql(\n alias: string,\n extraPatterns: readonly string[] = [],\n): string {\n const patterns = uniquePatterns([...TEST_FILE_PATTERNS, ...extraPatterns]);\n return patterns\n .map((pattern) => `${alias}.relative_path NOT LIKE '${pattern}'`)\n .join('\\n AND ');\n}\n\nexport function buildFileDepGraph(\n db: ScipDatabase,\n scope?: string,\n): Map<string, Set<string>> {\n const scopeFilter = scope ? `AND d1.relative_path LIKE '%${scope}%'` : '';\n\n const edges = db.all<{ from_file: string; to_file: string }>(\n `SELECT DISTINCT\n d1.relative_path AS from_file,\n d2.relative_path AS to_file\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d1 ON c.document_id = d1.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d2 ON der.document_id = d2.id\n WHERE d1.id != d2.id\n AND m.role = 0\n ${db.pathExclusionsFor('d1', 'd2')}\n ${scopeFilter}`,\n );\n\n const graph = new Map<string, Set<string>>();\n for (const edge of edges) {\n if (db.isIgnored(edge.from_file) || db.isIgnored(edge.to_file)) continue;\n if (!graph.has(edge.from_file)) graph.set(edge.from_file, new Set());\n graph.get(edge.from_file)!.add(edge.to_file);\n }\n\n return graph;\n}\n\nexport function findFirstSymbolMatch(\n db: ScipDatabase,\n symbolPattern: string,\n): SymbolMatch | null {\n // Handle file:line-line syntax (e.g., \"src/foo.ts:10-50\")\n const fileLineMatch = symbolPattern.match(/^(.+):(\\d+)-(\\d+)$/);\n if (fileLineMatch) {\n const [, filePath, startStr, endStr] = fileLineMatch;\n const row = db.get<{\n id: number;\n symbol: string;\n document_id: number;\n start_line: number;\n end_line: number;\n relative_path: string;\n }>(\n `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE d.relative_path LIKE ?\n AND der.start_line <= ? AND der.end_line >= ?\n ${db.pathExclusionsFor('d')}\n ORDER BY (der.end_line - der.start_line) ASC\n LIMIT 1`,\n `%${filePath}%`, parseInt(startStr!, 10), parseInt(endStr!, 10),\n );\n if (row && !db.isIgnored(row.relative_path)) {\n return {\n symbolId: row.id,\n symbol: row.symbol,\n documentId: row.document_id,\n startLine: row.start_line,\n endLine: row.end_line,\n relativePath: row.relative_path,\n };\n }\n }\n\n // Strip parentheses from the pattern to avoid shell escaping issues.\n // Agents often pass \"functionName()\" — strip the () for matching.\n const cleaned = symbolPattern.replace(/\\(\\)$/, '').replace(/\\(.*$/, '');\n\n // Try exact-ish match first (with noise filter), then fallback without noise filter.\n // The noise filter excludes %().(% which can incorrectly match some symbols.\n for (const useNoiseFilter of [true, false]) {\n const noiseClause = useNoiseFilter ? db.symbolNoiseFor('gs') : '';\n const row = db.get<{\n id: number;\n symbol: string;\n document_id: number;\n start_line: number;\n end_line: number;\n relative_path: string;\n }>(\n `SELECT gs.id, gs.symbol, der.document_id, der.start_line, der.end_line, d.relative_path\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE gs.symbol LIKE ?\n ${db.pathExclusionsFor('d')}\n ${noiseClause}\n ORDER BY (der.end_line - der.start_line) DESC\n LIMIT 1`,\n `%${cleaned}%`,\n );\n\n if (row && !db.isIgnored(row.relative_path)) {\n return {\n symbolId: row.id,\n symbol: row.symbol,\n documentId: row.document_id,\n startLine: row.start_line,\n endLine: row.end_line,\n relativePath: row.relative_path,\n };\n }\n }\n\n return null;\n}\n\nexport function getCalleeRowsForSymbol(\n db: ScipDatabase,\n symbol: SymbolLocation,\n opts: { limit?: number } = {},\n): CalleeRow[] {\n const rows = db.all<{\n symbol: string;\n file: string;\n chunk_id: number;\n }>(\n `SELECT DISTINCT\n callee_gs.symbol AS symbol,\n callee_d.relative_path AS file,\n c.id AS chunk_id\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN global_symbols callee_gs ON m.symbol_id = callee_gs.id\n JOIN defn_enclosing_ranges callee_der ON callee_gs.id = callee_der.symbol_id\n JOIN documents callee_d ON callee_der.document_id = callee_d.id\n WHERE c.document_id = ?\n AND c.start_line >= ?\n AND c.end_line <= ?\n AND m.role = 0\n AND callee_gs.id != ?\n ${db.symbolNoiseFor('callee_gs')}\n ${db.pathExclusionsFor('callee_d')}\n ORDER BY callee_d.relative_path\n ${opts.limit ? 'LIMIT ?' : ''}`,\n ...calleeQueryParams(symbol, opts.limit),\n );\n\n return rows.filter((row) => !db.isIgnored(row.file)).map((row) => ({\n symbol: row.symbol,\n file: row.file,\n chunkId: row.chunk_id,\n }));\n}\n\nfunction calleeQueryParams(\n symbol: SymbolLocation,\n limit?: number,\n): number[] {\n const params = [\n symbol.documentId,\n symbol.startLine,\n symbol.endLine,\n symbol.symbolId,\n ];\n\n if (typeof limit === 'number') {\n params.push(limit);\n }\n\n return params;\n}\n\nfunction uniquePatterns(patterns: readonly string[]): string[] {\n return [...new Set(patterns)];\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/stale-abstractions.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { testFileExclusionSql } from '../query-support.js';\nimport type { StaleAbstraction } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find stale abstractions: type-level symbols (classes, interfaces, type\n * aliases) that have 0 or 1 cross-file consumers.\n *\n * A type that only one file uses is over-abstracted — it was designed\n * for reuse that never materialized. Large single-consumer types are\n * the strongest signal of wasted abstraction.\n */\nexport function staleAbstractions(\n db: ScipDatabase,\n opts?: { scope?: string; minLoc?: number; limit?: number },\n): StaleAbstraction[] {\n const { scope, minLoc = 3, limit = 30 } = opts ?? {};\n const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';\n\n const rows = db.all<{\n symbol: string;\n file: string;\n start_line: number;\n end_line: number;\n loc: number;\n consumers: number;\n }>(\n `SELECT * FROM (\n SELECT\n gs.symbol,\n d.relative_path AS file,\n der.start_line,\n der.end_line,\n (der.end_line - der.start_line + 1) AS loc,\n (SELECT COUNT(DISTINCT ref_c.document_id)\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n WHERE ref_m.symbol_id = gs.id\n AND ref_m.role = 0\n AND ref_c.document_id != der.document_id\n ) AS consumers\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('d')}\n AND ${testFileExclusionSql('d')}\n ${db.symbolNoiseFor('gs')}\n -- Top-level type symbols: ends with # but does not contain nested #\n AND gs.symbol LIKE '%#'\n AND gs.symbol NOT LIKE '%#%#%'\n AND (der.end_line - der.start_line + 1) >= ?\n ${scopeFilter}\n ) WHERE consumers <= 1\n ORDER BY loc DESC\n LIMIT ?`,\n minLoc, limit,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.file))\n // Exclude types defined in dedicated type files (types.ts, types/, etc.)\n // These are intentional public API types, not premature abstractions.\n .filter((r) => {\n const basename = r.file.split('/').pop() ?? '';\n const isTypeFile = basename.includes('types') || r.file.includes('/types/');\n // Types in type files with 1 consumer are normal API types — skip them.\n // Types in type files with 0 consumers are genuinely unused — keep them.\n if (isTypeFile && r.consumers > 0) return false;\n return true;\n })\n .map((r) => ({\n symbol: r.symbol,\n shortName: shortenSymbol(r.symbol),\n file: r.file,\n startLine: r.start_line,\n endLine: r.end_line,\n loc: r.loc,\n consumers: r.consumers,\n }));\n}\n"],"mappings":";;;;;;;;AAaO,SAAS,kBACd,IACA,MACoB;AACpB,QAAM,EAAE,OAAO,SAAS,GAAG,QAAQ,GAAG,IAAI,QAAQ,CAAC;AACnD,QAAM,cAAc,QAAQ,8BAA8B,KAAK,OAAO;AAEtE,QAAM,OAAO,GAAG;AAAA,IAQd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAkBM,GAAG,kBAAkB,GAAG,CAAC;AAAA,cACrB,qBAAqB,GAAG,CAAC;AAAA,UAC7B,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,UAKvB,WAAW;AAAA;AAAA;AAAA;AAAA,IAIjB;AAAA,IAAQ;AAAA,EACV;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,EAGnC,OAAO,CAAC,MAAM;AACb,UAAM,WAAW,EAAE,KAAK,MAAM,GAAG,EAAE,IAAI,KAAK;AAC5C,UAAM,aAAa,SAAS,SAAS,OAAO,KAAK,EAAE,KAAK,SAAS,SAAS;AAG1E,QAAI,cAAc,EAAE,YAAY,EAAG,QAAO;AAC1C,WAAO;AAAA,EACT,CAAC,EACA,IAAI,CAAC,OAAO;AAAA,IACX,QAAQ,EAAE;AAAA,IACV,WAAW,cAAc,EAAE,MAAM;AAAA,IACjC,MAAM,EAAE;AAAA,IACR,WAAW,EAAE;AAAA,IACb,SAAS,EAAE;AAAA,IACX,KAAK,EAAE;AAAA,IACP,WAAW,EAAE;AAAA,EACf,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/fan.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { FanResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Fan-in: how many distinct files reference this symbol.\n * High fan-in = widely depended upon = high blast radius for changes.\n */\nexport function fanIn(\n db: ScipDatabase,\n symbolPattern: string,\n): FanResult[] {\n const rows = db.all<{\n symbol: string;\n file_count: number;\n }>(\n `SELECT gs.symbol, COUNT(DISTINCT c.document_id) AS file_count\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n WHERE gs.symbol LIKE ?\n AND m.role = 0\n GROUP BY gs.id\n ORDER BY file_count DESC`,\n `%${symbolPattern}%`,\n );\n\n return rows.map((r) => ({\n name: shortenSymbol(r.symbol),\n count: r.file_count,\n }));\n}\n\n/**\n * Fan-out: how many external symbols does this file reference.\n * High fan-out = depends on many things = fragile to upstream changes.\n */\nexport function fanOut(\n db: ScipDatabase,\n filePattern: string,\n): FanResult[] {\n const rows = db.all<{\n relative_path: string;\n symbol_count: number;\n }>(\n `SELECT d.relative_path, COUNT(DISTINCT gs.id) AS symbol_count\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents def_d ON der.document_id = def_d.id\n WHERE d.relative_path LIKE ?\n AND m.role = 0\n AND def_d.id != d.id\n GROUP BY d.id\n ORDER BY symbol_count DESC`,\n `%${filePattern}%`,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({\n name: r.relative_path,\n count: r.symbol_count,\n }));\n}\n\n/**\n * Top fan-in across the whole codebase — the most depended-on symbols.\n */\nexport function topFanIn(\n db: ScipDatabase,\n opts: { limit?: number; scope?: string } = {},\n): FanResult[] {\n const { limit = 30, scope } = opts;\n const scopeFilter = scope\n ? `AND def_d.relative_path LIKE '%${scope}%'`\n : '';\n\n const rows = db.all<{\n symbol: string;\n file_count: number;\n }>(\n `SELECT gs.symbol, COUNT(DISTINCT c.document_id) AS file_count\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents def_d ON der.document_id = def_d.id\n WHERE m.role = 0\n ${db.pathExclusionsFor('def_d')}\n ${db.symbolNoiseFor('gs')}\n ${scopeFilter}\n GROUP BY gs.id\n HAVING file_count > 1\n ORDER BY file_count DESC\n LIMIT ?`,\n limit,\n );\n\n return rows.map((r) => ({\n name: shortenSymbol(r.symbol),\n count: r.file_count,\n }));\n}\n\n/**\n * Top fan-out across the whole codebase — files that depend on the most external symbols.\n */\nexport function topFanOut(\n db: ScipDatabase,\n opts: { limit?: number; scope?: string } = {},\n): FanResult[] {\n const { limit = 30, scope } = opts;\n const scopeFilter = scope\n ? `AND d.relative_path LIKE '%${scope}%'`\n : '';\n\n const rows = db.all<{\n relative_path: string;\n symbol_count: number;\n }>(\n `SELECT d.relative_path, COUNT(DISTINCT gs.id) AS symbol_count\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents def_d ON der.document_id = def_d.id\n WHERE m.role = 0\n AND def_d.id != d.id\n ${db.pathExclusionsFor('d')}\n ${db.symbolNoiseFor('gs')}\n ${scopeFilter}\n GROUP BY d.id\n ORDER BY symbol_count DESC\n LIMIT ?`,\n limit,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.relative_path))\n .map((r) => ({\n name: r.relative_path,\n count: r.symbol_count,\n }));\n}\n"],"mappings":";;;;;AAQO,SAAS,MACd,IACA,eACa;AACb,QAAM,OAAO,GAAG;AAAA,IAId;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,IAAI,aAAa;AAAA,EACnB;AAEA,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,MAAM,cAAc,EAAE,MAAM;AAAA,IAC5B,OAAO,EAAE;AAAA,EACX,EAAE;AACJ;AAMO,SAAS,OACd,IACA,aACa;AACb,QAAM,OAAO,GAAG;AAAA,IAId;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAYA,IAAI,WAAW;AAAA,EACjB;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO;AAAA,IACX,MAAM,EAAE;AAAA,IACR,OAAO,EAAE;AAAA,EACX,EAAE;AACN;AAKO,SAAS,SACd,IACA,OAA2C,CAAC,GAC/B;AACb,QAAM,EAAE,QAAQ,IAAI,MAAM,IAAI;AAC9B,QAAM,cAAc,QAChB,kCAAkC,KAAK,OACvC;AAEJ,QAAM,OAAO,GAAG;AAAA,IAId;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOI,GAAG,kBAAkB,OAAO,CAAC;AAAA,QAC7B,GAAG,eAAe,IAAI,CAAC;AAAA,QACvB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,IAKf;AAAA,EACF;AAEA,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,MAAM,cAAc,EAAE,MAAM;AAAA,IAC5B,OAAO,EAAE;AAAA,EACX,EAAE;AACJ;AAKO,SAAS,UACd,IACA,OAA2C,CAAC,GAC/B;AACb,QAAM,EAAE,QAAQ,IAAI,MAAM,IAAI;AAC9B,QAAM,cAAc,QAChB,8BAA8B,KAAK,OACnC;AAEJ,QAAM,OAAO,GAAG;AAAA,IAId;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QASI,GAAG,kBAAkB,GAAG,CAAC;AAAA,QACzB,GAAG,eAAe,IAAI,CAAC;AAAA,QACvB,WAAW;AAAA;AAAA;AAAA;AAAA,IAIf;AAAA,EACF;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,aAAa,CAAC,EAC5C,IAAI,CAAC,OAAO;AAAA,IACX,MAAM,EAAE;AAAA,IACR,OAAO,EAAE;AAAA,EACX,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/coupling.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport type { CouplingResult } from '../types.js';\n\n/**\n * Measure coupling between two files: how many symbols do they share\n * (symbols defined in one and referenced in the other, or vice versa).\n */\nexport function coupling(\n db: ScipDatabase,\n file1: string,\n file2: string,\n): CouplingResult {\n const row = db.get<{ shared: number }>(\n `SELECT COUNT(DISTINCT gs.id) AS shared\n FROM global_symbols gs\n WHERE (\n -- Defined in file1, referenced in file2\n EXISTS (\n SELECT 1 FROM defn_enclosing_ranges der\n JOIN documents d ON der.document_id = d.id\n WHERE der.symbol_id = gs.id AND d.relative_path LIKE ?\n )\n AND EXISTS (\n SELECT 1 FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n WHERE m.symbol_id = gs.id AND m.role = 0 AND d.relative_path LIKE ?\n )\n ) OR (\n -- Defined in file2, referenced in file1\n EXISTS (\n SELECT 1 FROM defn_enclosing_ranges der\n JOIN documents d ON der.document_id = d.id\n WHERE der.symbol_id = gs.id AND d.relative_path LIKE ?\n )\n AND EXISTS (\n SELECT 1 FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n WHERE m.symbol_id = gs.id AND m.role = 0 AND d.relative_path LIKE ?\n )\n )`,\n `%${file1}%`, `%${file2}%`,\n `%${file2}%`, `%${file1}%`,\n );\n\n return {\n file1,\n file2,\n sharedSymbols: row?.shared ?? 0,\n };\n}\n\n/**\n * Find the most coupled file pairs in the codebase.\n */\nexport function topCoupling(\n db: ScipDatabase,\n opts: { limit?: number; scope?: string } = {},\n): CouplingResult[] {\n const { limit = 20, scope } = opts;\n const scopeFilter = scope\n ? `AND d1.relative_path LIKE '%${scope}%' AND d2.relative_path LIKE '%${scope}%'`\n : '';\n\n // Find file pairs that share the most symbols (one defines, other references)\n const rows = db.all<{\n file1: string;\n file2: string;\n shared: number;\n }>(\n `SELECT\n def_d.relative_path AS file1,\n ref_d.relative_path AS file2,\n COUNT(DISTINCT gs.id) AS shared\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents ref_d ON c.document_id = ref_d.id\n JOIN global_symbols gs ON m.symbol_id = gs.id\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents def_d ON der.document_id = def_d.id\n WHERE m.role = 0\n AND def_d.id != ref_d.id\n ${db.pathExclusionsFor('def_d', 'ref_d')}\n ${scopeFilter}\n GROUP BY def_d.id, ref_d.id\n ORDER BY shared DESC\n LIMIT ?`,\n limit,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.file1) && !db.isIgnored(r.file2))\n .map((r) => ({\n file1: r.file1,\n file2: r.file2,\n sharedSymbols: r.shared,\n }));\n}\n"],"mappings":";AAOO,SAAS,SACd,IACA,OACA,OACgB;AAChB,QAAM,MAAM,GAAG;AAAA,IACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IA6BA,IAAI,KAAK;AAAA,IAAK,IAAI,KAAK;AAAA,IACvB,IAAI,KAAK;AAAA,IAAK,IAAI,KAAK;AAAA,EACzB;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,eAAe,KAAK,UAAU;AAAA,EAChC;AACF;AAKO,SAAS,YACd,IACA,OAA2C,CAAC,GAC1B;AAClB,QAAM,EAAE,QAAQ,IAAI,MAAM,IAAI;AAC9B,QAAM,cAAc,QAChB,+BAA+B,KAAK,kCAAkC,KAAK,OAC3E;AAGJ,QAAM,OAAO,GAAG;AAAA,IAKd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAYI,GAAG,kBAAkB,SAAS,OAAO,CAAC;AAAA,QACtC,WAAW;AAAA;AAAA;AAAA;AAAA,IAIf;AAAA,EACF;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,KAAK,KAAK,CAAC,GAAG,UAAU,EAAE,KAAK,CAAC,EAC9D,IAAI,CAAC,OAAO;AAAA,IACX,OAAO,EAAE;AAAA,IACT,OAAO,EAAE;AAAA,IACT,eAAe,EAAE;AAAA,EACnB,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/dataflow.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { findFirstSymbolMatch } from '../query-support.js';\nimport type { DataflowResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Reference-level dataflow analysis: where does data around this symbol\n * come from and where does it go?\n *\n * This is not value-level dataflow (we can't trace x = foo(); bar(x);\n * as a chain). Instead it shows:\n * - Where the symbol is defined and used\n * - What other symbols appear in the same enclosing scope (co-occurring data)\n * - What feeds into the function that defines it (producers)\n * - What consumes the function that uses it (consumers)\n *\n * Language-agnostic: works with any SCIP index.\n */\nexport function dataflow(\n db: ScipDatabase,\n symbolPattern: string,\n): DataflowResult | null {\n const match = findFirstSymbolMatch(db, symbolPattern);\n if (!match) return null;\n\n // Definition sites (role=1)\n const defSites = db.all<{ file: string; line: number }>(\n `SELECT d.relative_path AS file, c.start_line AS line\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n WHERE m.symbol_id = ? AND m.role = 1\n ORDER BY d.relative_path, c.start_line`,\n match.symbolId,\n );\n\n // Usage sites (role=0) with enclosing symbol\n const usageSites = db.all<{\n file: string;\n line: number;\n enclosing_symbol: string | null;\n }>(\n `SELECT d.relative_path AS file, c.start_line AS line,\n (SELECT enc_gs.symbol\n FROM defn_enclosing_ranges enc_der\n JOIN global_symbols enc_gs ON enc_der.symbol_id = enc_gs.id\n WHERE enc_der.document_id = d.id\n AND enc_der.start_line <= c.start_line\n AND enc_der.end_line >= c.end_line\n ORDER BY (enc_der.end_line - enc_der.start_line) ASC\n LIMIT 1\n ) AS enclosing_symbol\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents d ON c.document_id = d.id\n WHERE m.symbol_id = ? AND m.role = 0\n ${db.pathExclusionsFor('d')}\n ORDER BY d.relative_path, c.start_line`,\n match.symbolId,\n );\n\n // Producers: other symbols referenced within the same function that defines our target\n const producers = db.all<{ symbol: string; file: string }>(\n `SELECT DISTINCT other_gs.symbol, other_d.relative_path AS file\n FROM mentions other_m\n JOIN chunks other_c ON other_m.chunk_id = other_c.id\n JOIN global_symbols other_gs ON other_m.symbol_id = other_gs.id\n JOIN defn_enclosing_ranges other_der ON other_gs.id = other_der.symbol_id\n JOIN documents other_d ON other_der.document_id = other_d.id\n WHERE other_c.document_id = ?\n AND other_c.start_line >= ? AND other_c.end_line <= ?\n AND other_m.role = 0\n AND other_gs.id != ?\n ${db.symbolNoiseFor('other_gs')}\n ${db.pathExclusionsFor('other_d')}\n ORDER BY other_d.relative_path\n LIMIT 30`,\n match.documentId, match.startLine, match.endLine, match.symbolId,\n );\n\n // Consumers: symbols exported/defined by functions that reference our target\n // (what does the data flow into after being used)\n const consumers = db.all<{ symbol: string; file: string }>(\n `SELECT DISTINCT consumer_gs.symbol, consumer_d.relative_path AS file\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n JOIN documents ref_d ON ref_c.document_id = ref_d.id\n -- Find the enclosing function at each usage site\n JOIN defn_enclosing_ranges enc_der\n ON enc_der.document_id = ref_d.id\n AND enc_der.start_line <= ref_c.start_line\n AND enc_der.end_line >= ref_c.end_line\n JOIN global_symbols enc_gs ON enc_der.symbol_id = enc_gs.id\n -- Find other symbols defined by that enclosing function's file\n JOIN mentions consumer_m ON consumer_m.symbol_id = enc_gs.id AND consumer_m.role = 0\n JOIN chunks consumer_c ON consumer_m.chunk_id = consumer_c.id\n JOIN documents consumer_d ON consumer_c.document_id = consumer_d.id\n JOIN global_symbols consumer_gs ON consumer_m.symbol_id = consumer_gs.id\n WHERE ref_m.symbol_id = ? AND ref_m.role = 0\n AND consumer_d.id != ref_d.id\n ${db.symbolNoiseFor('consumer_gs')}\n ${db.pathExclusionsFor('consumer_d')}\n ORDER BY consumer_d.relative_path\n LIMIT 30`,\n match.symbolId,\n );\n\n return {\n symbol: match.symbol,\n shortName: shortenSymbol(match.symbol),\n relativePath: match.relativePath,\n definitionSites: defSites.filter((s) => !db.isIgnored(s.file)),\n usageSites: usageSites\n .filter((s) => !db.isIgnored(s.file))\n .map((s) => ({\n file: s.file,\n line: s.line,\n enclosingSymbol: s.enclosing_symbol ?? '(top-level)',\n enclosingShort: s.enclosing_symbol ? shortenSymbol(s.enclosing_symbol) : '(top-level)',\n })),\n producers: producers\n .filter((p) => !db.isIgnored(p.file))\n .map((p) => ({ symbol: p.symbol, shortName: shortenSymbol(p.symbol), file: p.file })),\n consumers: consumers\n .filter((c) => !db.isIgnored(c.file))\n .map((c) => ({ symbol: c.symbol, shortName: shortenSymbol(c.symbol), file: c.file })),\n };\n}\n"],"mappings":";;;;;;;;AAkBO,SAAS,SACd,IACA,eACuB;AACvB,QAAM,QAAQ,qBAAqB,IAAI,aAAa;AACpD,MAAI,CAAC,MAAO,QAAO;AAGnB,QAAM,WAAW,GAAG;AAAA,IAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM;AAAA,EACR;AAGA,QAAM,aAAa,GAAG;AAAA,IAKpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAcI,GAAG,kBAAkB,GAAG,CAAC;AAAA;AAAA,IAE7B,MAAM;AAAA,EACR;AAGA,QAAM,YAAY,GAAG;AAAA,IACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAUI,GAAG,eAAe,UAAU,CAAC;AAAA,QAC7B,GAAG,kBAAkB,SAAS,CAAC;AAAA;AAAA;AAAA,IAGnC,MAAM;AAAA,IAAY,MAAM;AAAA,IAAW,MAAM;AAAA,IAAS,MAAM;AAAA,EAC1D;AAIA,QAAM,YAAY,GAAG;AAAA,IACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAiBI,GAAG,eAAe,aAAa,CAAC;AAAA,QAChC,GAAG,kBAAkB,YAAY,CAAC;AAAA;AAAA;AAAA,IAGtC,MAAM;AAAA,EACR;AAEA,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,WAAW,cAAc,MAAM,MAAM;AAAA,IACrC,cAAc,MAAM;AAAA,IACpB,iBAAiB,SAAS,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC;AAAA,IAC7D,YAAY,WACT,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,EACnC,IAAI,CAAC,OAAO;AAAA,MACX,MAAM,EAAE;AAAA,MACR,MAAM,EAAE;AAAA,MACR,iBAAiB,EAAE,oBAAoB;AAAA,MACvC,gBAAgB,EAAE,mBAAmB,cAAc,EAAE,gBAAgB,IAAI;AAAA,IAC3E,EAAE;AAAA,IACJ,WAAW,UACR,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,EACnC,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,WAAW,cAAc,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE;AAAA,IACtF,WAAW,UACR,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,EACnC,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,WAAW,cAAc,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE;AAAA,EACxF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/wrapper-candidates.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { testFileExclusionSql } from '../query-support.js';\nimport type { WrapperCandidate } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Find wrapper candidates: symbols called by only one other symbol.\n *\n * These are premature abstractions that add indirection without\n * providing reuse. A function with fan-in = 1 whose sole caller\n * is widely used is a strong signal of unnecessary wrapping.\n */\nexport function wrapperCandidates(\n db: ScipDatabase,\n opts?: { scope?: string; maxLoc?: number; limit?: number },\n): WrapperCandidate[] {\n const { scope, maxLoc = 15, limit = 30 } = opts ?? {};\n const scopeFilter = scope ? `AND d.relative_path LIKE '%${scope}%'` : '';\n\n // Find all symbols with exactly 1 cross-file consumer (fan-in = 1),\n // along with who that single caller is and the caller's own fan-in.\n const rows = db.all<{\n symbol: string;\n file: string;\n start_line: number;\n end_line: number;\n loc: number;\n caller_symbol: string;\n caller_fan_in: number;\n }>(\n `SELECT * FROM (\n SELECT\n gs.symbol,\n d.relative_path AS file,\n der.start_line,\n der.end_line,\n (der.end_line - der.start_line + 1) AS loc,\n -- The single caller: the symbol whose definition range contains\n -- the chunk that references our target\n (SELECT caller_gs.symbol\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n JOIN defn_enclosing_ranges caller_der\n ON caller_der.document_id = ref_c.document_id\n AND ref_c.start_line >= caller_der.start_line\n AND ref_c.end_line <= caller_der.end_line\n JOIN global_symbols caller_gs ON caller_der.symbol_id = caller_gs.id\n WHERE ref_m.symbol_id = gs.id\n AND ref_m.role = 0\n AND ref_c.document_id != der.document_id\n LIMIT 1\n ) AS caller_symbol,\n -- Fan-in of that single caller\n (SELECT COUNT(DISTINCT caller_ref_c.document_id)\n FROM mentions caller_ref_m\n JOIN chunks caller_ref_c ON caller_ref_m.chunk_id = caller_ref_c.id\n WHERE caller_ref_m.symbol_id = (\n SELECT caller_der2.symbol_id\n FROM mentions ref_m2\n JOIN chunks ref_c2 ON ref_m2.chunk_id = ref_c2.id\n JOIN defn_enclosing_ranges caller_der2\n ON caller_der2.document_id = ref_c2.document_id\n AND ref_c2.start_line >= caller_der2.start_line\n AND ref_c2.end_line <= caller_der2.end_line\n WHERE ref_m2.symbol_id = gs.id\n AND ref_m2.role = 0\n AND ref_c2.document_id != der.document_id\n LIMIT 1\n )\n AND caller_ref_m.role = 0\n ) AS caller_fan_in\n FROM global_symbols gs\n JOIN defn_enclosing_ranges der ON gs.id = der.symbol_id\n JOIN documents d ON der.document_id = d.id\n WHERE 1 = 1\n ${db.pathExclusionsFor('d')}\n AND ${testFileExclusionSql('d')}\n ${db.symbolNoiseFor('gs')}\n -- Only functions/terms, not type definitions (types with # are not wrappers)\n AND gs.symbol NOT LIKE '%#'\n AND (der.end_line - der.start_line + 1) <= ?\n AND (der.end_line - der.start_line + 1) >= 2\n ${scopeFilter}\n -- Exactly 1 cross-file consumer\n AND (\n SELECT COUNT(DISTINCT ref_c.document_id)\n FROM mentions ref_m\n JOIN chunks ref_c ON ref_m.chunk_id = ref_c.id\n WHERE ref_m.symbol_id = gs.id\n AND ref_m.role = 0\n AND ref_c.document_id != der.document_id\n ) = 1\n ) WHERE caller_symbol IS NOT NULL AND caller_fan_in > 3\n ORDER BY caller_fan_in DESC, loc DESC\n LIMIT ?`,\n maxLoc, limit,\n );\n\n return rows\n .filter((r) => !db.isIgnored(r.file))\n .map((r) => ({\n symbol: r.symbol,\n shortName: shortenSymbol(r.symbol),\n file: r.file,\n startLine: r.start_line,\n endLine: r.end_line,\n loc: r.loc,\n singleCaller: r.caller_symbol,\n singleCallerShort: shortenSymbol(r.caller_symbol),\n callerFanIn: r.caller_fan_in,\n }));\n}\n"],"mappings":";;;;;;;;AAYO,SAAS,kBACd,IACA,MACoB;AACpB,QAAM,EAAE,OAAO,SAAS,IAAI,QAAQ,GAAG,IAAI,QAAQ,CAAC;AACpD,QAAM,cAAc,QAAQ,8BAA8B,KAAK,OAAO;AAItE,QAAM,OAAO,GAAG;AAAA,IASd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UA6CM,GAAG,kBAAkB,GAAG,CAAC;AAAA,cACrB,qBAAqB,GAAG,CAAC;AAAA,UAC7B,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,UAKvB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAajB;AAAA,IAAQ;AAAA,EACV;AAEA,SAAO,KACJ,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,EACnC,IAAI,CAAC,OAAO;AAAA,IACX,QAAQ,EAAE;AAAA,IACV,WAAW,cAAc,EAAE,MAAM;AAAA,IACjC,MAAM,EAAE;AAAA,IACR,WAAW,EAAE;AAAA,IACb,SAAS,EAAE;AAAA,IACX,KAAK,EAAE;AAAA,IACP,cAAc,EAAE;AAAA,IAChB,mBAAmB,cAAc,EAAE,aAAa;AAAA,IAChD,aAAa,EAAE;AAAA,EACjB,EAAE;AACN;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/change-surface.ts"],"sourcesContent":["import type { ScipDatabase } from '../db.js';\nimport { TEST_FILE_PATTERNS, testFileMatchSql } from '../query-support.js';\nimport type { ChangeSurfaceEntry, ChangeSurfaceResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Pre-change briefing for a file. For each symbol defined in the file,\n * reports external consumer count, test coverage, and risk level.\n */\nexport function changeSurface(\n db: ScipDatabase,\n filePattern: string,\n): ChangeSurfaceResult | null {\n // Find the file\n const doc = db.get<{ id: number; relative_path: string }>(\n `SELECT id, relative_path FROM documents\n WHERE relative_path LIKE ?\n ${db.pathExclusionsFor('documents')}\n LIMIT 1`,\n `%${filePattern}%`,\n );\n\n if (!doc || db.isIgnored(doc.relative_path)) return null;\n\n // Get all symbols defined in this file, excluding typeLiterals\n const syms = db.all<{\n symbol_id: number;\n symbol: string;\n start_line: number;\n end_line: number;\n }>(\n `SELECT DISTINCT gs.id AS symbol_id, gs.symbol, der.start_line, der.end_line\n FROM defn_enclosing_ranges der\n JOIN global_symbols gs ON der.symbol_id = gs.id\n WHERE der.document_id = ?\n ${db.symbolNoiseFor('gs')}\n ORDER BY der.start_line`,\n doc.id,\n );\n\n const testPatternSql = testFileMatchSql('ref_d', TEST_FILE_PATTERNS);\n\n const symbols: ChangeSurfaceEntry[] = [];\n let totalExternalConsumers = 0;\n let coveredCount = 0;\n\n for (const sym of syms) {\n // Count external consumers: mentions with role=0 from different documents\n const consumerRow = db.get<{ consumer_count: number }>(\n `SELECT COUNT(DISTINCT c.document_id) AS consumer_count\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n WHERE m.symbol_id = ?\n AND m.role = 0\n AND c.document_id != ?`,\n sym.symbol_id,\n doc.id,\n );\n\n const externalConsumers = consumerRow?.consumer_count ?? 0;\n\n // Find test files that reference this symbol\n const testFiles = db.all<{ relative_path: string }>(\n `SELECT DISTINCT ref_d.relative_path\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents ref_d ON c.document_id = ref_d.id\n WHERE m.symbol_id = ?\n AND m.role = 0\n AND (${testPatternSql})\n ORDER BY ref_d.relative_path`,\n sym.symbol_id,\n ).map((r) => r.relative_path);\n\n const hasTests = testFiles.length > 0;\n if (hasTests) coveredCount++;\n\n // Risk level determination\n let riskLevel: 'low' | 'medium' | 'high';\n if (externalConsumers > 10 && !hasTests) {\n riskLevel = 'high';\n } else if (externalConsumers > 5 || !hasTests) {\n riskLevel = 'medium';\n } else {\n riskLevel = 'low';\n }\n\n totalExternalConsumers += externalConsumers;\n\n symbols.push({\n symbol: sym.symbol,\n shortName: shortenSymbol(sym.symbol),\n startLine: sym.start_line,\n endLine: sym.end_line,\n externalConsumers,\n testFiles,\n riskLevel,\n });\n }\n\n const testCoveragePercent =\n symbols.length > 0 ? Math.round((coveredCount / symbols.length) * 100) : 0;\n\n return {\n file: doc.relative_path,\n symbols,\n totalExternalConsumers,\n testCoveragePercent,\n };\n}\n"],"mappings":";;;;;;;;;AASO,SAAS,cACd,IACA,aAC4B;AAE5B,QAAM,MAAM,GAAG;AAAA,IACb;AAAA;AAAA,SAEK,GAAG,kBAAkB,WAAW,CAAC;AAAA;AAAA,IAEtC,IAAI,WAAW;AAAA,EACjB;AAEA,MAAI,CAAC,OAAO,GAAG,UAAU,IAAI,aAAa,EAAG,QAAO;AAGpD,QAAM,OAAO,GAAG;AAAA,IAMd;AAAA;AAAA;AAAA;AAAA,QAII,GAAG,eAAe,IAAI,CAAC;AAAA;AAAA,IAE3B,IAAI;AAAA,EACN;AAEA,QAAM,iBAAiB,iBAAiB,SAAS,kBAAkB;AAEnE,QAAM,UAAgC,CAAC;AACvC,MAAI,yBAAyB;AAC7B,MAAI,eAAe;AAEnB,aAAW,OAAO,MAAM;AAEtB,UAAM,cAAc,GAAG;AAAA,MACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMA,IAAI;AAAA,MACJ,IAAI;AAAA,IACN;AAEA,UAAM,oBAAoB,aAAa,kBAAkB;AAGzD,UAAM,YAAY,GAAG;AAAA,MACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,eAMS,cAAc;AAAA;AAAA,MAEvB,IAAI;AAAA,IACN,EAAE,IAAI,CAAC,MAAM,EAAE,aAAa;AAE5B,UAAM,WAAW,UAAU,SAAS;AACpC,QAAI,SAAU;AAGd,QAAI;AACJ,QAAI,oBAAoB,MAAM,CAAC,UAAU;AACvC,kBAAY;AAAA,IACd,WAAW,oBAAoB,KAAK,CAAC,UAAU;AAC7C,kBAAY;AAAA,IACd,OAAO;AACL,kBAAY;AAAA,IACd;AAEA,8BAA0B;AAE1B,YAAQ,KAAK;AAAA,MACX,QAAQ,IAAI;AAAA,MACZ,WAAW,cAAc,IAAI,MAAM;AAAA,MACnC,WAAW,IAAI;AAAA,MACf,SAAS,IAAI;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,sBACJ,QAAQ,SAAS,IAAI,KAAK,MAAO,eAAe,QAAQ,SAAU,GAAG,IAAI;AAE3E,SAAO;AAAA,IACL,MAAM,IAAI;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queries/diff-impact.ts"],"sourcesContent":["import { execFileSync } from 'node:child_process';\nimport type { ScipDatabase } from '../db.js';\nimport { TEST_FILE_PATTERNS, testFileMatchSql } from '../query-support.js';\nimport type { DiffImpactResult } from '../types.js';\nimport { shortenSymbol } from '../symbol-parser.js';\n\n/**\n * Given a git diff, compute the affected symbol set.\n * Finds all symbols defined in changed files, their fan-in,\n * the files that consume them, and test coverage gaps.\n */\nexport function diffImpact(\n db: ScipDatabase,\n opts: { base?: string } = {},\n): DiffImpactResult {\n const { base = 'HEAD' } = opts;\n\n // Get changed files from git\n let changedFileLines: string[];\n try {\n const stdout = execFileSync('git', ['diff', '--name-only', base], {\n encoding: 'utf-8',\n cwd: db.config.projectRoot,\n timeout: 10_000,\n });\n changedFileLines = stdout\n .split('\\n')\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n } catch {\n // Not in a git repo or git not available — return empty result\n return {\n changedFiles: [],\n changedSymbols: [],\n affectedConsumers: [],\n uncoveredSymbols: [],\n summary: {\n totalChangedFiles: 0,\n totalChangedSymbols: 0,\n totalAffectedFiles: 0,\n testCoveragePercent: 0,\n },\n };\n }\n\n if (changedFileLines.length === 0) {\n return {\n changedFiles: [],\n changedSymbols: [],\n affectedConsumers: [],\n uncoveredSymbols: [],\n summary: {\n totalChangedFiles: 0,\n totalChangedSymbols: 0,\n totalAffectedFiles: 0,\n testCoveragePercent: 0,\n },\n };\n }\n\n // Match changed files against the index\n const changedFiles: string[] = [];\n const changedDocIds: number[] = [];\n\n for (const file of changedFileLines) {\n const doc = db.get<{ id: number; relative_path: string }>(\n `SELECT id, relative_path FROM documents\n WHERE relative_path LIKE ?\n LIMIT 1`,\n `%${file}`,\n );\n if (doc && !db.isIgnored(doc.relative_path)) {\n changedFiles.push(doc.relative_path);\n changedDocIds.push(doc.id);\n }\n }\n\n if (changedDocIds.length === 0) {\n return {\n changedFiles: changedFileLines,\n changedSymbols: [],\n affectedConsumers: [],\n uncoveredSymbols: [],\n summary: {\n totalChangedFiles: changedFileLines.length,\n totalChangedSymbols: 0,\n totalAffectedFiles: 0,\n testCoveragePercent: 0,\n },\n };\n }\n\n // Get all symbols defined in changed files\n const docPlaceholders = changedDocIds.map(() => '?').join(',');\n const syms = db.all<{\n symbol_id: number;\n symbol: string;\n relative_path: string;\n }>(\n `SELECT DISTINCT gs.id AS symbol_id, gs.symbol, d.relative_path\n FROM defn_enclosing_ranges der\n JOIN global_symbols gs ON der.symbol_id = gs.id\n JOIN documents d ON der.document_id = d.id\n WHERE der.document_id IN (${docPlaceholders})\n ${db.symbolNoiseFor('gs')}\n ORDER BY d.relative_path`,\n ...changedDocIds,\n );\n\n // For each symbol, compute fan-in (distinct referencing documents)\n const testPatternSql = testFileMatchSql('ref_d', TEST_FILE_PATTERNS);\n const changedSymbols: DiffImpactResult['changedSymbols'] = [];\n const consumerMap = new Map<string, Set<string>>(); // file -> set of consumed symbol shortNames\n const uncoveredSymbols: DiffImpactResult['uncoveredSymbols'] = [];\n let coveredCount = 0;\n\n for (const sym of syms) {\n // Fan-in: distinct files that reference this symbol\n const fanInRow = db.get<{ fan_in: number }>(\n `SELECT COUNT(DISTINCT c.document_id) AS fan_in\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n WHERE m.symbol_id = ?\n AND m.role = 0`,\n sym.symbol_id,\n );\n\n const fanIn = fanInRow?.fan_in ?? 0;\n const shortName = shortenSymbol(sym.symbol);\n\n changedSymbols.push({\n symbol: sym.symbol,\n shortName,\n file: sym.relative_path,\n fanIn,\n });\n\n // Collect consumer files (excluding the changed files themselves)\n const consumers = db.all<{ relative_path: string }>(\n `SELECT DISTINCT ref_d.relative_path\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents ref_d ON c.document_id = ref_d.id\n WHERE m.symbol_id = ?\n AND m.role = 0\n AND ref_d.relative_path NOT IN (${changedFiles.map(() => '?').join(',')})\n ${db.pathExclusionsFor('ref_d')}`,\n sym.symbol_id,\n ...changedFiles,\n );\n\n for (const consumer of consumers) {\n if (db.isIgnored(consumer.relative_path)) continue;\n if (!consumerMap.has(consumer.relative_path)) {\n consumerMap.set(consumer.relative_path, new Set());\n }\n consumerMap.get(consumer.relative_path)!.add(shortName);\n }\n\n // Check test coverage\n const hasTest = db.get<{ c: number }>(\n `SELECT COUNT(*) AS c\n FROM mentions m\n JOIN chunks c ON m.chunk_id = c.id\n JOIN documents ref_d ON c.document_id = ref_d.id\n WHERE m.symbol_id = ?\n AND m.role = 0\n AND (${testPatternSql})`,\n sym.symbol_id,\n );\n\n if (hasTest && hasTest.c > 0) {\n coveredCount++;\n } else {\n uncoveredSymbols.push({\n symbol: sym.symbol,\n shortName,\n file: sym.relative_path,\n });\n }\n }\n\n // Build affected consumers list\n const affectedConsumers = [...consumerMap.entries()]\n .map(([file, symbols]) => ({ file, consumedSymbols: symbols.size }))\n .sort((a, b) => b.consumedSymbols - a.consumedSymbols);\n\n const totalSymbols = changedSymbols.length;\n const testCoveragePercent =\n totalSymbols > 0 ? Math.round((coveredCount / totalSymbols) * 100) : 0;\n\n return {\n changedFiles,\n changedSymbols,\n affectedConsumers,\n uncoveredSymbols,\n summary: {\n totalChangedFiles: changedFiles.length,\n totalChangedSymbols: totalSymbols,\n totalAffectedFiles: affectedConsumers.length,\n testCoveragePercent,\n },\n };\n}\n"],"mappings":"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","names":[]}