@optave/codegraph 3.11.0 → 3.11.2
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/README.md +38 -31
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +91 -60
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/visitor-utils.d.ts +3 -0
- package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
- package/dist/ast-analysis/visitor-utils.js +83 -49
- package/dist/ast-analysis/visitor-utils.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
- package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
- package/dist/cli/commands/embed.d.ts.map +1 -1
- package/dist/cli/commands/embed.js +49 -4
- package/dist/cli/commands/embed.js.map +1 -1
- package/dist/domain/analysis/dependencies.d.ts.map +1 -1
- package/dist/domain/analysis/dependencies.js +106 -80
- package/dist/domain/analysis/dependencies.js.map +1 -1
- package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
- package/dist/domain/analysis/fn-impact.js +77 -52
- package/dist/domain/analysis/fn-impact.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +132 -121
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/call-resolver.d.ts +71 -0
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
- package/dist/domain/graph/builder/call-resolver.js +130 -0
- package/dist/domain/graph/builder/call-resolver.js.map +1 -0
- package/dist/domain/graph/builder/helpers.d.ts +4 -4
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +47 -33
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts +6 -0
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +214 -127
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts +1 -44
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +10 -766
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +151 -192
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-structure.js +82 -65
- package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +60 -51
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
- package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
- package/dist/domain/graph/cycles.d.ts +6 -4
- package/dist/domain/graph/cycles.d.ts.map +1 -1
- package/dist/domain/graph/cycles.js +50 -55
- package/dist/domain/graph/cycles.js.map +1 -1
- package/dist/domain/graph/journal.d.ts.map +1 -1
- package/dist/domain/graph/journal.js +89 -70
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +10 -4
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +12 -23
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +126 -79
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/generator.d.ts +3 -1
- package/dist/domain/search/generator.d.ts.map +1 -1
- package/dist/domain/search/generator.js +68 -45
- package/dist/domain/search/generator.js.map +1 -1
- package/dist/domain/search/models.d.ts +2 -0
- package/dist/domain/search/models.d.ts.map +1 -1
- package/dist/domain/search/models.js +37 -3
- package/dist/domain/search/models.js.map +1 -1
- package/dist/domain/search/search/hybrid.d.ts.map +1 -1
- package/dist/domain/search/search/hybrid.js +49 -40
- package/dist/domain/search/search/hybrid.js.map +1 -1
- package/dist/domain/search/search/semantic.d.ts.map +1 -1
- package/dist/domain/search/search/semantic.js +69 -49
- package/dist/domain/search/search/semantic.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +201 -136
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/extractors/elixir.js +95 -71
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/gleam.d.ts.map +1 -1
- package/dist/extractors/gleam.js +23 -31
- package/dist/extractors/gleam.js.map +1 -1
- package/dist/extractors/helpers.d.ts +79 -1
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +137 -0
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.d.ts.map +1 -1
- package/dist/extractors/java.js +37 -49
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +44 -44
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/julia.js +27 -34
- package/dist/extractors/julia.js.map +1 -1
- package/dist/extractors/r.d.ts.map +1 -1
- package/dist/extractors/r.js +33 -58
- package/dist/extractors/r.js.map +1 -1
- package/dist/extractors/solidity.d.ts.map +1 -1
- package/dist/extractors/solidity.js +38 -61
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/features/boundaries.d.ts.map +1 -1
- package/dist/features/boundaries.js +49 -39
- package/dist/features/boundaries.js.map +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +90 -63
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/check.d.ts.map +1 -1
- package/dist/features/check.js +43 -34
- package/dist/features/check.js.map +1 -1
- package/dist/features/cochange.d.ts.map +1 -1
- package/dist/features/cochange.js +68 -56
- package/dist/features/cochange.js.map +1 -1
- package/dist/features/complexity.d.ts.map +1 -1
- package/dist/features/complexity.js +105 -75
- package/dist/features/complexity.js.map +1 -1
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +37 -29
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/flow.d.ts.map +1 -1
- package/dist/features/flow.js +31 -22
- package/dist/features/flow.js.map +1 -1
- package/dist/features/graph-enrichment.d.ts.map +1 -1
- package/dist/features/graph-enrichment.js +77 -70
- package/dist/features/graph-enrichment.js.map +1 -1
- package/dist/features/owners.d.ts +17 -26
- package/dist/features/owners.d.ts.map +1 -1
- package/dist/features/owners.js +120 -109
- package/dist/features/owners.js.map +1 -1
- package/dist/features/sequence.d.ts.map +1 -1
- package/dist/features/sequence.js +59 -54
- package/dist/features/sequence.js.map +1 -1
- package/dist/features/structure-query.d.ts.map +1 -1
- package/dist/features/structure-query.js +60 -60
- package/dist/features/structure-query.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +149 -52
- package/dist/features/structure.js.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
- package/dist/graph/algorithms/leiden/optimiser.js +100 -69
- package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +63 -59
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/infrastructure/config.d.ts +1 -1
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +1 -1
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/presentation/cfg.d.ts.map +1 -1
- package/dist/presentation/cfg.js +44 -29
- package/dist/presentation/cfg.js.map +1 -1
- package/dist/presentation/flow.d.ts.map +1 -1
- package/dist/presentation/flow.js +58 -38
- package/dist/presentation/flow.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/package.json +9 -9
- package/src/ast-analysis/engine.ts +145 -61
- package/src/ast-analysis/visitor-utils.ts +86 -46
- package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
- package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
- package/src/cli/commands/embed.ts +54 -4
- package/src/domain/analysis/dependencies.ts +166 -85
- package/src/domain/analysis/fn-impact.ts +120 -50
- package/src/domain/analysis/module-map.ts +175 -140
- package/src/domain/graph/builder/call-resolver.ts +181 -0
- package/src/domain/graph/builder/helpers.ts +85 -76
- package/src/domain/graph/builder/incremental.ts +321 -152
- package/src/domain/graph/builder/pipeline.ts +19 -957
- package/src/domain/graph/builder/stages/build-edges.ts +229 -275
- package/src/domain/graph/builder/stages/build-structure.ts +115 -82
- package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
- package/src/domain/graph/builder/stages/finalize.ts +72 -70
- package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
- package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
- package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
- package/src/domain/graph/cycles.ts +51 -49
- package/src/domain/graph/journal.ts +84 -69
- package/src/domain/graph/watcher.ts +12 -4
- package/src/domain/parser.ts +143 -66
- package/src/domain/search/generator.ts +132 -74
- package/src/domain/search/models.ts +39 -3
- package/src/domain/search/search/hybrid.ts +53 -42
- package/src/domain/search/search/semantic.ts +105 -65
- package/src/domain/wasm-worker-entry.ts +235 -152
- package/src/extractors/elixir.ts +91 -64
- package/src/extractors/gleam.ts +33 -37
- package/src/extractors/helpers.ts +205 -1
- package/src/extractors/java.ts +42 -45
- package/src/extractors/javascript.ts +44 -43
- package/src/extractors/julia.ts +28 -35
- package/src/extractors/r.ts +38 -56
- package/src/extractors/solidity.ts +43 -71
- package/src/features/boundaries.ts +64 -46
- package/src/features/cfg.ts +145 -74
- package/src/features/check.ts +60 -43
- package/src/features/cochange.ts +95 -72
- package/src/features/complexity.ts +134 -79
- package/src/features/dataflow.ts +57 -34
- package/src/features/flow.ts +48 -24
- package/src/features/graph-enrichment.ts +105 -70
- package/src/features/owners.ts +186 -146
- package/src/features/sequence.ts +99 -69
- package/src/features/structure-query.ts +94 -79
- package/src/features/structure.ts +199 -79
- package/src/graph/algorithms/leiden/optimiser.ts +142 -87
- package/src/graph/classifiers/roles.ts +64 -54
- package/src/infrastructure/config.ts +1 -1
- package/src/presentation/cfg.ts +48 -32
- package/src/presentation/flow.ts +100 -52
- package/src/types.ts +1 -1
package/src/features/owners.ts
CHANGED
|
@@ -139,18 +139,25 @@ interface OwnersDataOpts {
|
|
|
139
139
|
boundary?: boolean;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
interface OwnedSymbol {
|
|
143
|
+
name: string;
|
|
144
|
+
kind: string;
|
|
145
|
+
file: string;
|
|
146
|
+
line: number;
|
|
147
|
+
owners: string[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface OwnerBoundary {
|
|
151
|
+
from: OwnedSymbol;
|
|
152
|
+
to: OwnedSymbol;
|
|
153
|
+
edgeKind: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface OwnersDataResult {
|
|
146
157
|
codeownersFile: string | null;
|
|
147
158
|
files: { file: string; owners: string[] }[];
|
|
148
|
-
symbols:
|
|
149
|
-
boundaries:
|
|
150
|
-
from: { name: string; kind: string; file: string; line: number; owners: string[] };
|
|
151
|
-
to: { name: string; kind: string; file: string; line: number; owners: string[] };
|
|
152
|
-
edgeKind: string;
|
|
153
|
-
}[];
|
|
159
|
+
symbols: OwnedSymbol[];
|
|
160
|
+
boundaries: OwnerBoundary[];
|
|
154
161
|
summary: {
|
|
155
162
|
totalFiles: number;
|
|
156
163
|
ownedFiles: number;
|
|
@@ -159,160 +166,193 @@ export function ownersData(
|
|
|
159
166
|
ownerCount: number;
|
|
160
167
|
byOwner: { owner: string; fileCount: number }[];
|
|
161
168
|
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface BetterSqlite3DatabaseLike {
|
|
172
|
+
prepare(sql: string): { all(...params: unknown[]): unknown[] };
|
|
173
|
+
close(): void;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function emptyOwnersResult(codeownersFile: string | null): OwnersDataResult {
|
|
177
|
+
return {
|
|
178
|
+
codeownersFile,
|
|
179
|
+
files: [],
|
|
180
|
+
symbols: [],
|
|
181
|
+
boundaries: [],
|
|
182
|
+
summary: {
|
|
183
|
+
totalFiles: 0,
|
|
184
|
+
ownedFiles: 0,
|
|
185
|
+
unownedFiles: 0,
|
|
186
|
+
coveragePercent: 0,
|
|
187
|
+
ownerCount: 0,
|
|
188
|
+
byOwner: [],
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Load all distinct files from the DB and apply test/file filters. */
|
|
194
|
+
function loadFilteredFiles(db: BetterSqlite3DatabaseLike, opts: OwnersDataOpts): string[] {
|
|
195
|
+
let allFiles = (db.prepare('SELECT DISTINCT file FROM nodes').all() as { file: string }[]).map(
|
|
196
|
+
(r) => r.file,
|
|
197
|
+
);
|
|
198
|
+
if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
|
|
199
|
+
const fileFilters = normalizeFileFilter(opts.file);
|
|
200
|
+
if (fileFilters.length > 0) {
|
|
201
|
+
allFiles = allFiles.filter((f) => fileFilters.some((filter) => f.includes(filter)));
|
|
202
|
+
}
|
|
203
|
+
return allFiles;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Build owner index (owner -> list of files) and count owned files. */
|
|
207
|
+
function buildOwnerIndex(fileOwners: { file: string; owners: string[] }[]): {
|
|
208
|
+
ownerIndex: Map<string, string[]>;
|
|
209
|
+
ownedCount: number;
|
|
162
210
|
} {
|
|
211
|
+
const ownerIndex = new Map<string, string[]>();
|
|
212
|
+
let ownedCount = 0;
|
|
213
|
+
for (const fo of fileOwners) {
|
|
214
|
+
if (fo.owners.length > 0) ownedCount++;
|
|
215
|
+
for (const o of fo.owners) {
|
|
216
|
+
if (!ownerIndex.has(o)) ownerIndex.set(o, []);
|
|
217
|
+
ownerIndex.get(o)!.push(fo.file);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { ownerIndex, ownedCount };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Load symbols restricted to the given file set, applying noTests and kind filters. */
|
|
224
|
+
function loadSymbolsForFiles(
|
|
225
|
+
db: BetterSqlite3DatabaseLike,
|
|
226
|
+
fileSet: Set<string>,
|
|
227
|
+
opts: OwnersDataOpts,
|
|
228
|
+
rules: CodeownersRule[],
|
|
229
|
+
): OwnedSymbol[] {
|
|
230
|
+
let symbols = (
|
|
231
|
+
db.prepare('SELECT name, kind, file, line FROM nodes').all() as {
|
|
232
|
+
name: string;
|
|
233
|
+
kind: string;
|
|
234
|
+
file: string;
|
|
235
|
+
line: number;
|
|
236
|
+
}[]
|
|
237
|
+
).filter((n) => fileSet.has(n.file));
|
|
238
|
+
|
|
239
|
+
if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
|
|
240
|
+
if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
|
|
241
|
+
|
|
242
|
+
return symbols.map((s) => ({ ...s, owners: matchOwners(s.file, rules) }));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
interface CallEdgeRow {
|
|
246
|
+
id: number;
|
|
247
|
+
edgeKind: string;
|
|
248
|
+
srcName: string;
|
|
249
|
+
srcKind: string;
|
|
250
|
+
srcFile: string;
|
|
251
|
+
srcLine: number;
|
|
252
|
+
tgtName: string;
|
|
253
|
+
tgtKind: string;
|
|
254
|
+
tgtFile: string;
|
|
255
|
+
tgtLine: number;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Compute cross-owner call boundaries. Returns empty array when boundary mode is off. */
|
|
259
|
+
function computeOwnerBoundaries(
|
|
260
|
+
db: BetterSqlite3DatabaseLike,
|
|
261
|
+
rules: CodeownersRule[],
|
|
262
|
+
noTests: boolean,
|
|
263
|
+
): OwnerBoundary[] {
|
|
264
|
+
const edges = db
|
|
265
|
+
.prepare(
|
|
266
|
+
`SELECT e.id, e.kind AS edgeKind,
|
|
267
|
+
s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
|
|
268
|
+
t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
|
|
269
|
+
FROM edges e
|
|
270
|
+
JOIN nodes s ON e.source_id = s.id
|
|
271
|
+
JOIN nodes t ON e.target_id = t.id
|
|
272
|
+
WHERE e.kind = 'calls'`,
|
|
273
|
+
)
|
|
274
|
+
.all() as CallEdgeRow[];
|
|
275
|
+
|
|
276
|
+
const boundaries: OwnerBoundary[] = [];
|
|
277
|
+
for (const e of edges) {
|
|
278
|
+
if (noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
|
|
279
|
+
const srcOwners = matchOwners(e.srcFile, rules);
|
|
280
|
+
const tgtOwners = matchOwners(e.tgtFile, rules);
|
|
281
|
+
// Cross-boundary: different owner sets (sort for deterministic comparison + output)
|
|
282
|
+
const sortedSrc = [...srcOwners].sort();
|
|
283
|
+
const sortedTgt = [...tgtOwners].sort();
|
|
284
|
+
const srcKey = sortedSrc.join(',');
|
|
285
|
+
const tgtKey = sortedTgt.join(',');
|
|
286
|
+
if (srcKey === tgtKey) continue;
|
|
287
|
+
boundaries.push({
|
|
288
|
+
from: {
|
|
289
|
+
name: e.srcName,
|
|
290
|
+
kind: e.srcKind,
|
|
291
|
+
file: e.srcFile,
|
|
292
|
+
line: e.srcLine,
|
|
293
|
+
owners: sortedSrc,
|
|
294
|
+
},
|
|
295
|
+
to: { name: e.tgtName, kind: e.tgtKind, file: e.tgtFile, line: e.tgtLine, owners: sortedTgt },
|
|
296
|
+
edgeKind: e.edgeKind,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return boundaries;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Build summary stats (totals, coverage, by-owner counts). */
|
|
303
|
+
function buildOwnersSummary(
|
|
304
|
+
totalFiles: number,
|
|
305
|
+
ownedCount: number,
|
|
306
|
+
ownerIndex: Map<string, string[]>,
|
|
307
|
+
): OwnersDataResult['summary'] {
|
|
308
|
+
const byOwner = [...ownerIndex.entries()]
|
|
309
|
+
.map(([owner, files]) => ({ owner, fileCount: files.length }))
|
|
310
|
+
.sort((a, b) => b.fileCount - a.fileCount);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
totalFiles,
|
|
314
|
+
ownedFiles: ownedCount,
|
|
315
|
+
unownedFiles: totalFiles - ownedCount,
|
|
316
|
+
coveragePercent: totalFiles > 0 ? Math.round((ownedCount / totalFiles) * 100) : 0,
|
|
317
|
+
ownerCount: ownerIndex.size,
|
|
318
|
+
byOwner,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function ownersData(customDbPath?: string, opts: OwnersDataOpts = {}): OwnersDataResult {
|
|
163
323
|
const db = openReadonlyOrFail(customDbPath);
|
|
164
324
|
try {
|
|
165
325
|
const dbPath = findDbPath(customDbPath);
|
|
166
326
|
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
167
327
|
|
|
168
328
|
const parsed = parseCodeowners(repoRoot);
|
|
169
|
-
if (!parsed)
|
|
170
|
-
return {
|
|
171
|
-
codeownersFile: null,
|
|
172
|
-
files: [],
|
|
173
|
-
symbols: [],
|
|
174
|
-
boundaries: [],
|
|
175
|
-
summary: {
|
|
176
|
-
totalFiles: 0,
|
|
177
|
-
ownedFiles: 0,
|
|
178
|
-
unownedFiles: 0,
|
|
179
|
-
coveragePercent: 0,
|
|
180
|
-
ownerCount: 0,
|
|
181
|
-
byOwner: [],
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Get all distinct files from nodes
|
|
187
|
-
let allFiles = (db.prepare('SELECT DISTINCT file FROM nodes').all() as { file: string }[]).map(
|
|
188
|
-
(r) => r.file,
|
|
189
|
-
);
|
|
329
|
+
if (!parsed) return emptyOwnersResult(null);
|
|
190
330
|
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Map files to owners
|
|
198
|
-
const fileOwners = allFiles.map((file) => ({
|
|
199
|
-
file,
|
|
200
|
-
owners: matchOwners(file, parsed.rules),
|
|
201
|
-
}));
|
|
202
|
-
|
|
203
|
-
// Build owner-to-files index
|
|
204
|
-
const ownerIndex = new Map<string, string[]>();
|
|
205
|
-
let ownedCount = 0;
|
|
206
|
-
for (const fo of fileOwners) {
|
|
207
|
-
if (fo.owners.length > 0) ownedCount++;
|
|
208
|
-
for (const o of fo.owners) {
|
|
209
|
-
if (!ownerIndex.has(o)) ownerIndex.set(o, []);
|
|
210
|
-
ownerIndex.get(o)!.push(fo.file);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
331
|
+
// Stage 1: load files and bucket them by owner
|
|
332
|
+
const allFiles = loadFilteredFiles(db, opts);
|
|
333
|
+
const fileOwners = allFiles.map((file) => ({ file, owners: matchOwners(file, parsed.rules) }));
|
|
334
|
+
const { ownerIndex, ownedCount } = buildOwnerIndex(fileOwners);
|
|
213
335
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
336
|
+
// Stage 2: apply optional --owner filter
|
|
337
|
+
const filteredFiles = opts.owner
|
|
338
|
+
? fileOwners.filter((fo) => fo.owners.includes(opts.owner!))
|
|
339
|
+
: fileOwners;
|
|
219
340
|
|
|
220
|
-
//
|
|
341
|
+
// Stage 3: load symbols for filtered files
|
|
221
342
|
const fileSet = new Set(filteredFiles.map((fo) => fo.file));
|
|
222
|
-
|
|
223
|
-
db.prepare('SELECT name, kind, file, line FROM nodes').all() as {
|
|
224
|
-
name: string;
|
|
225
|
-
kind: string;
|
|
226
|
-
file: string;
|
|
227
|
-
line: number;
|
|
228
|
-
}[]
|
|
229
|
-
).filter((n) => fileSet.has(n.file));
|
|
230
|
-
|
|
231
|
-
if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
|
|
232
|
-
if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
|
|
233
|
-
|
|
234
|
-
const symbolsWithOwners = symbols.map((s) => ({
|
|
235
|
-
...s,
|
|
236
|
-
owners: matchOwners(s.file, parsed.rules),
|
|
237
|
-
}));
|
|
238
|
-
|
|
239
|
-
// Boundary analysis — cross-owner call edges
|
|
240
|
-
const boundaries: {
|
|
241
|
-
from: { name: string; kind: string; file: string; line: number; owners: string[] };
|
|
242
|
-
to: { name: string; kind: string; file: string; line: number; owners: string[] };
|
|
243
|
-
edgeKind: string;
|
|
244
|
-
}[] = [];
|
|
245
|
-
if (opts.boundary) {
|
|
246
|
-
const edges = db
|
|
247
|
-
.prepare(
|
|
248
|
-
`SELECT e.id, e.kind AS edgeKind,
|
|
249
|
-
s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
|
|
250
|
-
t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
|
|
251
|
-
FROM edges e
|
|
252
|
-
JOIN nodes s ON e.source_id = s.id
|
|
253
|
-
JOIN nodes t ON e.target_id = t.id
|
|
254
|
-
WHERE e.kind = 'calls'`,
|
|
255
|
-
)
|
|
256
|
-
.all() as {
|
|
257
|
-
id: number;
|
|
258
|
-
edgeKind: string;
|
|
259
|
-
srcName: string;
|
|
260
|
-
srcKind: string;
|
|
261
|
-
srcFile: string;
|
|
262
|
-
srcLine: number;
|
|
263
|
-
tgtName: string;
|
|
264
|
-
tgtKind: string;
|
|
265
|
-
tgtFile: string;
|
|
266
|
-
tgtLine: number;
|
|
267
|
-
}[];
|
|
268
|
-
|
|
269
|
-
for (const e of edges) {
|
|
270
|
-
if (opts.noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
|
|
271
|
-
const srcOwners = matchOwners(e.srcFile, parsed.rules);
|
|
272
|
-
const tgtOwners = matchOwners(e.tgtFile, parsed.rules);
|
|
273
|
-
// Cross-boundary: different owner sets
|
|
274
|
-
const srcKey = srcOwners.sort().join(',');
|
|
275
|
-
const tgtKey = tgtOwners.sort().join(',');
|
|
276
|
-
if (srcKey !== tgtKey) {
|
|
277
|
-
boundaries.push({
|
|
278
|
-
from: {
|
|
279
|
-
name: e.srcName,
|
|
280
|
-
kind: e.srcKind,
|
|
281
|
-
file: e.srcFile,
|
|
282
|
-
line: e.srcLine,
|
|
283
|
-
owners: srcOwners,
|
|
284
|
-
},
|
|
285
|
-
to: {
|
|
286
|
-
name: e.tgtName,
|
|
287
|
-
kind: e.tgtKind,
|
|
288
|
-
file: e.tgtFile,
|
|
289
|
-
line: e.tgtLine,
|
|
290
|
-
owners: tgtOwners,
|
|
291
|
-
},
|
|
292
|
-
edgeKind: e.edgeKind,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
343
|
+
const symbolsWithOwners = loadSymbolsForFiles(db, fileSet, opts, parsed.rules);
|
|
297
344
|
|
|
298
|
-
//
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
345
|
+
// Stage 4: optional boundary analysis (cross-owner call edges)
|
|
346
|
+
const boundaries = opts.boundary
|
|
347
|
+
? computeOwnerBoundaries(db, parsed.rules, opts.noTests ?? false)
|
|
348
|
+
: [];
|
|
302
349
|
|
|
303
350
|
return {
|
|
304
351
|
codeownersFile: parsed.path,
|
|
305
352
|
files: filteredFiles,
|
|
306
353
|
symbols: symbolsWithOwners,
|
|
307
354
|
boundaries,
|
|
308
|
-
summary:
|
|
309
|
-
totalFiles: allFiles.length,
|
|
310
|
-
ownedFiles: ownedCount,
|
|
311
|
-
unownedFiles: allFiles.length - ownedCount,
|
|
312
|
-
coveragePercent: allFiles.length > 0 ? Math.round((ownedCount / allFiles.length) * 100) : 0,
|
|
313
|
-
ownerCount: ownerIndex.size,
|
|
314
|
-
byOwner,
|
|
315
|
-
},
|
|
355
|
+
summary: buildOwnersSummary(allFiles.length, ownedCount, ownerIndex),
|
|
316
356
|
};
|
|
317
357
|
} finally {
|
|
318
358
|
db.close();
|
package/src/features/sequence.ts
CHANGED
|
@@ -91,6 +91,40 @@ interface BfsResult {
|
|
|
91
91
|
truncated: boolean;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
type CalleeNode = { id: number; name: string; file: string; kind: string; line: number };
|
|
95
|
+
|
|
96
|
+
interface BfsFrame {
|
|
97
|
+
visited: Set<number>;
|
|
98
|
+
messages: SequenceMessage[];
|
|
99
|
+
fileSet: Set<string>;
|
|
100
|
+
idToNode: Map<number, CalleeNode>;
|
|
101
|
+
nextFrontier: number[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function processCallee(
|
|
105
|
+
c: CalleeNode,
|
|
106
|
+
caller: CalleeNode,
|
|
107
|
+
depth: number,
|
|
108
|
+
noTests: boolean,
|
|
109
|
+
frame: BfsFrame,
|
|
110
|
+
): void {
|
|
111
|
+
if (noTests && isTestFile(c.file)) return;
|
|
112
|
+
|
|
113
|
+
frame.fileSet.add(c.file);
|
|
114
|
+
frame.messages.push({
|
|
115
|
+
from: caller.file,
|
|
116
|
+
to: c.file,
|
|
117
|
+
label: c.name,
|
|
118
|
+
type: 'call',
|
|
119
|
+
depth,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (frame.visited.has(c.id)) return;
|
|
123
|
+
frame.visited.add(c.id);
|
|
124
|
+
frame.nextFrontier.push(c.id);
|
|
125
|
+
frame.idToNode.set(c.id, c);
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
function bfsCallees(
|
|
95
129
|
repo: Repository,
|
|
96
130
|
matchNode: MatchNode,
|
|
@@ -101,46 +135,25 @@ function bfsCallees(
|
|
|
101
135
|
let frontier = [matchNode.id];
|
|
102
136
|
const messages: SequenceMessage[] = [];
|
|
103
137
|
const fileSet = new Set<string>([matchNode.file]);
|
|
104
|
-
const idToNode = new Map<
|
|
105
|
-
number,
|
|
106
|
-
{ id: number; name: string; file: string; kind: string; line: number }
|
|
107
|
-
>();
|
|
138
|
+
const idToNode = new Map<number, CalleeNode>();
|
|
108
139
|
idToNode.set(matchNode.id, matchNode);
|
|
109
140
|
let truncated = false;
|
|
110
141
|
|
|
111
142
|
for (let d = 1; d <= maxDepth; d++) {
|
|
112
|
-
const
|
|
143
|
+
const frame: BfsFrame = { visited, messages, fileSet, idToNode, nextFrontier: [] };
|
|
113
144
|
|
|
114
145
|
for (const fid of frontier) {
|
|
115
|
-
const callees = repo.findCallees(fid);
|
|
116
146
|
const caller = idToNode.get(fid)!;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (noTests && isTestFile(c.file)) continue;
|
|
120
|
-
|
|
121
|
-
fileSet.add(c.file);
|
|
122
|
-
messages.push({
|
|
123
|
-
from: caller.file,
|
|
124
|
-
to: c.file,
|
|
125
|
-
label: c.name,
|
|
126
|
-
type: 'call',
|
|
127
|
-
depth: d,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (visited.has(c.id)) continue;
|
|
131
|
-
|
|
132
|
-
visited.add(c.id);
|
|
133
|
-
nextFrontier.push(c.id);
|
|
134
|
-
idToNode.set(c.id, c);
|
|
147
|
+
for (const c of repo.findCallees(fid)) {
|
|
148
|
+
processCallee(c, caller, d, noTests, frame);
|
|
135
149
|
}
|
|
136
150
|
}
|
|
137
151
|
|
|
138
|
-
frontier = nextFrontier;
|
|
152
|
+
frontier = frame.nextFrontier;
|
|
139
153
|
if (frontier.length === 0) break;
|
|
140
154
|
|
|
141
|
-
if (d === maxDepth && frontier.length > 0) {
|
|
142
|
-
|
|
143
|
-
if (hasMoreCalls) truncated = true;
|
|
155
|
+
if (d === maxDepth && frontier.some((fid) => repo.findCallees(fid).length > 0)) {
|
|
156
|
+
truncated = true;
|
|
144
157
|
}
|
|
145
158
|
}
|
|
146
159
|
|
|
@@ -174,26 +187,16 @@ function annotateDataflow(
|
|
|
174
187
|
}
|
|
175
188
|
}
|
|
176
189
|
|
|
177
|
-
|
|
178
|
-
|
|
190
|
+
type DataflowStmts = {
|
|
191
|
+
getReturns: ReturnType<BetterSqlite3Database['prepare']>;
|
|
192
|
+
getFlowsTo: ReturnType<BetterSqlite3Database['prepare']>;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
function appendReturnMessages(
|
|
179
196
|
messages: SequenceMessage[],
|
|
180
|
-
|
|
197
|
+
nodeByNameFile: Map<string, { id: number; name: string; file: string }>,
|
|
198
|
+
stmts: DataflowStmts,
|
|
181
199
|
): void {
|
|
182
|
-
const nodeByNameFile = new Map<string, { id: number; name: string; file: string }>();
|
|
183
|
-
for (const n of idToNode.values()) {
|
|
184
|
-
nodeByNameFile.set(`${n.name}|${n.file}`, n);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const getReturns = db.prepare(
|
|
188
|
-
`SELECT d.expression FROM dataflow d
|
|
189
|
-
WHERE d.source_id = ? AND d.kind = 'returns'`,
|
|
190
|
-
);
|
|
191
|
-
const getFlowsTo = db.prepare(
|
|
192
|
-
`SELECT d.expression FROM dataflow d
|
|
193
|
-
WHERE d.target_id = ? AND d.kind = 'flows_to'
|
|
194
|
-
ORDER BY d.param_index`,
|
|
195
|
-
);
|
|
196
|
-
|
|
197
200
|
const seenReturns = new Set<string>();
|
|
198
201
|
for (const msg of [...messages]) {
|
|
199
202
|
if (msg.type !== 'call') continue;
|
|
@@ -203,40 +206,67 @@ function _annotateDataflowImpl(
|
|
|
203
206
|
const returnKey = `${msg.to}->${msg.from}:${msg.label}`;
|
|
204
207
|
if (seenReturns.has(returnKey)) continue;
|
|
205
208
|
|
|
206
|
-
const returns = getReturns.all(targetNode.id) as { expression: string }[];
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
});
|
|
218
|
-
}
|
|
209
|
+
const returns = stmts.getReturns.all(targetNode.id) as { expression: string }[];
|
|
210
|
+
if (returns.length === 0) continue;
|
|
211
|
+
|
|
212
|
+
seenReturns.add(returnKey);
|
|
213
|
+
messages.push({
|
|
214
|
+
from: msg.to,
|
|
215
|
+
to: msg.from,
|
|
216
|
+
label: returns[0]!.expression || 'result',
|
|
217
|
+
type: 'return',
|
|
218
|
+
depth: msg.depth,
|
|
219
|
+
});
|
|
219
220
|
}
|
|
221
|
+
}
|
|
220
222
|
|
|
223
|
+
function annotateCallParams(
|
|
224
|
+
messages: SequenceMessage[],
|
|
225
|
+
nodeByNameFile: Map<string, { id: number; name: string; file: string }>,
|
|
226
|
+
stmts: DataflowStmts,
|
|
227
|
+
): void {
|
|
221
228
|
for (const msg of messages) {
|
|
222
229
|
if (msg.type !== 'call') continue;
|
|
223
230
|
const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
|
|
224
231
|
if (!targetNode) continue;
|
|
225
232
|
|
|
226
|
-
const params = getFlowsTo.all(targetNode.id) as { expression: string }[];
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (paramNames.length > 0) {
|
|
234
|
-
msg.label = `${msg.label}(${paramNames.join(', ')})`;
|
|
235
|
-
}
|
|
233
|
+
const params = stmts.getFlowsTo.all(targetNode.id) as { expression: string }[];
|
|
234
|
+
const paramNames = params
|
|
235
|
+
.map((p) => p.expression)
|
|
236
|
+
.filter(Boolean)
|
|
237
|
+
.slice(0, 3);
|
|
238
|
+
if (paramNames.length > 0) {
|
|
239
|
+
msg.label = `${msg.label}(${paramNames.join(', ')})`;
|
|
236
240
|
}
|
|
237
241
|
}
|
|
238
242
|
}
|
|
239
243
|
|
|
244
|
+
function _annotateDataflowImpl(
|
|
245
|
+
db: BetterSqlite3Database,
|
|
246
|
+
messages: SequenceMessage[],
|
|
247
|
+
idToNode: Map<number, { id: number; name: string; file: string; kind: string; line: number }>,
|
|
248
|
+
): void {
|
|
249
|
+
const nodeByNameFile = new Map<string, { id: number; name: string; file: string }>();
|
|
250
|
+
for (const n of idToNode.values()) {
|
|
251
|
+
nodeByNameFile.set(`${n.name}|${n.file}`, n);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const stmts: DataflowStmts = {
|
|
255
|
+
getReturns: db.prepare(
|
|
256
|
+
`SELECT d.expression FROM dataflow d
|
|
257
|
+
WHERE d.source_id = ? AND d.kind = 'returns'`,
|
|
258
|
+
),
|
|
259
|
+
getFlowsTo: db.prepare(
|
|
260
|
+
`SELECT d.expression FROM dataflow d
|
|
261
|
+
WHERE d.target_id = ? AND d.kind = 'flows_to'
|
|
262
|
+
ORDER BY d.param_index`,
|
|
263
|
+
),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
appendReturnMessages(messages, nodeByNameFile, stmts);
|
|
267
|
+
annotateCallParams(messages, nodeByNameFile, stmts);
|
|
268
|
+
}
|
|
269
|
+
|
|
240
270
|
interface Participant {
|
|
241
271
|
id: string;
|
|
242
272
|
label: string;
|