Skip to content

Commit 61785f7

Browse files
feat: add git co-change analysis (backlog #9)
Analyze git history to surface files that historically change together, using Jaccard similarity coefficients. Results are stored in the DB and integrated into diff-impact to catch temporal coupling the static graph misses. - New src/cochange.js module with scan, compute, analyze, and query functions - DB migration v5: co_changes + co_change_meta tables - CLI: `codegraph co-change [file]` with --analyze, --since, --min-support, etc. - diff-impact now shows historicallyCoupled files when co-change data exists - MCP: new co_changes tool for AI agent access - 19 new tests covering pure logic, DB integration, and real git repos Impact: 13 functions changed, 9 affected
1 parent 0ca7560 commit 61785f7

File tree

9 files changed

+936
-3
lines changed

9 files changed

+936
-3
lines changed

src/cli.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,68 @@ program
562562
});
563563
});
564564

565+
program
566+
.command('co-change [file]')
567+
.description(
568+
'Analyze git history for files that change together. Use --analyze to scan, or query existing data.',
569+
)
570+
.option('--analyze', 'Scan git history and populate co-change data')
571+
.option('--since <date>', 'Git date for history window (default: "1 year ago")')
572+
.option('--min-support <n>', 'Minimum co-occurrence count (default: 3)')
573+
.option('--min-jaccard <n>', 'Minimum Jaccard similarity 0-1 (default: 0.3)')
574+
.option('--full', 'Force full re-scan (ignore incremental state)')
575+
.option('-n, --limit <n>', 'Max results', '20')
576+
.option('-d, --db <path>', 'Path to graph.db')
577+
.option('-T, --no-tests', 'Exclude test/spec files')
578+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
579+
.option('-j, --json', 'Output as JSON')
580+
.action(async (file, opts) => {
581+
const { analyzeCoChanges, coChangeData, coChangeTopData, formatCoChange, formatCoChangeTop } =
582+
await import('./cochange.js');
583+
584+
if (opts.analyze) {
585+
const result = analyzeCoChanges(opts.db, {
586+
since: opts.since || config.coChange?.since,
587+
minSupport: opts.minSupport ? parseInt(opts.minSupport, 10) : config.coChange?.minSupport,
588+
maxFilesPerCommit: config.coChange?.maxFilesPerCommit,
589+
full: opts.full,
590+
});
591+
if (opts.json) {
592+
console.log(JSON.stringify(result, null, 2));
593+
} else if (result.error) {
594+
console.error(result.error);
595+
process.exit(1);
596+
} else {
597+
console.log(
598+
`\nCo-change analysis complete: ${result.pairsFound} pairs from ${result.commitsScanned} commits (since: ${result.since})\n`,
599+
);
600+
}
601+
return;
602+
}
603+
604+
const queryOpts = {
605+
limit: parseInt(opts.limit, 10),
606+
minJaccard: opts.minJaccard ? parseFloat(opts.minJaccard) : config.coChange?.minJaccard,
607+
noTests: resolveNoTests(opts),
608+
};
609+
610+
if (file) {
611+
const data = coChangeData(file, opts.db, queryOpts);
612+
if (opts.json) {
613+
console.log(JSON.stringify(data, null, 2));
614+
} else {
615+
console.log(formatCoChange(data));
616+
}
617+
} else {
618+
const data = coChangeTopData(opts.db, queryOpts);
619+
if (opts.json) {
620+
console.log(JSON.stringify(data, null, 2));
621+
} else {
622+
console.log(formatCoChangeTop(data));
623+
}
624+
}
625+
});
626+
565627
program
566628
.command('watch [dir]')
567629
.description('Watch project for file changes and incrementally update the graph')

0 commit comments

Comments
 (0)