@syke1/mcp-server 1.4.16 → 1.4.18
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/dist/ai/realtime-analyzer.js +67 -2
- package/dist/git/change-coupling.d.ts +41 -0
- package/dist/git/change-coupling.js +250 -0
- package/dist/graph/incremental.d.ts +35 -0
- package/dist/graph/incremental.js +319 -0
- package/dist/graph/memo-cache.d.ts +47 -0
- package/dist/graph/memo-cache.js +176 -0
- package/dist/graph/scc.d.ts +57 -0
- package/dist/graph/scc.js +206 -0
- package/dist/graph.d.ts +6 -0
- package/dist/graph.js +17 -1
- package/dist/index.js +151 -11
- package/dist/scoring/pagerank.d.ts +67 -0
- package/dist/scoring/pagerank.js +221 -0
- package/dist/scoring/risk-scorer.d.ts +99 -0
- package/dist/scoring/risk-scorer.js +623 -0
- package/dist/tools/analyze-impact.d.ts +36 -1
- package/dist/tools/analyze-impact.js +278 -2
- package/dist/tools/gate-build.d.ts +7 -2
- package/dist/tools/gate-build.js +179 -13
- package/dist/watcher/file-cache.d.ts +9 -0
- package/dist/watcher/file-cache.js +41 -1
- package/dist/web/server.js +43 -5
- package/package.json +2 -2
|
@@ -35,9 +35,42 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.analyzeChangeRealtime = analyzeChangeRealtime;
|
|
37
37
|
const analyze_impact_1 = require("../tools/analyze-impact");
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const provider_1 = require("./provider");
|
|
40
41
|
const context_extractor_1 = require("./context-extractor");
|
|
42
|
+
const analysisCache = new Map();
|
|
43
|
+
const MAX_CACHE_SIZE = 100;
|
|
44
|
+
function computeContentHash(content, diff) {
|
|
45
|
+
return crypto.createHash("md5").update((content || "") + "\n---\n" + diff).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
function evictOldestCacheEntry() {
|
|
48
|
+
let oldestKey = null;
|
|
49
|
+
let oldestTime = Infinity;
|
|
50
|
+
for (const [key, entry] of analysisCache) {
|
|
51
|
+
if (entry.insertedAt < oldestTime) {
|
|
52
|
+
oldestTime = entry.insertedAt;
|
|
53
|
+
oldestKey = key;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (oldestKey)
|
|
57
|
+
analysisCache.delete(oldestKey);
|
|
58
|
+
}
|
|
59
|
+
// ── Rate limiter: max 10 AI calls per minute (sliding window) ──
|
|
60
|
+
const RATE_LIMIT_MAX = 10;
|
|
61
|
+
const RATE_LIMIT_WINDOW_MS = 60000;
|
|
62
|
+
const callTimestamps = [];
|
|
63
|
+
function isRateLimited() {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
// Remove timestamps outside the window
|
|
66
|
+
while (callTimestamps.length > 0 && callTimestamps[0] <= now - RATE_LIMIT_WINDOW_MS) {
|
|
67
|
+
callTimestamps.shift();
|
|
68
|
+
}
|
|
69
|
+
return callTimestamps.length >= RATE_LIMIT_MAX;
|
|
70
|
+
}
|
|
71
|
+
function recordCall() {
|
|
72
|
+
callTimestamps.push(Date.now());
|
|
73
|
+
}
|
|
41
74
|
function getSystemPrompt() {
|
|
42
75
|
return `You are a senior full-stack architect and code impact monitoring AI with 20 years of experience.
|
|
43
76
|
Role: Detect potential errors and cascading impacts before build when files are modified/added/deleted.
|
|
@@ -79,7 +112,7 @@ async function analyzeChangeRealtime(change, graph, getFileContent) {
|
|
|
79
112
|
const absPath = path.normalize(path.join(graph.sourceDir, relPath));
|
|
80
113
|
let affectedNodes = [];
|
|
81
114
|
if (graph.files.has(absPath)) {
|
|
82
|
-
const impact = (0, analyze_impact_1.analyzeImpact)(absPath, graph);
|
|
115
|
+
const impact = await (0, analyze_impact_1.analyzeImpact)(absPath, graph);
|
|
83
116
|
affectedNodes = [...impact.directDependents, ...impact.transitiveDependents];
|
|
84
117
|
}
|
|
85
118
|
// Build context: changed file + top 5 connected files' smart context
|
|
@@ -147,14 +180,41 @@ ${diffSummary}
|
|
|
147
180
|
${connectedFiles.length > 0 ? `## Connected files (${connectedFiles.length})\n${connectedFiles.join("\n\n")}` : "No connected files"}
|
|
148
181
|
|
|
149
182
|
Analyze the impact of this change on the project.`;
|
|
183
|
+
// ── Hash cache check: skip AI if content+diff unchanged ──
|
|
184
|
+
const diffStr = change.diff.map(d => `${d.type}:${d.line}:${d.old || ""}:${d.new || ""}`).join("|");
|
|
185
|
+
const contentHash = computeContentHash(change.newContent, diffStr);
|
|
186
|
+
const cached = analysisCache.get(relPath);
|
|
187
|
+
if (cached && cached.hash === contentHash) {
|
|
188
|
+
console.error(`[syke:ai] Cache hit for ${relPath} — skipping AI call`);
|
|
189
|
+
return { ...cached.result, timestamp: change.timestamp, analysisMs: 0 };
|
|
190
|
+
}
|
|
191
|
+
// ── Rate limit check ──
|
|
192
|
+
if (isRateLimited()) {
|
|
193
|
+
const analysisMs = Date.now() - start;
|
|
194
|
+
console.error(`[syke:ai] Rate limit reached (${RATE_LIMIT_MAX}/min) — skipping AI for ${relPath}`);
|
|
195
|
+
return {
|
|
196
|
+
file: relPath,
|
|
197
|
+
changeType: change.type,
|
|
198
|
+
timestamp: change.timestamp,
|
|
199
|
+
riskLevel: affectedNodes.length >= 10 ? "HIGH" : affectedNodes.length >= 5 ? "MEDIUM" : "LOW",
|
|
200
|
+
summary: `Rate limited — graph-based analysis: ${affectedNodes.length} files impacted`,
|
|
201
|
+
brokenImports: [],
|
|
202
|
+
sideEffects: [],
|
|
203
|
+
warnings: ["AI analysis skipped: rate limit (10 calls/min)"],
|
|
204
|
+
suggestion: "Wait a moment for AI analysis to resume",
|
|
205
|
+
affectedNodes,
|
|
206
|
+
analysisMs,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
150
209
|
try {
|
|
151
210
|
const provider = (0, provider_1.getAIProvider)();
|
|
152
211
|
if (!provider) {
|
|
153
212
|
throw new Error("No AI provider available (set GEMINI_KEY, OPENAI_KEY, or ANTHROPIC_KEY)");
|
|
154
213
|
}
|
|
214
|
+
recordCall();
|
|
155
215
|
const parsed = await provider.analyzeJSON(getSystemPrompt(), userPrompt);
|
|
156
216
|
const analysisMs = Date.now() - start;
|
|
157
|
-
|
|
217
|
+
const result = {
|
|
158
218
|
file: relPath,
|
|
159
219
|
changeType: change.type,
|
|
160
220
|
timestamp: change.timestamp,
|
|
@@ -167,6 +227,11 @@ Analyze the impact of this change on the project.`;
|
|
|
167
227
|
affectedNodes,
|
|
168
228
|
analysisMs,
|
|
169
229
|
};
|
|
230
|
+
// Store in cache
|
|
231
|
+
if (analysisCache.size >= MAX_CACHE_SIZE)
|
|
232
|
+
evictOldestCacheEntry();
|
|
233
|
+
analysisCache.set(relPath, { hash: contentHash, result, insertedAt: Date.now() });
|
|
234
|
+
return result;
|
|
170
235
|
}
|
|
171
236
|
catch (err) {
|
|
172
237
|
const analysisMs = Date.now() - start;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ChangeCoupling {
|
|
2
|
+
file1: string;
|
|
3
|
+
file2: string;
|
|
4
|
+
coChangeCount: number;
|
|
5
|
+
file1Changes: number;
|
|
6
|
+
file2Changes: number;
|
|
7
|
+
confidence: number;
|
|
8
|
+
support: number;
|
|
9
|
+
}
|
|
10
|
+
export interface CouplingResult {
|
|
11
|
+
couplings: ChangeCoupling[];
|
|
12
|
+
fileCouplings: Map<string, ChangeCoupling[]>;
|
|
13
|
+
totalCommitsAnalyzed: number;
|
|
14
|
+
analyzedAt: number;
|
|
15
|
+
}
|
|
16
|
+
export interface CouplingOptions {
|
|
17
|
+
maxCommits?: number;
|
|
18
|
+
minSupport?: number;
|
|
19
|
+
minConfidence?: number;
|
|
20
|
+
maxFilesPerCommit?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Invalidate the coupling cache. Call this when the graph is refreshed
|
|
24
|
+
* or when git history may have changed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function invalidateCouplingCache(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Mine git history to find files that frequently co-change.
|
|
29
|
+
*
|
|
30
|
+
* Runs `git log --name-only` and analyzes pairwise file combinations
|
|
31
|
+
* within each commit to identify hidden logical dependencies.
|
|
32
|
+
*/
|
|
33
|
+
export declare function mineGitHistory(projectRoot: string, options?: CouplingOptions): Promise<CouplingResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Get all significant couplings for a given file path.
|
|
36
|
+
* Returns an empty array if no couplings are found.
|
|
37
|
+
*
|
|
38
|
+
* The filePath should be a relative path matching git log output format
|
|
39
|
+
* (forward slashes, relative to project root).
|
|
40
|
+
*/
|
|
41
|
+
export declare function getCoupledFiles(filePath: string, result: CouplingResult): ChangeCoupling[];
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.invalidateCouplingCache = invalidateCouplingCache;
|
|
4
|
+
exports.mineGitHistory = mineGitHistory;
|
|
5
|
+
exports.getCoupledFiles = getCoupledFiles;
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
// ── Defaults ──
|
|
8
|
+
const DEFAULT_MAX_COMMITS = 500;
|
|
9
|
+
const DEFAULT_MIN_SUPPORT = 3;
|
|
10
|
+
const DEFAULT_MIN_CONFIDENCE = 0.3;
|
|
11
|
+
const DEFAULT_MAX_FILES_PER_COMMIT = 20;
|
|
12
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
13
|
+
// ── Cache ──
|
|
14
|
+
let cachedResult = null;
|
|
15
|
+
let cachedProjectRoot = null;
|
|
16
|
+
/**
|
|
17
|
+
* Invalidate the coupling cache. Call this when the graph is refreshed
|
|
18
|
+
* or when git history may have changed.
|
|
19
|
+
*/
|
|
20
|
+
function invalidateCouplingCache() {
|
|
21
|
+
cachedResult = null;
|
|
22
|
+
cachedProjectRoot = null;
|
|
23
|
+
}
|
|
24
|
+
// ── Git History Mining ──
|
|
25
|
+
/**
|
|
26
|
+
* Check whether the given directory is inside a git repository.
|
|
27
|
+
*/
|
|
28
|
+
function isGitRepo(projectRoot) {
|
|
29
|
+
try {
|
|
30
|
+
(0, child_process_1.execSync)("git rev-parse --is-inside-work-tree", {
|
|
31
|
+
cwd: projectRoot,
|
|
32
|
+
encoding: "utf-8",
|
|
33
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
34
|
+
});
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse git log output into a list of commits, each containing
|
|
43
|
+
* the list of files changed in that commit.
|
|
44
|
+
*/
|
|
45
|
+
function parseGitLog(raw) {
|
|
46
|
+
const commits = [];
|
|
47
|
+
const segments = raw.split("COMMIT:");
|
|
48
|
+
for (const segment of segments) {
|
|
49
|
+
const trimmed = segment.trim();
|
|
50
|
+
if (!trimmed)
|
|
51
|
+
continue;
|
|
52
|
+
// First line is the commit hash, remaining lines are file paths
|
|
53
|
+
const lines = trimmed.split("\n");
|
|
54
|
+
const files = [];
|
|
55
|
+
for (let i = 1; i < lines.length; i++) {
|
|
56
|
+
const fileLine = lines[i].trim();
|
|
57
|
+
if (fileLine) {
|
|
58
|
+
files.push(fileLine);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (files.length > 0) {
|
|
62
|
+
commits.push(files);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return commits;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Normalize a git-output path (forward slashes) to be consistent
|
|
69
|
+
* with how the dependency graph stores paths.
|
|
70
|
+
*/
|
|
71
|
+
function normalizePath(filePath) {
|
|
72
|
+
// Git always outputs forward slashes; normalize for consistency
|
|
73
|
+
return filePath.replace(/\\/g, "/");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check if a file path looks like a source file (not binary, not config noise).
|
|
77
|
+
* We keep this broad — the dependency graph comparison will handle the real filtering.
|
|
78
|
+
*/
|
|
79
|
+
function isSourceFile(filePath) {
|
|
80
|
+
// Skip obviously non-source files
|
|
81
|
+
const skipPatterns = [
|
|
82
|
+
/\.lock$/,
|
|
83
|
+
/package-lock\.json$/,
|
|
84
|
+
/yarn\.lock$/,
|
|
85
|
+
/\.min\.(js|css)$/,
|
|
86
|
+
/\.map$/,
|
|
87
|
+
/\.d\.ts$/,
|
|
88
|
+
/\.png$/,
|
|
89
|
+
/\.jpg$/,
|
|
90
|
+
/\.jpeg$/,
|
|
91
|
+
/\.gif$/,
|
|
92
|
+
/\.svg$/,
|
|
93
|
+
/\.ico$/,
|
|
94
|
+
/\.woff2?$/,
|
|
95
|
+
/\.ttf$/,
|
|
96
|
+
/\.eot$/,
|
|
97
|
+
/\.pdf$/,
|
|
98
|
+
/\.zip$/,
|
|
99
|
+
/\.tar$/,
|
|
100
|
+
/\.gz$/,
|
|
101
|
+
];
|
|
102
|
+
const normalized = filePath.toLowerCase();
|
|
103
|
+
return !skipPatterns.some((p) => p.test(normalized));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Create a canonical pair key for two files (order-independent).
|
|
107
|
+
*/
|
|
108
|
+
function pairKey(a, b) {
|
|
109
|
+
return a < b ? `${a}\0${b}` : `${b}\0${a}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Mine git history to find files that frequently co-change.
|
|
113
|
+
*
|
|
114
|
+
* Runs `git log --name-only` and analyzes pairwise file combinations
|
|
115
|
+
* within each commit to identify hidden logical dependencies.
|
|
116
|
+
*/
|
|
117
|
+
async function mineGitHistory(projectRoot, options) {
|
|
118
|
+
// Return cached result if still valid
|
|
119
|
+
if (cachedResult &&
|
|
120
|
+
cachedProjectRoot === projectRoot &&
|
|
121
|
+
Date.now() - cachedResult.analyzedAt < CACHE_TTL_MS) {
|
|
122
|
+
return cachedResult;
|
|
123
|
+
}
|
|
124
|
+
const maxCommits = options?.maxCommits ?? DEFAULT_MAX_COMMITS;
|
|
125
|
+
const minSupport = options?.minSupport ?? DEFAULT_MIN_SUPPORT;
|
|
126
|
+
const minConfidence = options?.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
127
|
+
const maxFilesPerCommit = options?.maxFilesPerCommit ?? DEFAULT_MAX_FILES_PER_COMMIT;
|
|
128
|
+
// Empty result for non-git projects
|
|
129
|
+
const emptyResult = {
|
|
130
|
+
couplings: [],
|
|
131
|
+
fileCouplings: new Map(),
|
|
132
|
+
totalCommitsAnalyzed: 0,
|
|
133
|
+
analyzedAt: Date.now(),
|
|
134
|
+
};
|
|
135
|
+
if (!isGitRepo(projectRoot)) {
|
|
136
|
+
cachedResult = emptyResult;
|
|
137
|
+
cachedProjectRoot = projectRoot;
|
|
138
|
+
return emptyResult;
|
|
139
|
+
}
|
|
140
|
+
// Run git log
|
|
141
|
+
let raw;
|
|
142
|
+
try {
|
|
143
|
+
raw = (0, child_process_1.execSync)(`git log --name-only --format="COMMIT:%H" --max-count=${maxCommits}`, {
|
|
144
|
+
cwd: projectRoot,
|
|
145
|
+
encoding: "utf-8",
|
|
146
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
147
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
cachedResult = emptyResult;
|
|
152
|
+
cachedProjectRoot = projectRoot;
|
|
153
|
+
return emptyResult;
|
|
154
|
+
}
|
|
155
|
+
const commits = parseGitLog(raw);
|
|
156
|
+
// Track per-file change counts and per-pair co-change counts
|
|
157
|
+
const fileChangeCount = new Map();
|
|
158
|
+
const pairCoChangeCount = new Map();
|
|
159
|
+
let totalCommitsAnalyzed = 0;
|
|
160
|
+
for (const commitFiles of commits) {
|
|
161
|
+
// Filter to source files and normalize paths
|
|
162
|
+
const filtered = commitFiles
|
|
163
|
+
.map(normalizePath)
|
|
164
|
+
.filter(isSourceFile);
|
|
165
|
+
// Skip mega-commits (merge commits, large refactors)
|
|
166
|
+
if (filtered.length > maxFilesPerCommit || filtered.length < 2) {
|
|
167
|
+
if (filtered.length === 1) {
|
|
168
|
+
// Still count single-file commits for per-file totals
|
|
169
|
+
const file = filtered[0];
|
|
170
|
+
fileChangeCount.set(file, (fileChangeCount.get(file) || 0) + 1);
|
|
171
|
+
}
|
|
172
|
+
totalCommitsAnalyzed++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
totalCommitsAnalyzed++;
|
|
176
|
+
// Count per-file changes
|
|
177
|
+
for (const file of filtered) {
|
|
178
|
+
fileChangeCount.set(file, (fileChangeCount.get(file) || 0) + 1);
|
|
179
|
+
}
|
|
180
|
+
// Count pairwise co-changes
|
|
181
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
182
|
+
for (let j = i + 1; j < filtered.length; j++) {
|
|
183
|
+
const key = pairKey(filtered[i], filtered[j]);
|
|
184
|
+
pairCoChangeCount.set(key, (pairCoChangeCount.get(key) || 0) + 1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Build coupling results, filtering by thresholds
|
|
189
|
+
const couplings = [];
|
|
190
|
+
for (const [key, coCount] of pairCoChangeCount) {
|
|
191
|
+
if (coCount < minSupport)
|
|
192
|
+
continue;
|
|
193
|
+
const [file1, file2] = key.split("\0");
|
|
194
|
+
const file1Changes = fileChangeCount.get(file1) || 0;
|
|
195
|
+
const file2Changes = fileChangeCount.get(file2) || 0;
|
|
196
|
+
const maxChanges = Math.max(file1Changes, file2Changes);
|
|
197
|
+
const confidence = maxChanges > 0 ? coCount / maxChanges : 0;
|
|
198
|
+
if (confidence < minConfidence)
|
|
199
|
+
continue;
|
|
200
|
+
couplings.push({
|
|
201
|
+
file1,
|
|
202
|
+
file2,
|
|
203
|
+
coChangeCount: coCount,
|
|
204
|
+
file1Changes,
|
|
205
|
+
file2Changes,
|
|
206
|
+
confidence,
|
|
207
|
+
support: coCount,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// Sort by confidence descending
|
|
211
|
+
couplings.sort((a, b) => b.confidence - a.confidence);
|
|
212
|
+
// Build the per-file lookup map
|
|
213
|
+
const fileCouplings = new Map();
|
|
214
|
+
for (const coupling of couplings) {
|
|
215
|
+
// Add to file1's list
|
|
216
|
+
if (!fileCouplings.has(coupling.file1)) {
|
|
217
|
+
fileCouplings.set(coupling.file1, []);
|
|
218
|
+
}
|
|
219
|
+
fileCouplings.get(coupling.file1).push(coupling);
|
|
220
|
+
// Add to file2's list
|
|
221
|
+
if (!fileCouplings.has(coupling.file2)) {
|
|
222
|
+
fileCouplings.set(coupling.file2, []);
|
|
223
|
+
}
|
|
224
|
+
fileCouplings.get(coupling.file2).push(coupling);
|
|
225
|
+
}
|
|
226
|
+
// Sort each file's couplings by confidence descending
|
|
227
|
+
for (const [, list] of fileCouplings) {
|
|
228
|
+
list.sort((a, b) => b.confidence - a.confidence);
|
|
229
|
+
}
|
|
230
|
+
const result = {
|
|
231
|
+
couplings,
|
|
232
|
+
fileCouplings,
|
|
233
|
+
totalCommitsAnalyzed,
|
|
234
|
+
analyzedAt: Date.now(),
|
|
235
|
+
};
|
|
236
|
+
cachedResult = result;
|
|
237
|
+
cachedProjectRoot = projectRoot;
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get all significant couplings for a given file path.
|
|
242
|
+
* Returns an empty array if no couplings are found.
|
|
243
|
+
*
|
|
244
|
+
* The filePath should be a relative path matching git log output format
|
|
245
|
+
* (forward slashes, relative to project root).
|
|
246
|
+
*/
|
|
247
|
+
function getCoupledFiles(filePath, result) {
|
|
248
|
+
const normalized = normalizePath(filePath);
|
|
249
|
+
return result.fileCouplings.get(normalized) || [];
|
|
250
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental Graph Updates for SYKE.
|
|
3
|
+
*
|
|
4
|
+
* Instead of rebuilding the entire dependency graph when a single file changes,
|
|
5
|
+
* this module re-parses only the changed file's imports and updates the
|
|
6
|
+
* forward/reverse maps in place. SCC and PageRank are recomputed fully
|
|
7
|
+
* (both are O(V+E) and fast enough) only when edges actually change.
|
|
8
|
+
*
|
|
9
|
+
* This brings update latency from O(N * parse) down to O(1 * parse + V+E)
|
|
10
|
+
* for large codebases (10K+ files).
|
|
11
|
+
*/
|
|
12
|
+
import { DependencyGraph } from "../graph";
|
|
13
|
+
export interface IncrementalUpdateResult {
|
|
14
|
+
updatedFile: string;
|
|
15
|
+
addedEdges: [string, string][];
|
|
16
|
+
removedEdges: [string, string][];
|
|
17
|
+
edgesChanged: boolean;
|
|
18
|
+
affectedFiles: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Update the graph for a single changed file.
|
|
22
|
+
* Re-parses only that file's imports and updates forward/reverse maps.
|
|
23
|
+
* Returns info about what changed for cache invalidation.
|
|
24
|
+
*/
|
|
25
|
+
export declare function updateGraphForFile(graph: DependencyGraph, filePath: string, projectRoot: string): IncrementalUpdateResult;
|
|
26
|
+
/**
|
|
27
|
+
* Add a new file to the graph.
|
|
28
|
+
* Initializes forward/reverse entries, parses imports, and adds edges.
|
|
29
|
+
*/
|
|
30
|
+
export declare function addFileToGraph(graph: DependencyGraph, filePath: string, projectRoot: string): IncrementalUpdateResult;
|
|
31
|
+
/**
|
|
32
|
+
* Remove a file from the graph.
|
|
33
|
+
* Cleans up all forward edges, reverse edges, and the files set.
|
|
34
|
+
*/
|
|
35
|
+
export declare function removeFileFromGraph(graph: DependencyGraph, filePath: string): IncrementalUpdateResult;
|