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