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