Skip to content

Commit d6c3ce3

Browse files
committed
feat: type domain/analysis modules, parser WASM maps, and add canStripTypes helper (#554)
Address deferred review feedback from PR #554: - Convert all 9 domain/analysis/*.js → .ts with BetterSqlite3.Database and NodeRow type annotations replacing untyped db/node parameters - Type WASM parser cache maps (Parser, Language, Query) in domain/parser.ts with LanguageRegistryEntry interface and full function signatures - Extract canStripTypes Node version probe to tests/helpers/node-version.js shared helper (ready for import when CLI test TS guards land)
1 parent a9086bf commit d6c3ce3

11 files changed

Lines changed: 3666 additions & 0 deletions

File tree

src/domain/analysis/brief.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import type BetterSqlite3 from 'better-sqlite3';
2+
import {
3+
findDistinctCallers,
4+
findFileNodes,
5+
findImportDependents,
6+
findImportSources,
7+
findImportTargets,
8+
findNodesByFile,
9+
openReadonlyOrFail,
10+
} from '../../db/index.js';
11+
import { loadConfig } from '../../infrastructure/config.js';
12+
import { isTestFile } from '../../infrastructure/test-filter.js';
13+
import type { ImportEdgeRow, NodeRow, RelatedNodeRow } from '../../types.js';
14+
15+
/** Symbol kinds meaningful for a file brief — excludes parameters, properties, constants. */
16+
const BRIEF_KINDS = new Set([
17+
'function',
18+
'method',
19+
'class',
20+
'interface',
21+
'type',
22+
'struct',
23+
'enum',
24+
'trait',
25+
'record',
26+
'module',
27+
]);
28+
29+
/**
30+
* Compute file risk tier from symbol roles and max fan-in.
31+
*/
32+
function computeRiskTier(
33+
symbols: Array<{ role: string | null; callerCount: number }>,
34+
highThreshold = 10,
35+
mediumThreshold = 3,
36+
): 'high' | 'medium' | 'low' {
37+
let maxCallers = 0;
38+
let hasCoreRole = false;
39+
for (const s of symbols) {
40+
if (s.callerCount > maxCallers) maxCallers = s.callerCount;
41+
if (s.role === 'core') hasCoreRole = true;
42+
}
43+
if (maxCallers >= highThreshold || hasCoreRole) return 'high';
44+
if (maxCallers >= mediumThreshold) return 'medium';
45+
return 'low';
46+
}
47+
48+
/**
49+
* BFS to count transitive callers for a single node.
50+
* Lightweight variant — only counts, does not collect details.
51+
*/
52+
function countTransitiveCallers(
53+
db: BetterSqlite3.Database,
54+
startId: number,
55+
noTests: boolean,
56+
maxDepth = 5,
57+
): number {
58+
const visited = new Set([startId]);
59+
let frontier = [startId];
60+
61+
for (let d = 1; d <= maxDepth; d++) {
62+
const nextFrontier: number[] = [];
63+
for (const fid of frontier) {
64+
const callers = findDistinctCallers(db, fid) as RelatedNodeRow[];
65+
for (const c of callers) {
66+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
67+
visited.add(c.id);
68+
nextFrontier.push(c.id);
69+
}
70+
}
71+
}
72+
frontier = nextFrontier;
73+
if (frontier.length === 0) break;
74+
}
75+
76+
return visited.size - 1;
77+
}
78+
79+
/**
80+
* Count transitive file-level import dependents via BFS.
81+
* Depth-bounded to match countTransitiveCallers and keep hook latency predictable.
82+
*/
83+
function countTransitiveImporters(
84+
db: BetterSqlite3.Database,
85+
fileNodeIds: number[],
86+
noTests: boolean,
87+
maxDepth = 5,
88+
): number {
89+
const visited = new Set(fileNodeIds);
90+
let frontier = [...fileNodeIds];
91+
92+
for (let d = 1; d <= maxDepth; d++) {
93+
const nextFrontier: number[] = [];
94+
for (const current of frontier) {
95+
const dependents = findImportDependents(db, current) as RelatedNodeRow[];
96+
for (const dep of dependents) {
97+
if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
98+
visited.add(dep.id);
99+
nextFrontier.push(dep.id);
100+
}
101+
}
102+
}
103+
frontier = nextFrontier;
104+
if (frontier.length === 0) break;
105+
}
106+
107+
return visited.size - fileNodeIds.length;
108+
}
109+
110+
/**
111+
* Produce a token-efficient file brief: symbols with roles and caller counts,
112+
* importer info with transitive count, and file risk tier.
113+
*/
114+
export function briefData(
115+
file: string,
116+
customDbPath: string,
117+
// biome-ignore lint/suspicious/noExplicitAny: config shape is dynamic
118+
opts: { noTests?: boolean; config?: any } = {},
119+
) {
120+
const db = openReadonlyOrFail(customDbPath);
121+
try {
122+
const noTests = opts.noTests || false;
123+
const config = opts.config || loadConfig();
124+
const callerDepth = config.analysis?.briefCallerDepth ?? 5;
125+
const importerDepth = config.analysis?.briefImporterDepth ?? 5;
126+
const highRiskCallers = config.analysis?.briefHighRiskCallers ?? 10;
127+
const mediumRiskCallers = config.analysis?.briefMediumRiskCallers ?? 3;
128+
const fileNodes = findFileNodes(db, `%${file}%`) as NodeRow[];
129+
if (fileNodes.length === 0) {
130+
return { file, results: [] };
131+
}
132+
133+
const results = fileNodes.map((fn) => {
134+
// Direct importers
135+
let importedBy = findImportSources(db, fn.id) as ImportEdgeRow[];
136+
if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
137+
const directImporters = [...new Set(importedBy.map((i) => i.file))];
138+
139+
// Transitive importer count
140+
const totalImporterCount = countTransitiveImporters(db, [fn.id], noTests, importerDepth);
141+
142+
// Direct imports
143+
let importsTo = findImportTargets(db, fn.id) as ImportEdgeRow[];
144+
if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));
145+
146+
// Symbol definitions with roles and caller counts
147+
const defs = (findNodesByFile(db, fn.file) as NodeRow[]).filter((d) =>
148+
BRIEF_KINDS.has(d.kind),
149+
);
150+
const symbols = defs.map((d) => {
151+
const callerCount = countTransitiveCallers(db, d.id, noTests, callerDepth);
152+
return {
153+
name: d.name,
154+
kind: d.kind,
155+
line: d.line,
156+
role: d.role || null,
157+
callerCount,
158+
};
159+
});
160+
161+
const riskTier = computeRiskTier(symbols, highRiskCallers, mediumRiskCallers);
162+
163+
return {
164+
file: fn.file,
165+
risk: riskTier,
166+
imports: importsTo.map((i) => i.file),
167+
importedBy: directImporters,
168+
totalImporterCount,
169+
symbols,
170+
};
171+
});
172+
173+
return { file, results };
174+
} finally {
175+
db.close();
176+
}
177+
}

0 commit comments

Comments
 (0)