scip-query 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/IMPROVEMENTS.md +143 -0
- package/PLAN.md +320 -0
- package/README.md +1213 -0
- package/dist/chunk-2QZ23IBN.js +55 -0
- package/dist/chunk-2QZ23IBN.js.map +1 -0
- package/dist/chunk-36OMT7ZJ.js +144 -0
- package/dist/chunk-36OMT7ZJ.js.map +1 -0
- package/dist/chunk-3E2X7RIE.js +101 -0
- package/dist/chunk-3E2X7RIE.js.map +1 -0
- package/dist/chunk-3UOUTZQT.js +45 -0
- package/dist/chunk-3UOUTZQT.js.map +1 -0
- package/dist/chunk-3ZZJVBIO.js +88 -0
- package/dist/chunk-3ZZJVBIO.js.map +1 -0
- package/dist/chunk-4TYLS5XX.js +10 -0
- package/dist/chunk-4TYLS5XX.js.map +1 -0
- package/dist/chunk-5FGUEU7N.js +101 -0
- package/dist/chunk-5FGUEU7N.js.map +1 -0
- package/dist/chunk-5WTJAXY2.js +61 -0
- package/dist/chunk-5WTJAXY2.js.map +1 -0
- package/dist/chunk-6NBLIDF4.js +24 -0
- package/dist/chunk-6NBLIDF4.js.map +1 -0
- package/dist/chunk-6SXADWLW.js +43 -0
- package/dist/chunk-6SXADWLW.js.map +1 -0
- package/dist/chunk-6VJ6Q7IE.js +65 -0
- package/dist/chunk-6VJ6Q7IE.js.map +1 -0
- package/dist/chunk-7OZPA5OO.js +258 -0
- package/dist/chunk-7OZPA5OO.js.map +1 -0
- package/dist/chunk-BEPIEVLR.js +76 -0
- package/dist/chunk-BEPIEVLR.js.map +1 -0
- package/dist/chunk-BFSCMC22.js +42 -0
- package/dist/chunk-BFSCMC22.js.map +1 -0
- package/dist/chunk-BP2ATLK2.js +110 -0
- package/dist/chunk-BP2ATLK2.js.map +1 -0
- package/dist/chunk-CM454WL3.js +114 -0
- package/dist/chunk-CM454WL3.js.map +1 -0
- package/dist/chunk-DCKMSTJ4.js +74 -0
- package/dist/chunk-DCKMSTJ4.js.map +1 -0
- package/dist/chunk-DEZKCZXD.js +40 -0
- package/dist/chunk-DEZKCZXD.js.map +1 -0
- package/dist/chunk-DVWGWHFW.js +99 -0
- package/dist/chunk-DVWGWHFW.js.map +1 -0
- package/dist/chunk-EMDQWNYR.js +102 -0
- package/dist/chunk-EMDQWNYR.js.map +1 -0
- package/dist/chunk-FFSWWE5O.js +33 -0
- package/dist/chunk-FFSWWE5O.js.map +1 -0
- package/dist/chunk-FGXRVW7G.js +73 -0
- package/dist/chunk-FGXRVW7G.js.map +1 -0
- package/dist/chunk-FUHJCHS4.js +158 -0
- package/dist/chunk-FUHJCHS4.js.map +1 -0
- package/dist/chunk-GJFURBEW.js +64 -0
- package/dist/chunk-GJFURBEW.js.map +1 -0
- package/dist/chunk-GTILYBH6.js +102 -0
- package/dist/chunk-GTILYBH6.js.map +1 -0
- package/dist/chunk-JJP7KQND.js +1 -0
- package/dist/chunk-JJP7KQND.js.map +1 -0
- package/dist/chunk-JKP5GH6T.js +213 -0
- package/dist/chunk-JKP5GH6T.js.map +1 -0
- package/dist/chunk-KCBMVQL5.js +38 -0
- package/dist/chunk-KCBMVQL5.js.map +1 -0
- package/dist/chunk-KVSW5KYP.js +78 -0
- package/dist/chunk-KVSW5KYP.js.map +1 -0
- package/dist/chunk-LAWMH22O.js +172 -0
- package/dist/chunk-LAWMH22O.js.map +1 -0
- package/dist/chunk-LB7OS35Q.js +72 -0
- package/dist/chunk-LB7OS35Q.js.map +1 -0
- package/dist/chunk-LUSIFBXO.js +57 -0
- package/dist/chunk-LUSIFBXO.js.map +1 -0
- package/dist/chunk-MBVNHJVN.js +44 -0
- package/dist/chunk-MBVNHJVN.js.map +1 -0
- package/dist/chunk-MGNMHKX3.js +15 -0
- package/dist/chunk-MGNMHKX3.js.map +1 -0
- package/dist/chunk-N5KEREIA.js +41 -0
- package/dist/chunk-N5KEREIA.js.map +1 -0
- package/dist/chunk-NDSQYIWT.js +71 -0
- package/dist/chunk-NDSQYIWT.js.map +1 -0
- package/dist/chunk-NUZ4OMU3.js +28 -0
- package/dist/chunk-NUZ4OMU3.js.map +1 -0
- package/dist/chunk-QOV2R2WT.js +170 -0
- package/dist/chunk-QOV2R2WT.js.map +1 -0
- package/dist/chunk-SEFSL2GF.js +78 -0
- package/dist/chunk-SEFSL2GF.js.map +1 -0
- package/dist/chunk-T6ARFSBZ.js +103 -0
- package/dist/chunk-T6ARFSBZ.js.map +1 -0
- package/dist/chunk-TBP6BICL.js +46 -0
- package/dist/chunk-TBP6BICL.js.map +1 -0
- package/dist/chunk-TDNNOR6D.js +97 -0
- package/dist/chunk-TDNNOR6D.js.map +1 -0
- package/dist/chunk-TSPZOMHC.js +195 -0
- package/dist/chunk-TSPZOMHC.js.map +1 -0
- package/dist/chunk-UNTPVD36.js +55 -0
- package/dist/chunk-UNTPVD36.js.map +1 -0
- package/dist/chunk-VRUJH4BO.js +88 -0
- package/dist/chunk-VRUJH4BO.js.map +1 -0
- package/dist/chunk-VZ7AMAFL.js +76 -0
- package/dist/chunk-VZ7AMAFL.js.map +1 -0
- package/dist/chunk-XFXDXEUN.js +24 -0
- package/dist/chunk-XFXDXEUN.js.map +1 -0
- package/dist/chunk-YZAA4LYG.js +169 -0
- package/dist/chunk-YZAA4LYG.js.map +1 -0
- package/dist/chunk-Z73NYSBZ.js +92 -0
- package/dist/chunk-Z73NYSBZ.js.map +1 -0
- package/dist/chunk-ZJRYBOEE.js +125 -0
- package/dist/chunk-ZJRYBOEE.js.map +1 -0
- package/dist/cli.js +5798 -0
- package/dist/cli.js.map +1 -0
- package/dist/db-BxaevAyc.d.ts +683 -0
- package/dist/index.d.ts +254 -0
- package/dist/index.js +1271 -0
- package/dist/index.js.map +1 -0
- package/dist/postinstall.js +167 -0
- package/dist/postinstall.js.map +1 -0
- package/dist/queries/affected.d.ts +14 -0
- package/dist/queries/affected.js +9 -0
- package/dist/queries/affected.js.map +1 -0
- package/dist/queries/bottlenecks.d.ts +18 -0
- package/dist/queries/bottlenecks.js +8 -0
- package/dist/queries/bottlenecks.js.map +1 -0
- package/dist/queries/by-kind.d.ts +20 -0
- package/dist/queries/by-kind.js +10 -0
- package/dist/queries/by-kind.js.map +1 -0
- package/dist/queries/call-graph.d.ts +13 -0
- package/dist/queries/call-graph.js +9 -0
- package/dist/queries/call-graph.js.map +1 -0
- package/dist/queries/change-surface.d.ts +10 -0
- package/dist/queries/change-surface.js +9 -0
- package/dist/queries/change-surface.js.map +1 -0
- package/dist/queries/clean-signature.d.ts +9 -0
- package/dist/queries/clean-signature.js +7 -0
- package/dist/queries/clean-signature.js.map +1 -0
- package/dist/queries/code.d.ts +17 -0
- package/dist/queries/code.js +9 -0
- package/dist/queries/code.js.map +1 -0
- package/dist/queries/complexity-hotspots.d.ts +19 -0
- package/dist/queries/complexity-hotspots.js +9 -0
- package/dist/queries/complexity-hotspots.js.map +1 -0
- package/dist/queries/complexity.d.ts +13 -0
- package/dist/queries/complexity.js +9 -0
- package/dist/queries/complexity.js.map +1 -0
- package/dist/queries/convergence.d.ts +11 -0
- package/dist/queries/convergence.js +9 -0
- package/dist/queries/convergence.js.map +1 -0
- package/dist/queries/coupling.d.ts +17 -0
- package/dist/queries/coupling.js +9 -0
- package/dist/queries/coupling.js.map +1 -0
- package/dist/queries/cycles.d.ts +16 -0
- package/dist/queries/cycles.js +8 -0
- package/dist/queries/cycles.js.map +1 -0
- package/dist/queries/dataflow.d.ts +19 -0
- package/dist/queries/dataflow.js +9 -0
- package/dist/queries/dataflow.js.map +1 -0
- package/dist/queries/dead.d.ts +10 -0
- package/dist/queries/dead.js +9 -0
- package/dist/queries/dead.js.map +1 -0
- package/dist/queries/deep-chains.d.ts +16 -0
- package/dist/queries/deep-chains.js +8 -0
- package/dist/queries/deep-chains.js.map +1 -0
- package/dist/queries/deps.d.ts +9 -0
- package/dist/queries/deps.js +9 -0
- package/dist/queries/deps.js.map +1 -0
- package/dist/queries/diff-impact.d.ts +13 -0
- package/dist/queries/diff-impact.js +9 -0
- package/dist/queries/diff-impact.js.map +1 -0
- package/dist/queries/doc-coverage.d.ts +14 -0
- package/dist/queries/doc-coverage.js +8 -0
- package/dist/queries/doc-coverage.js.map +1 -0
- package/dist/queries/drift.d.ts +25 -0
- package/dist/queries/drift.js +8 -0
- package/dist/queries/drift.js.map +1 -0
- package/dist/queries/extract-candidates.d.ts +25 -0
- package/dist/queries/extract-candidates.js +9 -0
- package/dist/queries/extract-candidates.js.map +1 -0
- package/dist/queries/fan.d.ts +29 -0
- package/dist/queries/fan.js +14 -0
- package/dist/queries/fan.js.map +1 -0
- package/dist/queries/files.d.ts +6 -0
- package/dist/queries/files.js +7 -0
- package/dist/queries/files.js.map +1 -0
- package/dist/queries/health.d.ts +18 -0
- package/dist/queries/health.js +21 -0
- package/dist/queries/health.js.map +1 -0
- package/dist/queries/hierarchy.d.ts +13 -0
- package/dist/queries/hierarchy.js +8 -0
- package/dist/queries/hierarchy.js.map +1 -0
- package/dist/queries/hotspots.d.ts +13 -0
- package/dist/queries/hotspots.js +8 -0
- package/dist/queries/hotspots.js.map +1 -0
- package/dist/queries/imports.d.ts +19 -0
- package/dist/queries/imports.js +12 -0
- package/dist/queries/imports.js.map +1 -0
- package/dist/queries/index.d.ts +47 -0
- package/dist/queries/index.js +207 -0
- package/dist/queries/index.js.map +1 -0
- package/dist/queries/isolated.d.ts +14 -0
- package/dist/queries/isolated.js +9 -0
- package/dist/queries/isolated.js.map +1 -0
- package/dist/queries/members.d.ts +10 -0
- package/dist/queries/members.js +8 -0
- package/dist/queries/members.js.map +1 -0
- package/dist/queries/methods.d.ts +6 -0
- package/dist/queries/methods.js +8 -0
- package/dist/queries/methods.js.map +1 -0
- package/dist/queries/outline.d.ts +10 -0
- package/dist/queries/outline.js +8 -0
- package/dist/queries/outline.js.map +1 -0
- package/dist/queries/passthrough-candidates.d.ts +18 -0
- package/dist/queries/passthrough-candidates.js +9 -0
- package/dist/queries/passthrough-candidates.js.map +1 -0
- package/dist/queries/redundant-reexports.d.ts +22 -0
- package/dist/queries/redundant-reexports.js +8 -0
- package/dist/queries/redundant-reexports.js.map +1 -0
- package/dist/queries/refs.d.ts +6 -0
- package/dist/queries/refs.js +7 -0
- package/dist/queries/refs.js.map +1 -0
- package/dist/queries/similar-chains.d.ts +29 -0
- package/dist/queries/similar-chains.js +8 -0
- package/dist/queries/similar-chains.js.map +1 -0
- package/dist/queries/similar-files.d.ts +19 -0
- package/dist/queries/similar-files.js +8 -0
- package/dist/queries/similar-files.js.map +1 -0
- package/dist/queries/similar-signatures.d.ts +21 -0
- package/dist/queries/similar-signatures.js +8 -0
- package/dist/queries/similar-signatures.js.map +1 -0
- package/dist/queries/similar.d.ts +34 -0
- package/dist/queries/similar.js +11 -0
- package/dist/queries/similar.js.map +1 -0
- package/dist/queries/slice.d.ts +21 -0
- package/dist/queries/slice.js +9 -0
- package/dist/queries/slice.js.map +1 -0
- package/dist/queries/stale-abstractions.d.ts +18 -0
- package/dist/queries/stale-abstractions.js +9 -0
- package/dist/queries/stale-abstractions.js.map +1 -0
- package/dist/queries/stats.d.ts +6 -0
- package/dist/queries/stats.js +7 -0
- package/dist/queries/stats.js.map +1 -0
- package/dist/queries/surface.d.ts +7 -0
- package/dist/queries/surface.js +8 -0
- package/dist/queries/surface.js.map +1 -0
- package/dist/queries/symbols.d.ts +6 -0
- package/dist/queries/symbols.js +9 -0
- package/dist/queries/symbols.js.map +1 -0
- package/dist/queries/system.d.ts +7 -0
- package/dist/queries/system.js +9 -0
- package/dist/queries/system.js.map +1 -0
- package/dist/queries/test-coverage.d.ts +22 -0
- package/dist/queries/test-coverage.js +11 -0
- package/dist/queries/test-coverage.js.map +1 -0
- package/dist/queries/trace.d.ts +6 -0
- package/dist/queries/trace.js +8 -0
- package/dist/queries/trace.js.map +1 -0
- package/dist/queries/wrapper-candidates.d.ts +17 -0
- package/dist/queries/wrapper-candidates.js +9 -0
- package/dist/queries/wrapper-candidates.js.map +1 -0
- package/dist/reindex-worker.js +368 -0
- package/dist/reindex-worker.js.map +1 -0
- package/docs/AGENT_GUIDE.md +359 -0
- package/package.json +70 -0
- package/reports/debloat/2026-04-10-scip-query-self-audit.md +161 -0
- package/skills/concrete-plan/SKILL.md +318 -0
- package/skills/scip-debloat/SKILL.md +413 -0
- package/skills/scip-explore/SKILL.md +235 -0
- package/skills/scip-verify/SKILL.md +323 -0
- package/src/cli.ts +1480 -0
- package/src/config.ts +117 -0
- package/src/db.ts +127 -0
- package/src/gitignore-filter.ts +143 -0
- package/src/index.ts +11 -0
- package/src/postinstall.ts +8 -0
- package/src/queries/affected.ts +86 -0
- package/src/queries/bottlenecks.ts +67 -0
- package/src/queries/by-kind.ts +204 -0
- package/src/queries/call-graph.ts +66 -0
- package/src/queries/change-surface.ts +110 -0
- package/src/queries/clean-signature.ts +22 -0
- package/src/queries/code.ts +101 -0
- package/src/queries/complexity-hotspots.ts +119 -0
- package/src/queries/complexity.ts +152 -0
- package/src/queries/convergence.ts +82 -0
- package/src/queries/coupling.ts +99 -0
- package/src/queries/cycles.ts +78 -0
- package/src/queries/dataflow.ts +128 -0
- package/src/queries/dead.ts +122 -0
- package/src/queries/deep-chains.ts +59 -0
- package/src/queries/deps.ts +46 -0
- package/src/queries/diff-impact.ts +204 -0
- package/src/queries/doc-coverage.ts +86 -0
- package/src/queries/drift.ts +224 -0
- package/src/queries/extract-candidates.ts +167 -0
- package/src/queries/fan.ts +148 -0
- package/src/queries/files.ts +16 -0
- package/src/queries/health.ts +324 -0
- package/src/queries/hierarchy.ts +49 -0
- package/src/queries/hotspots.ts +53 -0
- package/src/queries/imports.ts +95 -0
- package/src/queries/index.ts +45 -0
- package/src/queries/isolated.ts +67 -0
- package/src/queries/members.ts +54 -0
- package/src/queries/methods.ts +27 -0
- package/src/queries/outline.ts +52 -0
- package/src/queries/passthrough-candidates.ts +94 -0
- package/src/queries/redundant-reexports.ts +170 -0
- package/src/queries/refs.ts +27 -0
- package/src/queries/similar-chains.ts +314 -0
- package/src/queries/similar-files.ts +140 -0
- package/src/queries/similar-signatures.ts +151 -0
- package/src/queries/similar.ts +305 -0
- package/src/queries/slice.ts +154 -0
- package/src/queries/stale-abstractions.ts +82 -0
- package/src/queries/stats.ts +22 -0
- package/src/queries/surface.ts +34 -0
- package/src/queries/symbols.ts +39 -0
- package/src/queries/system.ts +86 -0
- package/src/queries/test-coverage.ts +106 -0
- package/src/queries/trace.ts +55 -0
- package/src/queries/wrapper-candidates.ts +112 -0
- package/src/query-support.ts +226 -0
- package/src/reindex/detect.ts +58 -0
- package/src/reindex/index.ts +153 -0
- package/src/reindex/indexers.ts +220 -0
- package/src/reindex/install.ts +125 -0
- package/src/reindex-worker.ts +35 -0
- package/src/setup.ts +202 -0
- package/src/symbol-parser.ts +278 -0
- package/src/types.ts +654 -0
- package/src/watch.ts +274 -0
- package/tests/gitignore-filter.test.ts +48 -0
- package/tests/queries.test.ts +300 -0
- package/tests/symbol-parser.test.ts +157 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +40 -0
- package/vitest.config.ts +7 -0
package/src/watch.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
2
|
+
import { readFileSync, existsSync, renameSync } from 'node:fs';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
import { fork } from 'node:child_process';
|
|
5
|
+
import ignore from 'ignore';
|
|
6
|
+
import type { WatcherStatus, ProjectConfig, SupportedLanguage } from './types.js';
|
|
7
|
+
import { resolveWatchConfig, resolveIndexPaths } from './config.js';
|
|
8
|
+
import { createGitignoreFilter } from './gitignore-filter.js';
|
|
9
|
+
|
|
10
|
+
export interface WatcherOptions {
|
|
11
|
+
projectRoot: string;
|
|
12
|
+
config: ProjectConfig;
|
|
13
|
+
languages?: SupportedLanguage[];
|
|
14
|
+
onStatus?: (status: WatcherStatus) => void;
|
|
15
|
+
onReindexComplete?: (durationMs: number) => void;
|
|
16
|
+
onError?: (error: Error) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* File watcher that triggers single-flight background reindexing.
|
|
21
|
+
*
|
|
22
|
+
* Design:
|
|
23
|
+
* - Debounce: waits 30s (configurable) after the last file change
|
|
24
|
+
* - Single-flight: only one reindex runs at a time, never queued
|
|
25
|
+
* - Dirty flag: changes during reindex schedule ONE follow-up
|
|
26
|
+
* - Cooldown: minimum interval between reindex completions
|
|
27
|
+
* - Atomic swap: writes to index.db.tmp, renames on success
|
|
28
|
+
*/
|
|
29
|
+
export class Watcher {
|
|
30
|
+
private projectRoot: string;
|
|
31
|
+
private watchConfig: Required<NonNullable<ProjectConfig['watch']>>;
|
|
32
|
+
private indexPaths: ReturnType<typeof resolveIndexPaths>;
|
|
33
|
+
private languages?: SupportedLanguage[];
|
|
34
|
+
private pnpmWorkspaces: boolean;
|
|
35
|
+
|
|
36
|
+
private onStatus: (status: WatcherStatus) => void;
|
|
37
|
+
private onReindexComplete: (durationMs: number) => void;
|
|
38
|
+
private onError: (error: Error) => void;
|
|
39
|
+
|
|
40
|
+
// State machine
|
|
41
|
+
private status: WatcherStatus = { state: 'idle' };
|
|
42
|
+
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
43
|
+
private cooldownTimer: ReturnType<typeof setTimeout> | null = null;
|
|
44
|
+
private dirty = false;
|
|
45
|
+
private changedFiles = 0;
|
|
46
|
+
private reindexInFlight = false;
|
|
47
|
+
private lastReindexEnd = 0;
|
|
48
|
+
|
|
49
|
+
// fs.watch watchers (one per watched directory)
|
|
50
|
+
private fsWatchers: ReturnType<typeof watch>[] = [];
|
|
51
|
+
private gitignoreFilter: ReturnType<typeof createGitignoreFilter>;
|
|
52
|
+
private extraIgnore: ReturnType<typeof ignore>;
|
|
53
|
+
private stopped = false;
|
|
54
|
+
|
|
55
|
+
constructor(opts: WatcherOptions) {
|
|
56
|
+
this.projectRoot = opts.projectRoot;
|
|
57
|
+
this.watchConfig = resolveWatchConfig(opts.config);
|
|
58
|
+
this.indexPaths = resolveIndexPaths(opts.projectRoot, opts.config);
|
|
59
|
+
this.languages = opts.languages;
|
|
60
|
+
this.pnpmWorkspaces = opts.config.indexer?.typescript?.pnpmWorkspaces ?? false;
|
|
61
|
+
|
|
62
|
+
this.onStatus = opts.onStatus ?? (() => {});
|
|
63
|
+
this.onReindexComplete = opts.onReindexComplete ?? (() => {});
|
|
64
|
+
this.onError = opts.onError ?? ((e) => console.error(e.message));
|
|
65
|
+
|
|
66
|
+
this.gitignoreFilter = createGitignoreFilter(opts.projectRoot);
|
|
67
|
+
this.extraIgnore = ignore();
|
|
68
|
+
if (this.watchConfig.ignore.length > 0) {
|
|
69
|
+
this.extraIgnore.add(this.watchConfig.ignore);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Start watching for file changes */
|
|
74
|
+
start(): void {
|
|
75
|
+
this.stopped = false;
|
|
76
|
+
this.setStatus({ state: 'idle' });
|
|
77
|
+
|
|
78
|
+
// Use recursive fs.watch on the project root
|
|
79
|
+
// This is supported on macOS (FSEvents) and Windows
|
|
80
|
+
// On Linux, falls back to inotify (may need per-directory watchers for large trees)
|
|
81
|
+
try {
|
|
82
|
+
const watcher = watch(
|
|
83
|
+
this.projectRoot,
|
|
84
|
+
{ recursive: true },
|
|
85
|
+
(_event, filename) => {
|
|
86
|
+
if (filename && !this.stopped) {
|
|
87
|
+
this.handleFileChange(filename);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
this.fsWatchers.push(watcher);
|
|
92
|
+
} catch {
|
|
93
|
+
this.onError(new Error(
|
|
94
|
+
'Failed to start file watcher. On Linux, you may need to increase inotify limits: ' +
|
|
95
|
+
'sysctl -w fs.inotify.max_user_watches=524288',
|
|
96
|
+
));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Stop watching and clean up */
|
|
101
|
+
stop(): void {
|
|
102
|
+
this.stopped = true;
|
|
103
|
+
for (const w of this.fsWatchers) w.close();
|
|
104
|
+
this.fsWatchers = [];
|
|
105
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
106
|
+
if (this.cooldownTimer) clearTimeout(this.cooldownTimer);
|
|
107
|
+
this.setStatus({ state: 'idle' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Get current watcher status */
|
|
111
|
+
getStatus(): WatcherStatus {
|
|
112
|
+
return this.status;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Internal ─────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
private handleFileChange(filename: string): void {
|
|
118
|
+
// Filter: skip gitignored files and extra ignore patterns
|
|
119
|
+
const rel = relative(this.projectRoot, join(this.projectRoot, filename));
|
|
120
|
+
if (this.gitignoreFilter.isIgnored(rel)) return;
|
|
121
|
+
if (this.extraIgnore.ignores(rel)) return;
|
|
122
|
+
|
|
123
|
+
// Skip the index files themselves
|
|
124
|
+
if (filename.endsWith('index.db') || filename.endsWith('index.scip') ||
|
|
125
|
+
filename.endsWith('index.db.tmp') || filename.endsWith('.scipquery.json')) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.changedFiles++;
|
|
130
|
+
|
|
131
|
+
if (this.reindexInFlight) {
|
|
132
|
+
// Reindex is running — just mark dirty, don't schedule anything
|
|
133
|
+
this.dirty = true;
|
|
134
|
+
this.setStatus({
|
|
135
|
+
state: 'indexing',
|
|
136
|
+
startedAt: (this.status as { startedAt: number }).startedAt,
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this.status.state === 'cooldown') {
|
|
142
|
+
// In cooldown — mark dirty, the cooldown handler will pick it up
|
|
143
|
+
this.dirty = true;
|
|
144
|
+
this.setStatus({ state: 'cooldown', until: (this.status as { until: number }).until, dirty: true });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Reset the debounce timer — every new change pushes the trigger out
|
|
149
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
150
|
+
|
|
151
|
+
const reindexAt = Date.now() + this.watchConfig.debounceMs;
|
|
152
|
+
this.setStatus({ state: 'waiting', changedFiles: this.changedFiles, reindexAt });
|
|
153
|
+
|
|
154
|
+
this.debounceTimer = setTimeout(() => {
|
|
155
|
+
this.debounceTimer = null;
|
|
156
|
+
this.triggerReindex();
|
|
157
|
+
}, this.watchConfig.debounceMs);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private triggerReindex(): void {
|
|
161
|
+
if (this.reindexInFlight || this.stopped) return;
|
|
162
|
+
|
|
163
|
+
// Check cooldown
|
|
164
|
+
const timeSinceLastReindex = Date.now() - this.lastReindexEnd;
|
|
165
|
+
if (this.lastReindexEnd > 0 && timeSinceLastReindex < this.watchConfig.cooldownMs) {
|
|
166
|
+
const remaining = this.watchConfig.cooldownMs - timeSinceLastReindex;
|
|
167
|
+
this.dirty = true;
|
|
168
|
+
const until = Date.now() + remaining;
|
|
169
|
+
this.setStatus({ state: 'cooldown', until, dirty: true });
|
|
170
|
+
|
|
171
|
+
this.cooldownTimer = setTimeout(() => {
|
|
172
|
+
this.cooldownTimer = null;
|
|
173
|
+
if (this.dirty && !this.stopped) {
|
|
174
|
+
this.dirty = false;
|
|
175
|
+
this.triggerReindex();
|
|
176
|
+
}
|
|
177
|
+
}, remaining);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.reindexInFlight = true;
|
|
182
|
+
this.dirty = false;
|
|
183
|
+
this.changedFiles = 0;
|
|
184
|
+
const startedAt = Date.now();
|
|
185
|
+
this.setStatus({ state: 'indexing', startedAt });
|
|
186
|
+
|
|
187
|
+
// Run reindex in a child process so it doesn't block the watcher
|
|
188
|
+
this.runReindex()
|
|
189
|
+
.then((durationMs) => {
|
|
190
|
+
this.reindexInFlight = false;
|
|
191
|
+
this.lastReindexEnd = Date.now();
|
|
192
|
+
this.onReindexComplete(durationMs);
|
|
193
|
+
|
|
194
|
+
if (this.dirty && !this.stopped) {
|
|
195
|
+
// Changes arrived during reindex — enter cooldown then reindex again
|
|
196
|
+
const until = Date.now() + this.watchConfig.cooldownMs;
|
|
197
|
+
this.setStatus({ state: 'cooldown', until, dirty: true });
|
|
198
|
+
|
|
199
|
+
this.cooldownTimer = setTimeout(() => {
|
|
200
|
+
this.cooldownTimer = null;
|
|
201
|
+
if (this.dirty && !this.stopped) {
|
|
202
|
+
this.dirty = false;
|
|
203
|
+
this.triggerReindex();
|
|
204
|
+
} else {
|
|
205
|
+
this.setStatus({ state: 'idle' });
|
|
206
|
+
}
|
|
207
|
+
}, this.watchConfig.cooldownMs);
|
|
208
|
+
} else {
|
|
209
|
+
this.setStatus({ state: 'idle' });
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
.catch((err) => {
|
|
213
|
+
this.reindexInFlight = false;
|
|
214
|
+
this.lastReindexEnd = Date.now();
|
|
215
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
216
|
+
this.setStatus({ state: 'idle' });
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Run the reindex in a forked child process.
|
|
222
|
+
* Writes to index.db.tmp, then atomically renames to index.db.
|
|
223
|
+
*/
|
|
224
|
+
private runReindex(): Promise<number> {
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
const start = Date.now();
|
|
227
|
+
const tmpDb = this.indexPaths.dbPath + '.tmp';
|
|
228
|
+
const tmpScip = this.indexPaths.indexPath + '.tmp';
|
|
229
|
+
|
|
230
|
+
// Fork a child that runs the reindex
|
|
231
|
+
const child = fork(
|
|
232
|
+
new URL('./reindex-worker.js', import.meta.url).pathname,
|
|
233
|
+
[],
|
|
234
|
+
{
|
|
235
|
+
env: {
|
|
236
|
+
...process.env,
|
|
237
|
+
SCIP_REINDEX_PROJECT_ROOT: this.projectRoot,
|
|
238
|
+
SCIP_REINDEX_OUTPUT_SCIP: tmpScip,
|
|
239
|
+
SCIP_REINDEX_OUTPUT_DB: tmpDb,
|
|
240
|
+
SCIP_REINDEX_LANGUAGES: this.languages?.join(',') ?? '',
|
|
241
|
+
SCIP_REINDEX_PNPM_WORKSPACES: this.pnpmWorkspaces ? '1' : '',
|
|
242
|
+
},
|
|
243
|
+
stdio: 'pipe',
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
child.on('exit', (code) => {
|
|
248
|
+
if (code === 0) {
|
|
249
|
+
// Atomic swap
|
|
250
|
+
try {
|
|
251
|
+
if (existsSync(tmpDb)) {
|
|
252
|
+
renameSync(tmpDb, this.indexPaths.dbPath);
|
|
253
|
+
}
|
|
254
|
+
if (existsSync(tmpScip)) {
|
|
255
|
+
renameSync(tmpScip, this.indexPaths.indexPath);
|
|
256
|
+
}
|
|
257
|
+
resolve(Date.now() - start);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
reject(new Error(`Atomic swap failed: ${err}`));
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
reject(new Error(`Reindex worker exited with code ${code}`));
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
child.on('error', reject);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private setStatus(status: WatcherStatus): void {
|
|
271
|
+
this.status = status;
|
|
272
|
+
this.onStatus(status);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createGitignoreFilter } from '../src/gitignore-filter.js';
|
|
3
|
+
import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
|
|
7
|
+
describe('createGitignoreFilter', () => {
|
|
8
|
+
it('respects .gitignore patterns', () => {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), 'scip-query-test-'));
|
|
10
|
+
writeFileSync(join(dir, '.gitignore'), 'node_modules/\ndist/\n*.pyc\n');
|
|
11
|
+
|
|
12
|
+
const filter = createGitignoreFilter(dir);
|
|
13
|
+
|
|
14
|
+
expect(filter.isIgnored('node_modules/foo/bar.js')).toBe(true);
|
|
15
|
+
expect(filter.isIgnored('dist/index.js')).toBe(true);
|
|
16
|
+
expect(filter.isIgnored('foo.pyc')).toBe(true);
|
|
17
|
+
expect(filter.isIgnored('src/app.ts')).toBe(false);
|
|
18
|
+
expect(filter.isIgnored('lib/utils.py')).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('uses defaults when no .gitignore exists', () => {
|
|
22
|
+
const dir = mkdtempSync(join(tmpdir(), 'scip-query-test-'));
|
|
23
|
+
|
|
24
|
+
const filter = createGitignoreFilter(dir);
|
|
25
|
+
|
|
26
|
+
// Defaults should exclude common dependency/build dirs
|
|
27
|
+
expect(filter.isIgnored('node_modules/foo.js')).toBe(true);
|
|
28
|
+
expect(filter.isIgnored('dist/bundle.js')).toBe(true);
|
|
29
|
+
expect(filter.isIgnored('target/debug/main')).toBe(true);
|
|
30
|
+
expect(filter.isIgnored('__pycache__/foo.pyc')).toBe(true);
|
|
31
|
+
expect(filter.isIgnored('.venv/lib/site-packages/foo.py')).toBe(true);
|
|
32
|
+
|
|
33
|
+
// Source files should NOT be excluded
|
|
34
|
+
expect(filter.isIgnored('src/main.rs')).toBe(false);
|
|
35
|
+
expect(filter.isIgnored('app/models/user.rb')).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('filters an array of paths', () => {
|
|
39
|
+
const dir = mkdtempSync(join(tmpdir(), 'scip-query-test-'));
|
|
40
|
+
writeFileSync(join(dir, '.gitignore'), 'node_modules/\n');
|
|
41
|
+
|
|
42
|
+
const filter = createGitignoreFilter(dir);
|
|
43
|
+
const paths = ['src/app.ts', 'node_modules/foo/index.js', 'lib/utils.ts'];
|
|
44
|
+
const filtered = filter.filter(paths);
|
|
45
|
+
|
|
46
|
+
expect(filtered).toEqual(['src/app.ts', 'lib/utils.ts']);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import { mkdtempSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { ScipDatabase } from '../src/db.js';
|
|
7
|
+
import * as queries from '../src/queries/index.js';
|
|
8
|
+
import type { ScipQueryConfig } from '../src/types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a minimal SCIP SQLite database with fixture data
|
|
12
|
+
* that exercises all query commands. This simulates what
|
|
13
|
+
* `scip expt-convert` produces, but with controlled test data.
|
|
14
|
+
*
|
|
15
|
+
* NOTE: db.exec() below is better-sqlite3's SQL execution method,
|
|
16
|
+
* NOT child_process.exec(). No shell commands are being run.
|
|
17
|
+
*/
|
|
18
|
+
function createFixtureDb(dbPath: string): void {
|
|
19
|
+
const sqliteDb = new Database(dbPath);
|
|
20
|
+
const run = (sql: string) => sqliteDb.exec(sql); // eslint-disable-line -- sqlite exec, not child_process
|
|
21
|
+
|
|
22
|
+
// Create the SCIP SQLite schema
|
|
23
|
+
run(`
|
|
24
|
+
CREATE TABLE documents (
|
|
25
|
+
id INTEGER PRIMARY KEY,
|
|
26
|
+
language TEXT,
|
|
27
|
+
relative_path TEXT NOT NULL UNIQUE,
|
|
28
|
+
position_encoding TEXT,
|
|
29
|
+
text TEXT
|
|
30
|
+
);
|
|
31
|
+
CREATE TABLE global_symbols (
|
|
32
|
+
id INTEGER PRIMARY KEY,
|
|
33
|
+
symbol TEXT NOT NULL UNIQUE,
|
|
34
|
+
display_name TEXT,
|
|
35
|
+
kind INTEGER,
|
|
36
|
+
documentation TEXT,
|
|
37
|
+
signature BLOB,
|
|
38
|
+
enclosing_symbol TEXT,
|
|
39
|
+
relationships BLOB
|
|
40
|
+
);
|
|
41
|
+
CREATE TABLE defn_enclosing_ranges (
|
|
42
|
+
id INTEGER PRIMARY KEY,
|
|
43
|
+
document_id INTEGER NOT NULL,
|
|
44
|
+
symbol_id INTEGER NOT NULL,
|
|
45
|
+
start_line INTEGER NOT NULL,
|
|
46
|
+
start_char INTEGER NOT NULL,
|
|
47
|
+
end_line INTEGER NOT NULL,
|
|
48
|
+
end_char INTEGER NOT NULL
|
|
49
|
+
);
|
|
50
|
+
CREATE TABLE mentions (
|
|
51
|
+
chunk_id INTEGER NOT NULL,
|
|
52
|
+
symbol_id INTEGER NOT NULL,
|
|
53
|
+
role INTEGER NOT NULL,
|
|
54
|
+
PRIMARY KEY (chunk_id, symbol_id, role)
|
|
55
|
+
);
|
|
56
|
+
CREATE TABLE chunks (
|
|
57
|
+
id INTEGER PRIMARY KEY,
|
|
58
|
+
document_id INTEGER NOT NULL,
|
|
59
|
+
chunk_index INTEGER NOT NULL,
|
|
60
|
+
start_line INTEGER NOT NULL,
|
|
61
|
+
end_line INTEGER NOT NULL,
|
|
62
|
+
occurrences BLOB NOT NULL
|
|
63
|
+
);
|
|
64
|
+
CREATE INDEX idx_mentions_symbol_id_role ON mentions(symbol_id, role);
|
|
65
|
+
CREATE INDEX idx_defn_enclosing_ranges_symbol_id ON defn_enclosing_ranges(symbol_id);
|
|
66
|
+
CREATE INDEX idx_defn_enclosing_ranges_document ON defn_enclosing_ranges(document_id, start_line, end_line);
|
|
67
|
+
CREATE INDEX idx_chunks_doc_id ON chunks(document_id);
|
|
68
|
+
CREATE INDEX idx_global_symbols_symbol ON global_symbols(symbol);
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
// Insert test documents
|
|
72
|
+
run(`
|
|
73
|
+
INSERT INTO documents (id, language, relative_path) VALUES
|
|
74
|
+
(1, 'typescript', 'src/services/auth.service.ts'),
|
|
75
|
+
(2, 'typescript', 'src/services/user.service.ts'),
|
|
76
|
+
(3, 'typescript', 'src/controllers/auth.controller.ts'),
|
|
77
|
+
(4, 'typescript', 'src/__tests__/auth.test.ts'),
|
|
78
|
+
(5, 'python', 'lib/utils.py'),
|
|
79
|
+
(6, 'python', 'lib/models.py'),
|
|
80
|
+
(7, 'rust', 'src/main.rs');
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
// Insert test symbols
|
|
84
|
+
const insertSymbol = sqliteDb.prepare(
|
|
85
|
+
`INSERT INTO global_symbols (id, symbol, display_name, kind, documentation) VALUES (?, ?, ?, ?, ?)`
|
|
86
|
+
);
|
|
87
|
+
insertSymbol.run(1, "scip-typescript npm my-app 1.0.0 src/services/`auth.service.ts`/AuthService#", 'AuthService', 1, 'AuthService class|class AuthService');
|
|
88
|
+
insertSymbol.run(2, "scip-typescript npm my-app 1.0.0 src/services/`auth.service.ts`/AuthService#login().", 'login', 2, "Login method|```ts\n(method) login(email: string): Promise<Token>\n```");
|
|
89
|
+
insertSymbol.run(3, "scip-typescript npm my-app 1.0.0 src/services/`auth.service.ts`/AuthService#logout().", 'logout', 2, "Logout method|```ts\n(method) logout(): void\n```");
|
|
90
|
+
insertSymbol.run(4, "scip-typescript npm my-app 1.0.0 src/services/`user.service.ts`/UserService#", 'UserService', 1, 'User service|class UserService');
|
|
91
|
+
insertSymbol.run(5, "scip-typescript npm my-app 1.0.0 src/services/`user.service.ts`/UserService#findById().", 'findById', 2, "Find by ID|```ts\n(method) findById(id: string): Promise<User>\n```");
|
|
92
|
+
insertSymbol.run(6, "scip-typescript npm my-app 1.0.0 src/services/`user.service.ts`/deadExport.", 'deadExport', 3, 'Not used anywhere|function deadExport(): void');
|
|
93
|
+
insertSymbol.run(7, "scip-python pip my-lib 1.0.0 lib/`utils.py`/format_name.", 'format_name', 3, 'Format name|def format_name(name: str) -> str');
|
|
94
|
+
insertSymbol.run(8, "rust-analyzer cargo my-crate 0.1.0 src/`main.rs`/Config#", 'Config', 1, 'Config struct|struct Config');
|
|
95
|
+
|
|
96
|
+
// Insert definition ranges
|
|
97
|
+
run(`
|
|
98
|
+
INSERT INTO defn_enclosing_ranges (id, document_id, symbol_id, start_line, start_char, end_line, end_char) VALUES
|
|
99
|
+
(1, 1, 1, 1, 0, 50, 1),
|
|
100
|
+
(2, 1, 2, 5, 2, 20, 3),
|
|
101
|
+
(3, 1, 3, 22, 2, 35, 3),
|
|
102
|
+
(4, 2, 4, 1, 0, 40, 1),
|
|
103
|
+
(5, 2, 5, 5, 2, 25, 3),
|
|
104
|
+
(6, 2, 6, 27, 2, 45, 3),
|
|
105
|
+
(7, 5, 7, 10, 0, 25, 0),
|
|
106
|
+
(8, 7, 8, 1, 0, 15, 1);
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
// Insert chunks
|
|
110
|
+
run(`
|
|
111
|
+
INSERT INTO chunks (id, document_id, chunk_index, start_line, end_line, occurrences) VALUES
|
|
112
|
+
(1, 1, 0, 0, 50, X'00'),
|
|
113
|
+
(2, 2, 0, 0, 45, X'00'),
|
|
114
|
+
(3, 3, 0, 0, 30, X'00'),
|
|
115
|
+
(4, 4, 0, 0, 20, X'00'),
|
|
116
|
+
(5, 5, 0, 0, 30, X'00'),
|
|
117
|
+
(6, 6, 0, 0, 20, X'00'),
|
|
118
|
+
(7, 7, 0, 0, 20, X'00');
|
|
119
|
+
`);
|
|
120
|
+
|
|
121
|
+
// Insert mentions (role: 0 = reference, 1 = definition)
|
|
122
|
+
run(`
|
|
123
|
+
INSERT INTO mentions (chunk_id, symbol_id, role) VALUES
|
|
124
|
+
(1, 2, 1), (3, 2, 0), (4, 2, 0),
|
|
125
|
+
(1, 3, 1), (3, 3, 0),
|
|
126
|
+
(2, 5, 1), (1, 5, 0),
|
|
127
|
+
(2, 6, 1),
|
|
128
|
+
(5, 7, 1), (6, 7, 0),
|
|
129
|
+
(7, 8, 1),
|
|
130
|
+
(1, 1, 1), (3, 1, 0),
|
|
131
|
+
(2, 4, 1), (1, 4, 0);
|
|
132
|
+
`);
|
|
133
|
+
|
|
134
|
+
sqliteDb.close();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Test Suite ──────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
describe('query engine', () => {
|
|
140
|
+
let db: ScipDatabase;
|
|
141
|
+
let tempDir: string;
|
|
142
|
+
let dbPath: string;
|
|
143
|
+
|
|
144
|
+
beforeAll(() => {
|
|
145
|
+
tempDir = mkdtempSync(join(tmpdir(), 'scip-query-test-'));
|
|
146
|
+
dbPath = join(tempDir, 'index.db');
|
|
147
|
+
createFixtureDb(dbPath);
|
|
148
|
+
|
|
149
|
+
const config: ScipQueryConfig = {
|
|
150
|
+
dbPath,
|
|
151
|
+
indexPath: join(tempDir, 'index.scip'),
|
|
152
|
+
projectRoot: tempDir,
|
|
153
|
+
};
|
|
154
|
+
db = new ScipDatabase(config);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
afterAll(() => {
|
|
158
|
+
db.close();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('stats', () => {
|
|
162
|
+
it('returns correct counts', () => {
|
|
163
|
+
const s = queries.stats(db);
|
|
164
|
+
expect(s.documents).toBe(7);
|
|
165
|
+
expect(s.symbols).toBe(8);
|
|
166
|
+
expect(s.definitions).toBeGreaterThan(0);
|
|
167
|
+
expect(s.references).toBeGreaterThan(0);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('files', () => {
|
|
172
|
+
it('finds files matching a pattern', () => {
|
|
173
|
+
const results = queries.files(db, 'auth');
|
|
174
|
+
const paths = results.map((r) => r.relativePath);
|
|
175
|
+
expect(paths).toContain('src/services/auth.service.ts');
|
|
176
|
+
expect(paths).toContain('src/controllers/auth.controller.ts');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('finds Python files', () => {
|
|
180
|
+
const results = queries.files(db, 'utils');
|
|
181
|
+
expect(results).toHaveLength(1);
|
|
182
|
+
expect(results[0]!.relativePath).toBe('lib/utils.py');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('finds Rust files', () => {
|
|
186
|
+
const results = queries.files(db, 'main.rs');
|
|
187
|
+
expect(results).toHaveLength(1);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('symbols', () => {
|
|
192
|
+
it('lists symbols in a TypeScript file', () => {
|
|
193
|
+
const results = queries.symbols(db, 'auth.service.ts');
|
|
194
|
+
expect(results.length).toBeGreaterThan(0);
|
|
195
|
+
|
|
196
|
+
const loginSymbol = results.find((s) => s.shortName.includes('login'));
|
|
197
|
+
expect(loginSymbol).toBeDefined();
|
|
198
|
+
expect(loginSymbol!.startLine).toBe(5);
|
|
199
|
+
expect(loginSymbol!.endLine).toBe(20);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('returns cleaned signatures', () => {
|
|
203
|
+
const results = queries.symbols(db, 'auth.service.ts');
|
|
204
|
+
const login = results.find((s) => s.shortName.includes('login'));
|
|
205
|
+
expect(login?.signature).toBeDefined();
|
|
206
|
+
expect(login?.signature).not.toContain('```');
|
|
207
|
+
expect(login?.signature).not.toContain('(method)');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('methods', () => {
|
|
212
|
+
it('lists methods of a class', () => {
|
|
213
|
+
const results = queries.methods(db, 'AuthService');
|
|
214
|
+
expect(results.length).toBe(2);
|
|
215
|
+
const names = results.map((m) => m.name);
|
|
216
|
+
expect(names).toContain('login');
|
|
217
|
+
expect(names).toContain('logout');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('refs', () => {
|
|
222
|
+
it('finds cross-file references', () => {
|
|
223
|
+
const results = queries.refs(db, 'login');
|
|
224
|
+
const files = results.map((r) => r.relativePath);
|
|
225
|
+
expect(files).toContain('src/controllers/auth.controller.ts');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('trace', () => {
|
|
230
|
+
it('returns definitions and references', () => {
|
|
231
|
+
const result = queries.trace(db, 'login');
|
|
232
|
+
expect(result.definitions.length).toBeGreaterThan(0);
|
|
233
|
+
expect(result.definitions[0]!.relativePath).toBe('src/services/auth.service.ts');
|
|
234
|
+
expect(result.referencedBy.length).toBeGreaterThan(0);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('deps', () => {
|
|
239
|
+
it('finds forward dependencies', () => {
|
|
240
|
+
const results = queries.deps(db, 'auth.service.ts');
|
|
241
|
+
const paths = results.map((r) => r.relativePath);
|
|
242
|
+
expect(paths).toContain('src/services/user.service.ts');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('rdeps', () => {
|
|
247
|
+
it('finds reverse dependencies', () => {
|
|
248
|
+
const results = queries.rdeps(db, 'auth.service.ts');
|
|
249
|
+
const paths = results.map((r) => r.relativePath);
|
|
250
|
+
expect(paths).toContain('src/controllers/auth.controller.ts');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('system', () => {
|
|
255
|
+
it('returns full module map', () => {
|
|
256
|
+
const result = queries.system(db, 'services');
|
|
257
|
+
expect(result.files.length).toBe(2);
|
|
258
|
+
expect(result.symbols.length).toBeGreaterThan(0);
|
|
259
|
+
expect(result.dependedOnBy.length).toBeGreaterThan(0);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('surface', () => {
|
|
264
|
+
it('finds externally consumed symbols', () => {
|
|
265
|
+
const results = queries.surface(db, 'auth.service');
|
|
266
|
+
expect(results.length).toBeGreaterThan(0);
|
|
267
|
+
const consumers = results.map((r) => r.consumer);
|
|
268
|
+
expect(consumers).toContain('src/controllers/auth.controller.ts');
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('dead', () => {
|
|
273
|
+
it('finds dead exports', () => {
|
|
274
|
+
const result = queries.dead(db, { minLoc: 1 });
|
|
275
|
+
const deadNames = result.symbols.map((s) => s.shortName);
|
|
276
|
+
const hasDead = deadNames.some((n) => n.includes('deadExport'));
|
|
277
|
+
expect(hasDead).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('respects scope filter', () => {
|
|
281
|
+
const result = queries.dead(db, { scope: 'lib/', minLoc: 1 });
|
|
282
|
+
for (const s of result.symbols) {
|
|
283
|
+
expect(s.relativePath).toMatch(/^lib\//);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('classifies dead-code vs dead-export correctly', () => {
|
|
288
|
+
const result = queries.dead(db, { minLoc: 1 });
|
|
289
|
+
const deadExportSymbol = result.symbols.find((s) => s.shortName.includes('deadExport'));
|
|
290
|
+
expect(deadExportSymbol?.kind).toBe('dead-code');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('excludes test files by default', () => {
|
|
294
|
+
const result = queries.dead(db, { minLoc: 1 });
|
|
295
|
+
for (const s of result.symbols) {
|
|
296
|
+
expect(s.relativePath).not.toContain('__tests__');
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|