@optave/codegraph 3.1.5 → 3.2.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/README.md +3 -2
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +252 -258
- package/src/ast-analysis/shared.js +0 -12
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +2 -1
- package/src/cli/commands/audit.js +2 -1
- package/src/cli/commands/batch.js +2 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/cfg.js +2 -1
- package/src/cli/commands/check.js +20 -23
- package/src/cli/commands/children.js +6 -1
- package/src/cli/commands/complexity.js +2 -1
- package/src/cli/commands/context.js +6 -1
- package/src/cli/commands/dataflow.js +2 -1
- package/src/cli/commands/deps.js +8 -3
- package/src/cli/commands/flow.js +2 -1
- package/src/cli/commands/fn-impact.js +6 -1
- package/src/cli/commands/owners.js +4 -2
- package/src/cli/commands/query.js +6 -1
- package/src/cli/commands/roles.js +2 -1
- package/src/cli/commands/search.js +8 -2
- package/src/cli/commands/sequence.js +2 -1
- package/src/cli/commands/triage.js +38 -27
- package/src/db/connection.js +18 -12
- package/src/db/migrations.js +41 -64
- package/src/db/query-builder.js +60 -4
- package/src/db/repository/in-memory-repository.js +27 -16
- package/src/db/repository/nodes.js +8 -10
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +174 -190
- package/src/domain/analysis/dependencies.js +200 -146
- package/src/domain/analysis/exports.js +3 -2
- package/src/domain/analysis/impact.js +267 -152
- package/src/domain/analysis/module-map.js +247 -221
- package/src/domain/analysis/roles.js +8 -5
- package/src/domain/analysis/symbol-lookup.js +7 -5
- package/src/domain/graph/builder/helpers.js +1 -1
- package/src/domain/graph/builder/incremental.js +116 -90
- package/src/domain/graph/builder/pipeline.js +106 -80
- package/src/domain/graph/builder/stages/build-edges.js +318 -239
- package/src/domain/graph/builder/stages/detect-changes.js +198 -177
- package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
- package/src/domain/graph/watcher.js +2 -2
- package/src/domain/parser.js +20 -11
- package/src/domain/queries.js +1 -0
- package/src/domain/search/search/filters.js +9 -5
- package/src/domain/search/search/keyword.js +12 -5
- package/src/domain/search/search/prepare.js +13 -5
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +274 -304
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/features/ast.js +5 -3
- package/src/features/audit.js +4 -2
- package/src/features/boundaries.js +98 -83
- package/src/features/cfg.js +134 -143
- package/src/features/communities.js +68 -53
- package/src/features/complexity.js +143 -132
- package/src/features/dataflow.js +146 -149
- package/src/features/export.js +3 -3
- package/src/features/graph-enrichment.js +2 -2
- package/src/features/manifesto.js +9 -6
- package/src/features/owners.js +4 -3
- package/src/features/sequence.js +152 -141
- package/src/features/shared/find-nodes.js +31 -0
- package/src/features/structure.js +130 -99
- package/src/features/triage.js +83 -68
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.js +1 -0
- package/src/mcp/server.js +65 -56
- package/src/mcp/tool-registry.js +13 -0
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/index.js +2 -0
- package/src/presentation/brief.js +51 -0
- package/src/presentation/queries-cli/exports.js +21 -14
- package/src/presentation/queries-cli/impact.js +55 -39
- package/src/presentation/queries-cli/inspect.js +184 -189
- package/src/presentation/queries-cli/overview.js +57 -58
- package/src/presentation/queries-cli/path.js +36 -29
- package/src/presentation/table.js +0 -8
- package/src/shared/generators.js +7 -3
- package/src/shared/kinds.js +1 -1
|
@@ -13,6 +13,7 @@ import { evaluateBoundaries } from '../../features/boundaries.js';
|
|
|
13
13
|
import { coChangeForFiles } from '../../features/cochange.js';
|
|
14
14
|
import { ownersForFiles } from '../../features/owners.js';
|
|
15
15
|
import { loadConfig } from '../../infrastructure/config.js';
|
|
16
|
+
import { debug } from '../../infrastructure/logger.js';
|
|
16
17
|
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
17
18
|
import { normalizeSymbol } from '../../shared/normalize.js';
|
|
18
19
|
import { paginateResult } from '../../shared/paginate.js';
|
|
@@ -133,6 +134,251 @@ export function fnImpactData(name, customDbPath, opts = {}) {
|
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
// ─── diffImpactData helpers ─────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Walk up from repoRoot until a .git directory is found.
|
|
141
|
+
* Returns true if a git root exists, false otherwise.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} repoRoot
|
|
144
|
+
* @returns {boolean}
|
|
145
|
+
*/
|
|
146
|
+
function findGitRoot(repoRoot) {
|
|
147
|
+
let checkDir = repoRoot;
|
|
148
|
+
while (checkDir) {
|
|
149
|
+
if (fs.existsSync(path.join(checkDir, '.git'))) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
const parent = path.dirname(checkDir);
|
|
153
|
+
if (parent === checkDir) break;
|
|
154
|
+
checkDir = parent;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Execute git diff and return the raw output string.
|
|
161
|
+
* Returns `{ output: string }` on success or `{ error: string }` on failure.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} repoRoot
|
|
164
|
+
* @param {{ staged?: boolean, ref?: string }} opts
|
|
165
|
+
* @returns {{ output: string } | { error: string }}
|
|
166
|
+
*/
|
|
167
|
+
function runGitDiff(repoRoot, opts) {
|
|
168
|
+
try {
|
|
169
|
+
const args = opts.staged
|
|
170
|
+
? ['diff', '--cached', '--unified=0', '--no-color']
|
|
171
|
+
: ['diff', opts.ref || 'HEAD', '--unified=0', '--no-color'];
|
|
172
|
+
const output = execFileSync('git', args, {
|
|
173
|
+
cwd: repoRoot,
|
|
174
|
+
encoding: 'utf-8',
|
|
175
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
176
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
177
|
+
});
|
|
178
|
+
return { output };
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return { error: `Failed to run git diff: ${e.message}` };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Parse raw git diff output into a changedRanges map and newFiles set.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} diffOutput
|
|
188
|
+
* @returns {{ changedRanges: Map<string, Array<{start: number, end: number}>>, newFiles: Set<string> }}
|
|
189
|
+
*/
|
|
190
|
+
function parseGitDiff(diffOutput) {
|
|
191
|
+
const changedRanges = new Map();
|
|
192
|
+
const newFiles = new Set();
|
|
193
|
+
let currentFile = null;
|
|
194
|
+
let prevIsDevNull = false;
|
|
195
|
+
|
|
196
|
+
for (const line of diffOutput.split('\n')) {
|
|
197
|
+
if (line.startsWith('--- /dev/null')) {
|
|
198
|
+
prevIsDevNull = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (line.startsWith('--- ')) {
|
|
202
|
+
prevIsDevNull = false;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const fileMatch = line.match(/^\+\+\+ b\/(.+)/);
|
|
206
|
+
if (fileMatch) {
|
|
207
|
+
currentFile = fileMatch[1];
|
|
208
|
+
if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []);
|
|
209
|
+
if (prevIsDevNull) newFiles.add(currentFile);
|
|
210
|
+
prevIsDevNull = false;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const hunkMatch = line.match(/^@@ .+ \+(\d+)(?:,(\d+))? @@/);
|
|
214
|
+
if (hunkMatch && currentFile) {
|
|
215
|
+
const start = parseInt(hunkMatch[1], 10);
|
|
216
|
+
const count = parseInt(hunkMatch[2] || '1', 10);
|
|
217
|
+
changedRanges.get(currentFile).push({ start, end: start + count - 1 });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { changedRanges, newFiles };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Find all function/method/class nodes whose line ranges overlap any changed range.
|
|
226
|
+
*
|
|
227
|
+
* @param {import('better-sqlite3').Database} db
|
|
228
|
+
* @param {Map<string, Array<{start: number, end: number}>} changedRanges
|
|
229
|
+
* @param {boolean} noTests
|
|
230
|
+
* @returns {Array<object>}
|
|
231
|
+
*/
|
|
232
|
+
function findAffectedFunctions(db, changedRanges, noTests) {
|
|
233
|
+
const affectedFunctions = [];
|
|
234
|
+
for (const [file, ranges] of changedRanges) {
|
|
235
|
+
if (noTests && isTestFile(file)) continue;
|
|
236
|
+
const defs = db
|
|
237
|
+
.prepare(
|
|
238
|
+
`SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`,
|
|
239
|
+
)
|
|
240
|
+
.all(file);
|
|
241
|
+
for (let i = 0; i < defs.length; i++) {
|
|
242
|
+
const def = defs[i];
|
|
243
|
+
const endLine = def.end_line || (defs[i + 1] ? defs[i + 1].line - 1 : 999999);
|
|
244
|
+
for (const range of ranges) {
|
|
245
|
+
if (range.start <= endLine && range.end >= def.line) {
|
|
246
|
+
affectedFunctions.push(def);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return affectedFunctions;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Run BFS per affected function, collecting per-function results and the full affected set.
|
|
257
|
+
*
|
|
258
|
+
* @param {import('better-sqlite3').Database} db
|
|
259
|
+
* @param {Array<object>} affectedFunctions
|
|
260
|
+
* @param {boolean} noTests
|
|
261
|
+
* @param {number} maxDepth
|
|
262
|
+
* @returns {{ functionResults: Array<object>, allAffected: Set<string> }}
|
|
263
|
+
*/
|
|
264
|
+
function buildFunctionImpactResults(db, affectedFunctions, noTests, maxDepth) {
|
|
265
|
+
const allAffected = new Set();
|
|
266
|
+
const functionResults = affectedFunctions.map((fn) => {
|
|
267
|
+
const edges = [];
|
|
268
|
+
const idToKey = new Map();
|
|
269
|
+
idToKey.set(fn.id, `${fn.file}::${fn.name}:${fn.line}`);
|
|
270
|
+
|
|
271
|
+
const { levels, totalDependents } = bfsTransitiveCallers(db, fn.id, {
|
|
272
|
+
noTests,
|
|
273
|
+
maxDepth,
|
|
274
|
+
onVisit(c, parentId) {
|
|
275
|
+
allAffected.add(`${c.file}:${c.name}`);
|
|
276
|
+
const callerKey = `${c.file}::${c.name}:${c.line}`;
|
|
277
|
+
idToKey.set(c.id, callerKey);
|
|
278
|
+
edges.push({ from: idToKey.get(parentId), to: callerKey });
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
name: fn.name,
|
|
284
|
+
kind: fn.kind,
|
|
285
|
+
file: fn.file,
|
|
286
|
+
line: fn.line,
|
|
287
|
+
transitiveCallers: totalDependents,
|
|
288
|
+
levels,
|
|
289
|
+
edges,
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return { functionResults, allAffected };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Look up historically co-changed files for the set of changed files.
|
|
298
|
+
* Returns an empty array if the co_changes table is unavailable.
|
|
299
|
+
*
|
|
300
|
+
* @param {import('better-sqlite3').Database} db
|
|
301
|
+
* @param {Map<string, any>} changedRanges
|
|
302
|
+
* @param {Set<string>} affectedFiles
|
|
303
|
+
* @param {boolean} noTests
|
|
304
|
+
* @returns {Array<object>}
|
|
305
|
+
*/
|
|
306
|
+
function lookupCoChanges(db, changedRanges, affectedFiles, noTests) {
|
|
307
|
+
try {
|
|
308
|
+
db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
|
|
309
|
+
const changedFilesList = [...changedRanges.keys()];
|
|
310
|
+
const coResults = coChangeForFiles(changedFilesList, db, {
|
|
311
|
+
minJaccard: 0.3,
|
|
312
|
+
limit: 20,
|
|
313
|
+
noTests,
|
|
314
|
+
});
|
|
315
|
+
return coResults.filter((r) => !affectedFiles.has(r.file));
|
|
316
|
+
} catch (e) {
|
|
317
|
+
debug(`co_changes lookup skipped: ${e.message}`);
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Look up CODEOWNERS for changed and affected files.
|
|
324
|
+
* Returns null if no owners are found or lookup fails.
|
|
325
|
+
*
|
|
326
|
+
* @param {Map<string, any>} changedRanges
|
|
327
|
+
* @param {Set<string>} affectedFiles
|
|
328
|
+
* @param {string} repoRoot
|
|
329
|
+
* @returns {{ owners: object, affectedOwners: Array<string>, suggestedReviewers: Array<string> } | null}
|
|
330
|
+
*/
|
|
331
|
+
function lookupOwnership(changedRanges, affectedFiles, repoRoot) {
|
|
332
|
+
try {
|
|
333
|
+
const allFilePaths = [...new Set([...changedRanges.keys(), ...affectedFiles])];
|
|
334
|
+
const ownerResult = ownersForFiles(allFilePaths, repoRoot);
|
|
335
|
+
if (ownerResult.affectedOwners.length > 0) {
|
|
336
|
+
return {
|
|
337
|
+
owners: Object.fromEntries(ownerResult.owners),
|
|
338
|
+
affectedOwners: ownerResult.affectedOwners,
|
|
339
|
+
suggestedReviewers: ownerResult.suggestedReviewers,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
} catch (e) {
|
|
344
|
+
debug(`CODEOWNERS lookup skipped: ${e.message}`);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check manifesto boundary violations scoped to the changed files.
|
|
351
|
+
* Returns `{ boundaryViolations, boundaryViolationCount }`.
|
|
352
|
+
*
|
|
353
|
+
* @param {import('better-sqlite3').Database} db
|
|
354
|
+
* @param {Map<string, any>} changedRanges
|
|
355
|
+
* @param {boolean} noTests
|
|
356
|
+
* @param {object} opts — full diffImpactData opts (may contain `opts.config`)
|
|
357
|
+
* @param {string} repoRoot
|
|
358
|
+
* @returns {{ boundaryViolations: Array<object>, boundaryViolationCount: number }}
|
|
359
|
+
*/
|
|
360
|
+
function checkBoundaryViolations(db, changedRanges, noTests, opts, repoRoot) {
|
|
361
|
+
try {
|
|
362
|
+
const cfg = opts.config || loadConfig(repoRoot);
|
|
363
|
+
const boundaryConfig = cfg.manifesto?.boundaries;
|
|
364
|
+
if (boundaryConfig) {
|
|
365
|
+
const result = evaluateBoundaries(db, boundaryConfig, {
|
|
366
|
+
scopeFiles: [...changedRanges.keys()],
|
|
367
|
+
noTests,
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
boundaryViolations: result.violations,
|
|
371
|
+
boundaryViolationCount: result.violationCount,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
} catch (e) {
|
|
375
|
+
debug(`boundary check skipped: ${e.message}`);
|
|
376
|
+
}
|
|
377
|
+
return { boundaryViolations: [], boundaryViolationCount: 0 };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ─── diffImpactData ─────────────────────────────────────────────────────
|
|
381
|
+
|
|
136
382
|
/**
|
|
137
383
|
* Fix #2: Shell injection vulnerability.
|
|
138
384
|
* Uses execFileSync instead of execSync to prevent shell interpretation of user input.
|
|
@@ -146,38 +392,14 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
146
392
|
const dbPath = findDbPath(customDbPath);
|
|
147
393
|
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
148
394
|
|
|
149
|
-
|
|
150
|
-
let checkDir = repoRoot;
|
|
151
|
-
let isGitRepo = false;
|
|
152
|
-
while (checkDir) {
|
|
153
|
-
if (fs.existsSync(path.join(checkDir, '.git'))) {
|
|
154
|
-
isGitRepo = true;
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
const parent = path.dirname(checkDir);
|
|
158
|
-
if (parent === checkDir) break;
|
|
159
|
-
checkDir = parent;
|
|
160
|
-
}
|
|
161
|
-
if (!isGitRepo) {
|
|
395
|
+
if (!findGitRoot(repoRoot)) {
|
|
162
396
|
return { error: `Not a git repository: ${repoRoot}` };
|
|
163
397
|
}
|
|
164
398
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const args = opts.staged
|
|
168
|
-
? ['diff', '--cached', '--unified=0', '--no-color']
|
|
169
|
-
: ['diff', opts.ref || 'HEAD', '--unified=0', '--no-color'];
|
|
170
|
-
diffOutput = execFileSync('git', args, {
|
|
171
|
-
cwd: repoRoot,
|
|
172
|
-
encoding: 'utf-8',
|
|
173
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
174
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
175
|
-
});
|
|
176
|
-
} catch (e) {
|
|
177
|
-
return { error: `Failed to run git diff: ${e.message}` };
|
|
178
|
-
}
|
|
399
|
+
const gitResult = runGitDiff(repoRoot, opts);
|
|
400
|
+
if (gitResult.error) return { error: gitResult.error };
|
|
179
401
|
|
|
180
|
-
if (!
|
|
402
|
+
if (!gitResult.output.trim()) {
|
|
181
403
|
return {
|
|
182
404
|
changedFiles: 0,
|
|
183
405
|
newFiles: [],
|
|
@@ -187,34 +409,7 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
187
409
|
};
|
|
188
410
|
}
|
|
189
411
|
|
|
190
|
-
const changedRanges =
|
|
191
|
-
const newFiles = new Set();
|
|
192
|
-
let currentFile = null;
|
|
193
|
-
let prevIsDevNull = false;
|
|
194
|
-
for (const line of diffOutput.split('\n')) {
|
|
195
|
-
if (line.startsWith('--- /dev/null')) {
|
|
196
|
-
prevIsDevNull = true;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
if (line.startsWith('--- ')) {
|
|
200
|
-
prevIsDevNull = false;
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
const fileMatch = line.match(/^\+\+\+ b\/(.+)/);
|
|
204
|
-
if (fileMatch) {
|
|
205
|
-
currentFile = fileMatch[1];
|
|
206
|
-
if (!changedRanges.has(currentFile)) changedRanges.set(currentFile, []);
|
|
207
|
-
if (prevIsDevNull) newFiles.add(currentFile);
|
|
208
|
-
prevIsDevNull = false;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
const hunkMatch = line.match(/^@@ .+ \+(\d+)(?:,(\d+))? @@/);
|
|
212
|
-
if (hunkMatch && currentFile) {
|
|
213
|
-
const start = parseInt(hunkMatch[1], 10);
|
|
214
|
-
const count = parseInt(hunkMatch[2] || '1', 10);
|
|
215
|
-
changedRanges.get(currentFile).push({ start, end: start + count - 1 });
|
|
216
|
-
}
|
|
217
|
-
}
|
|
412
|
+
const { changedRanges, newFiles } = parseGitDiff(gitResult.output);
|
|
218
413
|
|
|
219
414
|
if (changedRanges.size === 0) {
|
|
220
415
|
return {
|
|
@@ -226,106 +421,26 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
226
421
|
};
|
|
227
422
|
}
|
|
228
423
|
|
|
229
|
-
const affectedFunctions =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
.all(file);
|
|
237
|
-
for (let i = 0; i < defs.length; i++) {
|
|
238
|
-
const def = defs[i];
|
|
239
|
-
const endLine = def.end_line || (defs[i + 1] ? defs[i + 1].line - 1 : 999999);
|
|
240
|
-
for (const range of ranges) {
|
|
241
|
-
if (range.start <= endLine && range.end >= def.line) {
|
|
242
|
-
affectedFunctions.push(def);
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const allAffected = new Set();
|
|
250
|
-
const functionResults = affectedFunctions.map((fn) => {
|
|
251
|
-
const edges = [];
|
|
252
|
-
const idToKey = new Map();
|
|
253
|
-
idToKey.set(fn.id, `${fn.file}::${fn.name}:${fn.line}`);
|
|
254
|
-
|
|
255
|
-
const { levels, totalDependents } = bfsTransitiveCallers(db, fn.id, {
|
|
256
|
-
noTests,
|
|
257
|
-
maxDepth,
|
|
258
|
-
onVisit(c, parentId) {
|
|
259
|
-
allAffected.add(`${c.file}:${c.name}`);
|
|
260
|
-
const callerKey = `${c.file}::${c.name}:${c.line}`;
|
|
261
|
-
idToKey.set(c.id, callerKey);
|
|
262
|
-
edges.push({ from: idToKey.get(parentId), to: callerKey });
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
name: fn.name,
|
|
268
|
-
kind: fn.kind,
|
|
269
|
-
file: fn.file,
|
|
270
|
-
line: fn.line,
|
|
271
|
-
transitiveCallers: totalDependents,
|
|
272
|
-
levels,
|
|
273
|
-
edges,
|
|
274
|
-
};
|
|
275
|
-
});
|
|
424
|
+
const affectedFunctions = findAffectedFunctions(db, changedRanges, noTests);
|
|
425
|
+
const { functionResults, allAffected } = buildFunctionImpactResults(
|
|
426
|
+
db,
|
|
427
|
+
affectedFunctions,
|
|
428
|
+
noTests,
|
|
429
|
+
maxDepth,
|
|
430
|
+
);
|
|
276
431
|
|
|
277
432
|
const affectedFiles = new Set();
|
|
278
433
|
for (const key of allAffected) affectedFiles.add(key.split(':')[0]);
|
|
279
434
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
db
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
});
|
|
290
|
-
// Exclude files already found via static analysis
|
|
291
|
-
historicallyCoupled = coResults.filter((r) => !affectedFiles.has(r.file));
|
|
292
|
-
} catch {
|
|
293
|
-
/* co_changes table doesn't exist — skip silently */
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Look up CODEOWNERS for changed + affected files
|
|
297
|
-
let ownership = null;
|
|
298
|
-
try {
|
|
299
|
-
const allFilePaths = [...new Set([...changedRanges.keys(), ...affectedFiles])];
|
|
300
|
-
const ownerResult = ownersForFiles(allFilePaths, repoRoot);
|
|
301
|
-
if (ownerResult.affectedOwners.length > 0) {
|
|
302
|
-
ownership = {
|
|
303
|
-
owners: Object.fromEntries(ownerResult.owners),
|
|
304
|
-
affectedOwners: ownerResult.affectedOwners,
|
|
305
|
-
suggestedReviewers: ownerResult.suggestedReviewers,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
} catch {
|
|
309
|
-
/* CODEOWNERS missing or unreadable — skip silently */
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Check boundary violations scoped to changed files
|
|
313
|
-
let boundaryViolations = [];
|
|
314
|
-
let boundaryViolationCount = 0;
|
|
315
|
-
try {
|
|
316
|
-
const cfg = opts.config || loadConfig(repoRoot);
|
|
317
|
-
const boundaryConfig = cfg.manifesto?.boundaries;
|
|
318
|
-
if (boundaryConfig) {
|
|
319
|
-
const result = evaluateBoundaries(db, boundaryConfig, {
|
|
320
|
-
scopeFiles: [...changedRanges.keys()],
|
|
321
|
-
noTests,
|
|
322
|
-
});
|
|
323
|
-
boundaryViolations = result.violations;
|
|
324
|
-
boundaryViolationCount = result.violationCount;
|
|
325
|
-
}
|
|
326
|
-
} catch {
|
|
327
|
-
/* boundary check failed — skip silently */
|
|
328
|
-
}
|
|
435
|
+
const historicallyCoupled = lookupCoChanges(db, changedRanges, affectedFiles, noTests);
|
|
436
|
+
const ownership = lookupOwnership(changedRanges, affectedFiles, repoRoot);
|
|
437
|
+
const { boundaryViolations, boundaryViolationCount } = checkBoundaryViolations(
|
|
438
|
+
db,
|
|
439
|
+
changedRanges,
|
|
440
|
+
noTests,
|
|
441
|
+
opts,
|
|
442
|
+
repoRoot,
|
|
443
|
+
);
|
|
329
444
|
|
|
330
445
|
const base = {
|
|
331
446
|
changedFiles: changedRanges.size,
|