@tienne/gestalt 0.76.1 → 0.77.1

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.
Files changed (57) hide show
  1. package/CLAUDE.md +5 -3
  2. package/dist/package.json +4 -4
  3. package/dist/plugin/role-agents/_shared/references/README.md +13 -1
  4. package/dist/plugin/role-agents/_shared/references/ai-tell-quick-rules.md +2 -2
  5. package/dist/plugin/role-agents/code-review-responder/AGENT.md +1 -1
  6. package/dist/plugin/role-agents/code-review-writer/AGENT.md +3 -2
  7. package/dist/plugin/role-agents/explainer/AGENT.md +1 -1
  8. package/dist/plugin/role-agents/explainer/references/audience.md +1 -1
  9. package/dist/plugin/skills/blast-radius/SKILL.md +58 -9
  10. package/dist/plugin/skills/build-graph/SKILL.md +28 -3
  11. package/dist/plugin/skills/diff-radius/SKILL.md +44 -6
  12. package/dist/src/code-graph/blast-radius.d.ts +3 -3
  13. package/dist/src/code-graph/blast-radius.d.ts.map +1 -1
  14. package/dist/src/code-graph/blast-radius.js +111 -6
  15. package/dist/src/code-graph/blast-radius.js.map +1 -1
  16. package/dist/src/code-graph/cochange.d.ts +117 -0
  17. package/dist/src/code-graph/cochange.d.ts.map +1 -0
  18. package/dist/src/code-graph/cochange.js +444 -0
  19. package/dist/src/code-graph/cochange.js.map +1 -0
  20. package/dist/src/code-graph/engine.d.ts +13 -1
  21. package/dist/src/code-graph/engine.d.ts.map +1 -1
  22. package/dist/src/code-graph/engine.js +58 -4
  23. package/dist/src/code-graph/engine.js.map +1 -1
  24. package/dist/src/code-graph/index.d.ts +5 -2
  25. package/dist/src/code-graph/index.d.ts.map +1 -1
  26. package/dist/src/code-graph/index.js +2 -1
  27. package/dist/src/code-graph/index.js.map +1 -1
  28. package/dist/src/code-graph/storage.d.ts +93 -1
  29. package/dist/src/code-graph/storage.d.ts.map +1 -1
  30. package/dist/src/code-graph/storage.js +213 -0
  31. package/dist/src/code-graph/storage.js.map +1 -1
  32. package/dist/src/code-graph/types.d.ts +141 -0
  33. package/dist/src/code-graph/types.d.ts.map +1 -1
  34. package/dist/src/mcp/schemas.d.ts +12 -3
  35. package/dist/src/mcp/schemas.d.ts.map +1 -1
  36. package/dist/src/mcp/schemas.js +26 -3
  37. package/dist/src/mcp/schemas.js.map +1 -1
  38. package/dist/src/mcp/server.d.ts.map +1 -1
  39. package/dist/src/mcp/server.js +10 -4
  40. package/dist/src/mcp/server.js.map +1 -1
  41. package/dist/src/mcp/tools/code-graph-passthrough.d.ts +4 -1
  42. package/dist/src/mcp/tools/code-graph-passthrough.d.ts.map +1 -1
  43. package/dist/src/mcp/tools/code-graph-passthrough.js +23 -0
  44. package/dist/src/mcp/tools/code-graph-passthrough.js.map +1 -1
  45. package/package.json +4 -4
  46. package/plugin/.codex-plugin/plugin.json +1 -1
  47. package/plugin/.mcp.json +1 -1
  48. package/plugin/mcp.json +1 -1
  49. package/plugin/role-agents/_shared/references/README.md +13 -1
  50. package/plugin/role-agents/_shared/references/ai-tell-quick-rules.md +2 -2
  51. package/plugin/role-agents/code-review-responder/AGENT.md +1 -1
  52. package/plugin/role-agents/code-review-writer/AGENT.md +3 -2
  53. package/plugin/role-agents/explainer/AGENT.md +1 -1
  54. package/plugin/role-agents/explainer/references/audience.md +1 -1
  55. package/plugin/skills/blast-radius/SKILL.md +58 -9
  56. package/plugin/skills/build-graph/SKILL.md +28 -3
  57. package/plugin/skills/diff-radius/SKILL.md +44 -6
@@ -0,0 +1,444 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { logger } from '../core/logger.js';
5
+ import { MAX_MATCHED_ROWS } from './storage.js';
6
+ /**
7
+ * 한 커밋이 이 개수를 넘게 건드리면 신호에서 뺀다.
8
+ * 릴리즈 커밋이나 일괄 포맷팅이 모든 파일을 서로 연결해버리는 걸 막는다.
9
+ */
10
+ export const MAX_FILES_PER_COMMIT = 20;
11
+ /** 파일이 하나뿐인 커밋은 페어를 만들 수 없다 */
12
+ export const MIN_FILES_PER_COMMIT = 2;
13
+ /** 동시 등장이 이 미만인 페어는 우연으로 본다 */
14
+ export const MIN_PAIR_COUNT = 3;
15
+ export const DEFAULT_MIN_CONFIDENCE = 0.3;
16
+ export const DEFAULT_NEIGHBOR_LIMIT = 30;
17
+ export const DEFAULT_PAIR_LIMIT = 50;
18
+ const SHA_RE = /^[0-9a-f]{40}$/;
19
+ /**
20
+ * maxBuffer를 반드시 명시한다. 기본 1MB를 넘으면 ENOBUFS로 출력이 잘린 채
21
+ * 예외가 나는데, 그걸 삼키면 부분 수집이 성공으로 둔갑한다.
22
+ */
23
+ function defaultRunGit(root, args) {
24
+ return execFileSync('git', ['-c', 'core.fsmonitor=', '-c', 'core.quotepath=false', ...args], {
25
+ cwd: root,
26
+ encoding: 'utf-8',
27
+ maxBuffer: 64 * 1024 * 1024,
28
+ stdio: ['pipe', 'pipe', 'pipe'],
29
+ });
30
+ }
31
+ export function pairKey(a, b) {
32
+ return a < b ? `${a}\t${b}` : `${b}\t${a}`;
33
+ }
34
+ /** `git log --format=%H --name-only --no-merges` 출력을 커밋 단위로 쪼갠다 */
35
+ export function parseGitLog(raw) {
36
+ const commits = [];
37
+ let current = null;
38
+ for (const line of raw.split('\n')) {
39
+ const trimmed = line.trim();
40
+ if (trimmed.length === 0)
41
+ continue;
42
+ if (SHA_RE.test(trimmed)) {
43
+ current = { sha: trimmed, files: [] };
44
+ commits.push(current);
45
+ continue;
46
+ }
47
+ if (current)
48
+ current.files.push(trimmed);
49
+ }
50
+ return commits;
51
+ }
52
+ export function countPairs(commits) {
53
+ const pairs = new Map();
54
+ const solos = new Map();
55
+ let commitsUsed = 0;
56
+ for (const commit of commits) {
57
+ const uniq = [...new Set(commit.files)];
58
+ if (uniq.length < MIN_FILES_PER_COMMIT || uniq.length > MAX_FILES_PER_COMMIT)
59
+ continue;
60
+ commitsUsed++;
61
+ for (const file of uniq) {
62
+ solos.set(file, (solos.get(file) ?? 0) + 1);
63
+ }
64
+ for (let i = 0; i < uniq.length; i++) {
65
+ for (let j = i + 1; j < uniq.length; j++) {
66
+ const key = pairKey(uniq[i], uniq[j]);
67
+ pairs.set(key, (pairs.get(key) ?? 0) + 1);
68
+ }
69
+ }
70
+ }
71
+ return { pairs, solos, commitsUsed, commitsScanned: commits.length };
72
+ }
73
+ /** 보고 정밀도. 원시 카운트만 저장하므로 언제든 다시 계산할 수 있다 */
74
+ export const CONFIDENCE_DECIMALS = 2;
75
+ export const LIFT_DECIMALS = 1;
76
+ function round(value, decimals) {
77
+ return Number(value.toFixed(decimals));
78
+ }
79
+ /**
80
+ * confidence는 방향별로 다르므로 큰 쪽을 쓴다.
81
+ * lift 분모는 이긴 방향의 상대 파일이 전체 커밋에서 등장하는 비율이다 —
82
+ * 어디에나 끼는 파일일수록 분모가 커져 점수가 눌린다.
83
+ *
84
+ * 반환 직전에 반올림한다. 이 함수가 두 소비 경로(cochange 응답과 blast-radius
85
+ * rankedFiles)가 만나는 유일한 지점이라 여기서 자르면 양쪽이 같은 값을 본다.
86
+ * 랭킹도 보고된 값 그대로 매겨져 "같아 보이는데 순서가 다른" 자리가 없어진다.
87
+ */
88
+ export function scoreNeighbor(pairCount, soloSelf, soloOther, commitsUsed) {
89
+ const confSelf = soloSelf > 0 ? pairCount / soloSelf : 0;
90
+ const confOther = soloOther > 0 ? pairCount / soloOther : 0;
91
+ const confidence = Math.max(confSelf, confOther);
92
+ const counterpartSolo = confSelf >= confOther ? soloOther : soloSelf;
93
+ const expected = commitsUsed > 0 ? counterpartSolo / commitsUsed : 0;
94
+ const lift = expected > 0 ? confidence / expected : 0;
95
+ return {
96
+ confidence: round(confidence, CONFIDENCE_DECIMALS),
97
+ lift: round(lift, LIFT_DECIMALS),
98
+ };
99
+ }
100
+ /** 랭킹 키. 프로토타입이 검증한 정렬이다 */
101
+ export function neighborScore(n) {
102
+ return n.confidence * n.lift;
103
+ }
104
+ /**
105
+ * 점수와 동시등장 다음에 경로를 마지막 열쇠로 둔다. 앞의 둘이 같은 행은 흔한데
106
+ * (카운트가 같으면 점수도 같기 쉽다) 거기서 멈추면 순서가 DB가 돌려준 행
107
+ * 순서에 걸린다. 질의에 `ORDER BY`가 없으므로 그건 보장이 없다.
108
+ */
109
+ export function compareNeighbors(a, b) {
110
+ return (neighborScore(b) - neighborScore(a) ||
111
+ b.pairCount - a.pairCount ||
112
+ a.filePath.localeCompare(b.filePath));
113
+ }
114
+ function isAncestor(runGit, root, ancestor, descendant) {
115
+ try {
116
+ runGit(root, ['merge-base', '--is-ancestor', ancestor, descendant]);
117
+ return true;
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ }
123
+ /**
124
+ * git 이력에서 co-change 카운트를 수집해 DB에 병합한다.
125
+ *
126
+ * repoRoot가 레포 최상위가 아니면 아무것도 하지 않는다. 하위 디렉토리를
127
+ * repoRoot로 넘기면 `git log`가 부모 레포의 전체 이력을 돌려주기 때문이다 —
128
+ * 테스트가 `.gestalt-test/<uuid>`를 repoRoot로 쓰는 자리가 정확히 그 경우다.
129
+ *
130
+ * 수집 불가는 예외가 아니라 undefined로 알린다. 호출부는 그래프 빌드를
131
+ * 계속해야 한다. 0건과 미수집도 구분돼야 한다.
132
+ */
133
+ export function syncCoChange(store, repoRoot, opts = {}) {
134
+ const root = resolve(repoRoot);
135
+ const runGit = opts.runGit ?? defaultRunGit;
136
+ const mode = opts.mode ?? 'full';
137
+ let toplevel;
138
+ try {
139
+ toplevel = runGit(root, ['rev-parse', '--show-toplevel']).trim();
140
+ }
141
+ catch {
142
+ return undefined;
143
+ }
144
+ if (toplevel.length === 0 || resolve(toplevel) !== root) {
145
+ logger.warn('code_graph.cochange_skipped_not_repo_root', {
146
+ module: 'code-graph/cochange',
147
+ repoRoot: root,
148
+ toplevel,
149
+ });
150
+ return undefined;
151
+ }
152
+ let headSha;
153
+ try {
154
+ headSha = runGit(root, ['rev-parse', 'HEAD']).trim();
155
+ }
156
+ catch {
157
+ return undefined;
158
+ }
159
+ if (!SHA_RE.test(headSha))
160
+ return undefined;
161
+ const meta = store.getCoChangeMeta();
162
+ let range = null;
163
+ if (mode === 'incremental' && meta && SHA_RE.test(meta.headSha)) {
164
+ if (meta.headSha === headSha) {
165
+ return {
166
+ pairs: store.countCoChangePairs(),
167
+ commitsUsed: meta.commitsUsed,
168
+ commitsScanned: meta.commitsScanned,
169
+ mode: 'incremental',
170
+ };
171
+ }
172
+ if (isAncestor(runGit, root, meta.headSha, headSha)) {
173
+ range = `${meta.headSha}..HEAD`;
174
+ }
175
+ else {
176
+ // rebase나 amend, shallow clone이면 이전 기준점에 도달할 수 없다.
177
+ // 조용히 넘어가면 카운트가 영영 어긋난 채 남으므로 전량 재수집으로 내린다.
178
+ logger.warn('code_graph.cochange_full_rescan', {
179
+ module: 'code-graph/cochange',
180
+ repoRoot: root,
181
+ reason: 'previous head is unreachable from HEAD',
182
+ previousHead: meta.headSha,
183
+ });
184
+ }
185
+ }
186
+ const args = ['log', '--format=%H', '--name-only', '--no-merges'];
187
+ if (range)
188
+ args.push(range);
189
+ let raw;
190
+ try {
191
+ raw = runGit(root, args);
192
+ }
193
+ catch (e) {
194
+ logger.warn('code_graph.cochange_log_failed', {
195
+ module: 'code-graph/cochange',
196
+ repoRoot: root,
197
+ reason: e instanceof Error ? e.message : String(e),
198
+ });
199
+ return undefined;
200
+ }
201
+ const counts = countPairs(parseGitLog(raw));
202
+ // git은 repo 상대경로를 내고 cg_nodes는 절대경로를 쓴다. 경계에서 한 번에
203
+ // 절대화하지 않으면 조인이 조용히 0건이 된다.
204
+ const pairs = [...counts.pairs.entries()].map(([key, count]) => {
205
+ const tab = key.indexOf('\t');
206
+ const a = resolve(root, key.slice(0, tab));
207
+ const b = resolve(root, key.slice(tab + 1));
208
+ return a < b ? { fileA: a, fileB: b, count } : { fileA: b, fileB: a, count };
209
+ });
210
+ const solos = [...counts.solos.entries()].map(([rel, count]) => ({
211
+ filePath: resolve(root, rel),
212
+ count,
213
+ }));
214
+ store.mergeCoChange({
215
+ pairs,
216
+ solos,
217
+ meta: {
218
+ headSha,
219
+ commitsUsed: counts.commitsUsed,
220
+ commitsScanned: counts.commitsScanned,
221
+ maxFilesPerCommit: MAX_FILES_PER_COMMIT,
222
+ defaultMinPairCount: MIN_PAIR_COUNT,
223
+ },
224
+ reset: range === null,
225
+ });
226
+ const merged = store.getCoChangeMeta();
227
+ return {
228
+ pairs: store.countCoChangePairs(),
229
+ commitsUsed: merged?.commitsUsed ?? counts.commitsUsed,
230
+ commitsScanned: merged?.commitsScanned ?? counts.commitsScanned,
231
+ mode: range ? 'incremental' : 'full',
232
+ };
233
+ }
234
+ function absolutize(root, filePath) {
235
+ return filePath.startsWith('/') ? resolve(filePath) : resolve(root, filePath);
236
+ }
237
+ const NOT_COLLECTED = 'co-change history has not been collected for this repository — run a code graph build at the repo root';
238
+ /**
239
+ * 임계를 통과한 이웃을 전부 점수순으로 세운다. 점수순으로 세운 뒤 말고는
240
+ * 개수를 자르지 않는 게 요점이다 — 자른 뒤 다시 세우면 무엇이 잘렸는지 셀
241
+ * 수 없다. 목록이 전체 상위의 접두사라는 말도 못 한다.
242
+ *
243
+ * 예외가 절대 천장 하나뿐이고 그건 켜졌다는 사실을 `capped`로 내보낸다.
244
+ * 말없이 자르는 자리는 두지 않는다 — 조용히 자르면 하한이 정확한 수로
245
+ * 둔갑한다.
246
+ */
247
+ function neighborsFor(store, absTarget, commitsUsed, minPairCount, minConfidence, exists, maxMatchedRows) {
248
+ const soloSelf = store.getCoChangeSolo(absTarget);
249
+ const out = [];
250
+ // 두 임계는 질의가 건다. 여기서 한 번 더 거르면 같은 규칙이 두 곳에 살아
251
+ // 어긋날 자리가 생긴다. 천장도 질의가 건다 — 여기서 걸면 아래 exists가
252
+ // 이미 다 돈 뒤라 막으려던 stat 팬아웃을 못 막는다.
253
+ const scan = store.getCoChangeNeighbors(absTarget, minPairCount, minConfidence, soloSelf, maxMatchedRows);
254
+ for (const row of scan.rows) {
255
+ // 삭제되거나 이름이 바뀐 파일은 이력에만 남는다. 수집이 아니라 조회에서
256
+ // 거른다 — 수집에서 빼면 그 시절 solo 카운트가 깎여 confidence가 부푼다.
257
+ if (!exists(row.other))
258
+ continue;
259
+ const { confidence, lift } = scoreNeighbor(row.pairCount, soloSelf, row.soloOther, commitsUsed);
260
+ out.push({ filePath: row.other, pairCount: row.pairCount, confidence, lift });
261
+ }
262
+ out.sort(compareNeighbors);
263
+ return { neighbors: out, capped: scan.capped };
264
+ }
265
+ /**
266
+ * 같은 파일이 여러 seed의 이웃으로 거듭 나온다. 감싸지 않으면 그때마다
267
+ * 동기 stat이 한 번씩 더 돈다.
268
+ */
269
+ function memoizeExists(exists) {
270
+ const cache = new Map();
271
+ return (filePath) => {
272
+ const hit = cache.get(filePath);
273
+ if (hit !== undefined)
274
+ return hit;
275
+ const value = exists(filePath);
276
+ cache.set(filePath, value);
277
+ return value;
278
+ };
279
+ }
280
+ export function queryCoChange(store, repoRoot, opts = {}) {
281
+ const root = resolve(repoRoot);
282
+ const exists = memoizeExists(opts.exists ?? existsSync);
283
+ const minPairCount = opts.minPairCount ?? MIN_PAIR_COUNT;
284
+ const minConfidence = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
285
+ const maxMatchedRows = opts.maxMatchedRows ?? MAX_MATCHED_ROWS;
286
+ const meta = store.getCoChangeMeta();
287
+ const pairsInDb = store.countCoChangePairs();
288
+ if (!meta) {
289
+ return {
290
+ target: opts.target,
291
+ neighbors: [],
292
+ pairs: [],
293
+ commitsUsed: 0,
294
+ commitsScanned: 0,
295
+ pairsInDb,
296
+ totalMatched: 0,
297
+ truncated: false,
298
+ matchedCapped: false,
299
+ available: false,
300
+ reason: NOT_COLLECTED,
301
+ };
302
+ }
303
+ if (opts.target) {
304
+ const absTarget = absolutize(root, opts.target);
305
+ const neighborLimit = opts.limit ?? DEFAULT_NEIGHBOR_LIMIT;
306
+ const scan = neighborsFor(store, absTarget, meta.commitsUsed, minPairCount, minConfidence, exists, maxMatchedRows);
307
+ const matched = scan.neighbors;
308
+ const neighbors = matched.slice(0, neighborLimit);
309
+ let reason;
310
+ if (neighbors.length === 0 && pairsInDb > 0) {
311
+ // 이력은 있는데 이 파일만 안 걸린 것과 경로 표기가 어긋난 것은 다르다.
312
+ // 둘 다 여기로 오므로 진단 단서를 남긴다.
313
+ reason =
314
+ `no co-change neighbors for ${absTarget} (${pairsInDb} pair(s) in db). ` +
315
+ `The file may have no shared history, or the path may not match the stored absolute paths.`;
316
+ logger.warn('code_graph.cochange_no_neighbors', {
317
+ module: 'code-graph/cochange',
318
+ repoRoot: root,
319
+ target: absTarget,
320
+ pairsInDb,
321
+ });
322
+ }
323
+ return {
324
+ target: absTarget,
325
+ neighbors,
326
+ pairs: [],
327
+ commitsUsed: meta.commitsUsed,
328
+ commitsScanned: meta.commitsScanned,
329
+ pairsInDb,
330
+ totalMatched: matched.length,
331
+ truncated: matched.length > neighbors.length,
332
+ matchedCapped: scan.capped,
333
+ available: true,
334
+ reason,
335
+ };
336
+ }
337
+ const limit = opts.limit ?? DEFAULT_PAIR_LIMIT;
338
+ const scored = [];
339
+ const scan = store.getCoChangePairs(minPairCount, minConfidence, maxMatchedRows);
340
+ for (const row of scan.rows) {
341
+ if (!exists(row.fileA) || !exists(row.fileB))
342
+ continue;
343
+ const { confidence, lift } = scoreNeighbor(row.pairCount, row.soloA, row.soloB, meta.commitsUsed);
344
+ scored.push({
345
+ fileA: row.fileA,
346
+ fileB: row.fileB,
347
+ pairCount: row.pairCount,
348
+ confidence,
349
+ lift,
350
+ });
351
+ }
352
+ scored.sort((a, b) => compareNeighbors({ ...a, filePath: `${a.fileA}\t${a.fileB}` }, { ...b, filePath: `${b.fileA}\t${b.fileB}` }));
353
+ const pairs = scored.slice(0, limit);
354
+ return {
355
+ neighbors: [],
356
+ pairs,
357
+ commitsUsed: meta.commitsUsed,
358
+ commitsScanned: meta.commitsScanned,
359
+ pairsInDb,
360
+ totalMatched: scored.length,
361
+ truncated: scored.length > pairs.length,
362
+ matchedCapped: scan.capped,
363
+ available: true,
364
+ reason: scored.length === 0 && pairsInDb > 0
365
+ ? `no pair passed the thresholds (minPairCount=${minPairCount}, minConfidence=${minConfidence}) out of ${pairsInDb} pair(s) in db`
366
+ : undefined,
367
+ };
368
+ }
369
+ /**
370
+ * 여러 seed 파일의 이웃을 하나로 합친다. blast-radius가 쓰는 진입점이다.
371
+ * 같은 파일이 여러 seed에 걸리면 점수가 높은 쪽을 남긴다. 합친 목록은
372
+ * `opts.limit`(기본 `DEFAULT_NEIGHBOR_LIMIT`)까지만 돌려준다. 잘렸으면
373
+ * `truncated`로 알린다 — 목록만 보면 그게 전부인 줄 안다.
374
+ *
375
+ * 자르는 자리가 여기 하나뿐이라 돌려준 목록은 임계를 통과한 전체의 상위
376
+ * 접두사다. `totalMatched`도 하한이 아니라 정확한 수다. 단 seed 중 하나라도
377
+ * 질의 천장에 걸리면(`matchedCapped`) 둘 다 성립하지 않는다.
378
+ */
379
+ export function buildCoChangeLookup(store, repoRoot, seeds, opts = {}) {
380
+ const root = resolve(repoRoot);
381
+ // seed 하나가 아니라 seed 전부에 걸쳐 메모이즈한다. 같은 파일이 여러 seed의
382
+ // 이웃으로 거듭 나오는 게 흔한 경우라 seed 안에서만 캐시하면 거의 못 줄인다.
383
+ const exists = memoizeExists(opts.exists ?? existsSync);
384
+ const minPairCount = opts.minPairCount ?? MIN_PAIR_COUNT;
385
+ const minConfidence = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
386
+ const limit = opts.limit ?? DEFAULT_NEIGHBOR_LIMIT;
387
+ const maxMatchedRows = opts.maxMatchedRows ?? MAX_MATCHED_ROWS;
388
+ const meta = store.getCoChangeMeta();
389
+ const pairsInDb = store.countCoChangePairs();
390
+ if (!meta) {
391
+ return {
392
+ available: false,
393
+ reason: NOT_COLLECTED,
394
+ pairsInDb,
395
+ neighbors: [],
396
+ totalMatched: 0,
397
+ truncated: false,
398
+ matchedCapped: false,
399
+ };
400
+ }
401
+ const seedSet = new Set(seeds.map((s) => absolutize(root, s)));
402
+ const best = new Map();
403
+ // 천장은 seed마다 따로 걸린다. 하나라도 걸리면 합친 목록도 전부가 아니므로
404
+ // 여기서 OR로 모은다.
405
+ let matchedCapped = false;
406
+ for (const seed of seedSet) {
407
+ const scan = neighborsFor(store, seed, meta.commitsUsed, minPairCount, minConfidence, exists, maxMatchedRows);
408
+ if (scan.capped)
409
+ matchedCapped = true;
410
+ for (const n of scan.neighbors) {
411
+ if (seedSet.has(n.filePath))
412
+ continue;
413
+ const prev = best.get(n.filePath);
414
+ if (!prev || neighborScore(n) > neighborScore(prev))
415
+ best.set(n.filePath, n);
416
+ }
417
+ }
418
+ // seed마다 임계를 통과한 이웃이 전부 들어오므로 여기서 자르지 않으면
419
+ // seed 개수만큼 부풀어 rankedFiles가 import 신호를 밀어낸다.
420
+ const ranked = [...best.values()].sort(compareNeighbors);
421
+ const neighbors = ranked.slice(0, limit);
422
+ let reason;
423
+ if (neighbors.length === 0 && pairsInDb > 0) {
424
+ reason =
425
+ `no co-change neighbors for the ${seedSet.size} seed file(s) (${pairsInDb} pair(s) in db). ` +
426
+ `Either they share no history, or their paths do not match the stored absolute paths.`;
427
+ logger.warn('code_graph.cochange_no_neighbors', {
428
+ module: 'code-graph/cochange',
429
+ repoRoot: root,
430
+ seeds: seedSet.size,
431
+ pairsInDb,
432
+ });
433
+ }
434
+ return {
435
+ available: true,
436
+ reason,
437
+ pairsInDb,
438
+ neighbors,
439
+ totalMatched: ranked.length,
440
+ truncated: ranked.length > neighbors.length,
441
+ matchedCapped,
442
+ };
443
+ }
444
+ //# sourceMappingURL=cochange.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cochange.js","sourceRoot":"","sources":["../../../src/code-graph/cochange.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAuB,MAAM,cAAc,CAAC;AAWrE;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AACvC,+BAA+B;AAC/B,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AACtC,+BAA+B;AAC/B,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAChC,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAC1C,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AACzC,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,MAAM,MAAM,GAAG,gBAAgB,CAAC;AAiBhC;;;GAGG;AACH,SAAS,aAAa,CAAC,IAAY,EAAE,IAAc;IACjD,OAAO,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,sBAAsB,EAAE,GAAG,IAAI,CAAC,EAAE;QAC3F,GAAG,EAAE,IAAI;QACT,QAAQ,EAAE,OAAO;QACjB,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;QAC3B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;KAChC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,CAAS,EAAE,CAAS;IAC1C,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;AAC7C,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,OAAO,GAAmB,EAAE,CAAC;IACnC,IAAI,OAAO,GAAwB,IAAI,CAAC;IAExC,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACnC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,OAAO,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;YACtC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACtB,SAAS;QACX,CAAC;QACD,IAAI,OAAO;YAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAuB;IAChD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,IAAI,WAAW,GAAG,CAAC,CAAC;IAEpB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACxC,IAAI,IAAI,CAAC,MAAM,GAAG,oBAAoB,IAAI,IAAI,CAAC,MAAM,GAAG,oBAAoB;YAAE,SAAS;QACvF,WAAW,EAAE,CAAC;QAEd,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;YACxB,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9C,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrC,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACzC,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAE,EAAE,IAAI,CAAC,CAAC,CAAE,CAAC,CAAC;gBACxC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;AACvE,CAAC;AAED,4CAA4C;AAC5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AACrC,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC;AAE/B,SAAS,KAAK,CAAC,KAAa,EAAE,QAAgB;IAC5C,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAC3B,SAAiB,EACjB,QAAgB,EAChB,SAAiB,EACjB,WAAmB;IAEnB,MAAM,QAAQ,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5D,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IAEjD,MAAM,eAAe,GAAG,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;IACrE,MAAM,QAAQ,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,MAAM,IAAI,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAEtD,OAAO;QACL,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,mBAAmB,CAAC;QAClD,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,aAAa,CAAC;KACjC,CAAC;AACJ,CAAC;AAED,4BAA4B;AAC5B,MAAM,UAAU,aAAa,CAAC,CAAuC;IACnE,OAAO,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC;AAC/B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,CAA4E,EAC5E,CAA4E;IAE5E,OAAO,CACL,aAAa,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;QACnC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS;QACzB,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CACrC,CAAC;AACJ,CAAC;AASD,SAAS,UAAU,CACjB,MAAiB,EACjB,IAAY,EACZ,QAAgB,EAChB,UAAkB;IAElB,IAAI,CAAC;QACH,MAAM,CAAC,IAAI,EAAE,CAAC,YAAY,EAAE,eAAe,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;QACpE,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,KAAqB,EACrB,QAAgB,EAChB,OAA4B,EAAE;IAE9B,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,aAAa,CAAC;IAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC;IAEjC,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACnE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC;QACxD,MAAM,CAAC,IAAI,CAAC,2CAA2C,EAAE;YACvD,MAAM,EAAE,qBAAqB;YAC7B,QAAQ,EAAE,IAAI;YACd,QAAQ;SACT,CAAC,CAAC;QACH,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IAE5C,MAAM,IAAI,GAAG,KAAK,CAAC,eAAe,EAAE,CAAC;IACrC,IAAI,KAAK,GAAkB,IAAI,CAAC;IAEhC,IAAI,IAAI,KAAK,aAAa,IAAI,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAChE,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YAC7B,OAAO;gBACL,KAAK,EAAE,KAAK,CAAC,kBAAkB,EAAE;gBACjC,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,IAAI,EAAE,aAAa;aACpB,CAAC;QACJ,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;YACpD,KAAK,GAAG,GAAG,IAAI,CAAC,OAAO,QAAQ,CAAC;QAClC,CAAC;aAAM,CAAC;YACN,mDAAmD;YACnD,4CAA4C;YAC5C,MAAM,CAAC,IAAI,CAAC,iCAAiC,EAAE;gBAC7C,MAAM,EAAE,qBAAqB;gBAC7B,QAAQ,EAAE,IAAI;gBACd,MAAM,EAAE,wCAAwC;gBAChD,YAAY,EAAE,IAAI,CAAC,OAAO;aAC3B,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,CAAC,CAAC;IAClE,IAAI,KAAK;QAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAE5B,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE;YAC5C,MAAM,EAAE,qBAAqB;YAC7B,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;SACnD,CAAC,CAAC;QACH,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;IAE5C,mDAAmD;IACnD,4BAA4B;IAC5B,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QAC7D,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QAC3C,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QAC5C,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC;IAC/E,CAAC,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/D,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC;QAC5B,KAAK;KACN,CAAC,CAAC,CAAC;IAEJ,KAAK,CAAC,aAAa,CAAC;QAClB,KAAK;QACL,KAAK;QACL,IAAI,EAAE;YACJ,OAAO;YACP,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,cAAc,EAAE,MAAM,CAAC,cAAc;YACrC,iBAAiB,EAAE,oBAAoB;YACvC,mBAAmB,EAAE,cAAc;SACpC;QACD,KAAK,EAAE,KAAK,KAAK,IAAI;KACtB,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,KAAK,CAAC,eAAe,EAAE,CAAC;IACvC,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,kBAAkB,EAAE;QACjC,WAAW,EAAE,MAAM,EAAE,WAAW,IAAI,MAAM,CAAC,WAAW;QACtD,cAAc,EAAE,MAAM,EAAE,cAAc,IAAI,MAAM,CAAC,cAAc;QAC/D,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM;KACrC,CAAC;AACJ,CAAC;AAkBD,SAAS,UAAU,CAAC,IAAY,EAAE,QAAgB;IAChD,OAAO,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAChF,CAAC;AAED,MAAM,aAAa,GACjB,wGAAwG,CAAC;AAY3G;;;;;;;;GAQG;AACH,SAAS,YAAY,CACnB,KAAqB,EACrB,SAAiB,EACjB,WAAmB,EACnB,YAAoB,EACpB,aAAqB,EACrB,MAAqC,EACrC,cAAsB;IAEtB,MAAM,QAAQ,GAAG,KAAK,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;IAClD,MAAM,GAAG,GAAuB,EAAE,CAAC;IAEnC,6CAA6C;IAC7C,8CAA8C;IAC9C,kCAAkC;IAClC,MAAM,IAAI,GAAG,KAAK,CAAC,oBAAoB,CACrC,SAAS,EACT,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,cAAc,CACf,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,0CAA0C;QAC1C,mDAAmD;QACnD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,SAAS;QACjC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QAChG,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAChF,CAAC;IAED,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC3B,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AACjD,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa,CAAC,MAAqC;IAC1D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAmB,CAAC;IACzC,OAAO,CAAC,QAAQ,EAAE,EAAE;QAClB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAChC,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,GAAG,CAAC;QAClC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC/B,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,KAAqB,EACrB,QAAgB,EAChB,OAA6B,EAAE;IAE/B,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,MAAM,IAAI,UAAU,CAAC,CAAC;IACxD,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,cAAc,CAAC;IACzD,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,sBAAsB,CAAC;IACnE,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,gBAAgB,CAAC;IAE/D,MAAM,IAAI,GAAG,KAAK,CAAC,eAAe,EAAE,CAAC;IACrC,MAAM,SAAS,GAAG,KAAK,CAAC,kBAAkB,EAAE,CAAC;IAE7C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,SAAS,EAAE,EAAE;YACb,KAAK,EAAE,EAAE;YACT,WAAW,EAAE,CAAC;YACd,cAAc,EAAE,CAAC;YACjB,SAAS;YACT,YAAY,EAAE,CAAC;YACf,SAAS,EAAE,KAAK;YAChB,aAAa,EAAE,KAAK;YACpB,SAAS,EAAE,KAAK;YAChB,MAAM,EAAE,aAAa;SACtB,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAChD,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,IAAI,sBAAsB,CAAC;QAC3D,MAAM,IAAI,GAAG,YAAY,CACvB,KAAK,EACL,SAAS,EACT,IAAI,CAAC,WAAW,EAChB,YAAY,EACZ,aAAa,EACb,MAAM,EACN,cAAc,CACf,CAAC;QACF,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC;QAC/B,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;QAElD,IAAI,MAA0B,CAAC;QAC/B,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YAC5C,2CAA2C;YAC3C,0BAA0B;YAC1B,MAAM;gBACJ,8BAA8B,SAAS,KAAK,SAAS,mBAAmB;oBACxE,2FAA2F,CAAC;YAC9F,MAAM,CAAC,IAAI,CAAC,kCAAkC,EAAE;gBAC9C,MAAM,EAAE,qBAAqB;gBAC7B,QAAQ,EAAE,IAAI;gBACd,MAAM,EAAE,SAAS;gBACjB,SAAS;aACV,CAAC,CAAC;QACL,CAAC;QAED,OAAO;YACL,MAAM,EAAE,SAAS;YACjB,SAAS;YACT,KAAK,EAAE,EAAE;YACT,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,SAAS;YACT,YAAY,EAAE,OAAO,CAAC,MAAM;YAC5B,SAAS,EAAE,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM;YAC5C,aAAa,EAAE,IAAI,CAAC,MAAM;YAC1B,SAAS,EAAE,IAAI;YACf,MAAM;SACP,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,kBAAkB,CAAC;IAC/C,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,KAAK,CAAC,gBAAgB,CAAC,YAAY,EAAE,aAAa,EAAE,cAAc,CAAC,CAAC;IACjF,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,SAAS;QACvD,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,aAAa,CACxC,GAAG,CAAC,SAAS,EACb,GAAG,CAAC,KAAK,EACT,GAAG,CAAC,KAAK,EACT,IAAI,CAAC,WAAW,CACjB,CAAC;QACF,MAAM,CAAC,IAAI,CAAC;YACV,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,UAAU;YACV,IAAI;SACL,CAAC,CAAC;IACL,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACnB,gBAAgB,CACd,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,EAC5C,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAC7C,CACF,CAAC;IAEF,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAErC,OAAO;QACL,SAAS,EAAE,EAAE;QACb,KAAK;QACL,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,SAAS;QACT,YAAY,EAAE,MAAM,CAAC,MAAM;QAC3B,SAAS,EAAE,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM;QACvC,aAAa,EAAE,IAAI,CAAC,MAAM;QAC1B,SAAS,EAAE,IAAI;QACf,MAAM,EACJ,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,GAAG,CAAC;YAClC,CAAC,CAAC,+CAA+C,YAAY,mBAAmB,aAAa,YAAY,SAAS,gBAAgB;YAClI,CAAC,CAAC,SAAS;KAChB,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAqB,EACrB,QAAgB,EAChB,KAAe,EACf,OAA6B,EAAE;IAE/B,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B,mDAAmD;IACnD,gDAAgD;IAChD,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,MAAM,IAAI,UAAU,CAAC,CAAC;IACxD,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,cAAc,CAAC;IACzD,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,sBAAsB,CAAC;IACnE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,sBAAsB,CAAC;IACnD,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,gBAAgB,CAAC;IAE/D,MAAM,IAAI,GAAG,KAAK,CAAC,eAAe,EAAE,CAAC;IACrC,MAAM,SAAS,GAAG,KAAK,CAAC,kBAAkB,EAAE,CAAC;IAE7C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,MAAM,EAAE,aAAa;YACrB,SAAS;YACT,SAAS,EAAE,EAAE;YACb,YAAY,EAAE,CAAC;YACf,SAAS,EAAE,KAAK;YAChB,aAAa,EAAE,KAAK;SACrB,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/D,MAAM,IAAI,GAAG,IAAI,GAAG,EAA4B,CAAC;IACjD,8CAA8C;IAC9C,eAAe;IACf,IAAI,aAAa,GAAG,KAAK,CAAC;IAE1B,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,YAAY,CACvB,KAAK,EACL,IAAI,EACJ,IAAI,CAAC,WAAW,EAChB,YAAY,EACZ,aAAa,EACb,MAAM,EACN,cAAc,CACf,CAAC;QACF,IAAI,IAAI,CAAC,MAAM;YAAE,aAAa,GAAG,IAAI,CAAC;QACtC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC/B,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACtC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;YAClC,IAAI,CAAC,IAAI,IAAI,aAAa,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC;gBAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IAED,0CAA0C;IAC1C,8CAA8C;IAC9C,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAEzC,IAAI,MAA0B,CAAC;IAC/B,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM;YACJ,kCAAkC,OAAO,CAAC,IAAI,kBAAkB,SAAS,mBAAmB;gBAC5F,sFAAsF,CAAC;QACzF,MAAM,CAAC,IAAI,CAAC,kCAAkC,EAAE;YAC9C,MAAM,EAAE,qBAAqB;YAC7B,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,OAAO,CAAC,IAAI;YACnB,SAAS;SACV,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,SAAS,EAAE,IAAI;QACf,MAAM;QACN,SAAS;QACT,SAAS;QACT,YAAY,EAAE,MAAM,CAAC,MAAM;QAC3B,SAAS,EAAE,MAAM,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM;QAC3C,aAAa;KACd,CAAC;AACJ,CAAC"}
@@ -1,4 +1,5 @@
1
- import type { BuildOptions, BuildResult, BlastRadiusOptions, BlastRadiusResult, DiffRadiusOptions, QueryPattern, QueryResult, CodeGraphStats } from './types.js';
1
+ import type { CoChangeQueryOptions } from './cochange.js';
2
+ import type { BuildOptions, BuildResult, BlastRadiusOptions, BlastRadiusResult, DiffRadiusOptions, QueryPattern, QueryResult, CodeGraphStats, CoChangeResult } from './types.js';
2
3
  import type { EmbeddingProvider } from './embedding-provider.js';
3
4
  import type { SummaryProvider } from './summary-provider.js';
4
5
  import { type ScoredFile } from './rrf.js';
@@ -32,6 +33,17 @@ export declare class CodeGraphEngine {
32
33
  * - all (default): git diff HEAD --name-only
33
34
  */
34
35
  diffRadius(repoRoot: string, opts?: DiffRadiusOptions): BlastRadiusResult;
36
+ /**
37
+ * git 이력에서 함께 바뀐 파일을 조회한다.
38
+ * target을 주면 그 파일의 이웃, 생략하면 상위 페어 전역 목록.
39
+ */
40
+ coChange(repoRoot: string, opts?: CoChangeQueryOptions): CoChangeResult;
41
+ /**
42
+ * co-change 조회 실패가 blast-radius 전체를 막지 않게 가둔다.
43
+ * 실패는 undefined가 아니라 available:false로 내려보내 "수집 안 됨"이
44
+ * "함께 바뀐 게 없음"으로 읽히지 않게 한다.
45
+ */
46
+ private safeCoChangeLookup;
35
47
  /**
36
48
  * Query the graph with a specific pattern.
37
49
  */
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/code-graph/engine.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,cAAc,EACf,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AA+CD,qBAAa,eAAe;IAC1B,OAAO,CAAC,UAAU,CAAqC;IAEvD,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,QAAQ;IAQhB;;OAEG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,WAAW;IA+G7D;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,iBAAiB;IAuB/E;;;;;OAKG;IACH,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,iBAAiB;IA8B7E;;OAEG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW;IA6D3E;;OAEG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc;IAMvC;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAInC;;;OAGG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE;IASxC;;;OAGG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;IA0BhE;;;OAGG;IACG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAuCzF;;;OAGG;IACG,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA4BnF;;;OAGG;IACG,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,oBAAoB,CAAC;IAyEhC;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb,OAAO,CAAC,kBAAkB;CAiB3B;AAGD,eAAO,MAAM,eAAe,iBAAwB,CAAC"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/code-graph/engine.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAG1D,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,iBAAiB,EAEjB,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,cAAc,EAEd,cAAc,EAEf,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AA+CD,qBAAa,eAAe;IAC1B,OAAO,CAAC,UAAU,CAAqC;IAEvD,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,QAAQ;IAQhB;;OAEG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,WAAW;IA8H7D;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,iBAAiB;IAyB/E;;;;;OAKG;IACH,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,iBAAiB;IA+B7E;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,oBAAyB,GAAG,cAAc;IAK3E;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW;IA6D3E;;OAEG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc;IAMvC;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAInC;;;OAGG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE;IASxC;;;OAGG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;IA0BhE;;;OAGG;IACG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAuCzF;;;OAGG;IACG,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA4BnF;;;OAGG;IACG,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,oBAAoB,CAAC;IAyEhC;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb,OAAO,CAAC,kBAAkB;CAiB3B;AAGD,eAAO,MAAM,eAAe,iBAAwB,CAAC"}
@@ -5,6 +5,7 @@ import { execSync } from 'node:child_process';
5
5
  import { CodeGraphStore } from './storage.js';
6
6
  import { computeBlastRadius } from './blast-radius.js';
7
7
  import { getPluginForFile } from './plugins/index.js';
8
+ import { syncCoChange, queryCoChange, buildCoChangeLookup } from './cochange.js';
8
9
  import { NodeKind } from './types.js';
9
10
  import { logger } from '../core/logger.js';
10
11
  function getFilesRecursively(repoRoot, excludePatterns, include) {
@@ -145,6 +146,19 @@ export class CodeGraphEngine {
145
146
  files: skippedFiles.slice(0, 10).map((f) => f.filePath),
146
147
  });
147
148
  }
149
+ // git 이력 신호는 노드나 엣지와 독립이다. 실패해도 그래프 빌드까지
150
+ // 무너뜨리면 안 되므로 여기서 가둔다.
151
+ let coChange;
152
+ try {
153
+ coChange = syncCoChange(store, repoRoot, { mode });
154
+ }
155
+ catch (e) {
156
+ logger.warn('code_graph.cochange_failed', {
157
+ module: 'code-graph/engine',
158
+ repoRoot,
159
+ reason: e instanceof Error ? e.message : String(e),
160
+ });
161
+ }
148
162
  const timeTakenMs = Date.now() - start;
149
163
  logger.info('code_graph.build_completed', {
150
164
  module: 'code-graph/engine',
@@ -152,6 +166,7 @@ export class CodeGraphEngine {
152
166
  nodesBuilt,
153
167
  edgesBuilt,
154
168
  skippedCount: skippedFiles.length,
169
+ coChangePairs: coChange?.pairs,
155
170
  durationMs: timeTakenMs,
156
171
  });
157
172
  return {
@@ -160,6 +175,7 @@ export class CodeGraphEngine {
160
175
  timeTakenMs,
161
176
  installedHook: false, // Hook installation handled separately via GitHookManager
162
177
  skippedFiles,
178
+ coChange,
163
179
  };
164
180
  }
165
181
  /**
@@ -168,20 +184,22 @@ export class CodeGraphEngine {
168
184
  */
169
185
  blastRadius(repoRoot, opts = {}) {
170
186
  const store = this.getStore(repoRoot);
171
- const { changedFiles, base = 'HEAD~1', maxDepth = 2 } = opts;
187
+ const { changedFiles, base = 'HEAD~1', maxDepth = 2, coChange } = opts;
172
188
  let files = changedFiles;
173
189
  if (!files || files.length === 0) {
174
190
  files = this.getGitChangedFiles(repoRoot, base);
175
191
  }
176
192
  // Convert to absolute paths
177
193
  const absoluteFiles = files.map((f) => (f.startsWith('/') ? f : resolve(repoRoot, f)));
178
- const result = computeBlastRadius(store, absoluteFiles, maxDepth);
194
+ const lookup = this.safeCoChangeLookup(store, repoRoot, absoluteFiles, coChange);
195
+ const result = computeBlastRadius(store, absoluteFiles, maxDepth, lookup);
179
196
  logger.info('code_graph.blast_radius_completed', {
180
197
  module: 'code-graph/engine',
181
198
  repoRoot,
182
199
  base,
183
200
  changedFiles: absoluteFiles.length,
184
201
  riskScore: result.riskScore,
202
+ coChangeAvailable: result.coChangeAvailable,
185
203
  });
186
204
  return result;
187
205
  }
@@ -192,7 +210,7 @@ export class CodeGraphEngine {
192
210
  * - all (default): git diff HEAD --name-only
193
211
  */
194
212
  diffRadius(repoRoot, opts = {}) {
195
- const { mode = 'all', maxDepth = 2 } = opts;
213
+ const { mode = 'all', maxDepth = 2, coChange } = opts;
196
214
  const store = this.getStore(repoRoot);
197
215
  const gitCmd = mode === 'staged'
198
216
  ? 'git diff --cached --name-only'
@@ -215,7 +233,43 @@ export class CodeGraphEngine {
215
233
  catch {
216
234
  files = [];
217
235
  }
218
- return computeBlastRadius(store, files, maxDepth);
236
+ const lookup = this.safeCoChangeLookup(store, repoRoot, files, coChange);
237
+ return computeBlastRadius(store, files, maxDepth, lookup);
238
+ }
239
+ /**
240
+ * git 이력에서 함께 바뀐 파일을 조회한다.
241
+ * target을 주면 그 파일의 이웃, 생략하면 상위 페어 전역 목록.
242
+ */
243
+ coChange(repoRoot, opts = {}) {
244
+ const store = this.getStore(repoRoot);
245
+ return queryCoChange(store, repoRoot, opts);
246
+ }
247
+ /**
248
+ * co-change 조회 실패가 blast-radius 전체를 막지 않게 가둔다.
249
+ * 실패는 undefined가 아니라 available:false로 내려보내 "수집 안 됨"이
250
+ * "함께 바뀐 게 없음"으로 읽히지 않게 한다.
251
+ */
252
+ safeCoChangeLookup(store, repoRoot, seeds, tuning = {}) {
253
+ try {
254
+ return buildCoChangeLookup(store, repoRoot, seeds, tuning);
255
+ }
256
+ catch (e) {
257
+ const reason = e instanceof Error ? e.message : String(e);
258
+ logger.warn('code_graph.cochange_lookup_failed', {
259
+ module: 'code-graph/engine',
260
+ repoRoot,
261
+ reason,
262
+ });
263
+ return {
264
+ available: false,
265
+ reason,
266
+ pairsInDb: 0,
267
+ neighbors: [],
268
+ totalMatched: 0,
269
+ truncated: false,
270
+ matchedCapped: false,
271
+ };
272
+ }
219
273
  }
220
274
  /**
221
275
  * Query the graph with a specific pattern.