@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.
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ /**
3
+ * Strongly Connected Components (SCC) via Tarjan's algorithm,
4
+ * graph condensation into a DAG, and topological sort via Kahn's algorithm.
5
+ *
6
+ * Used to detect circular dependencies and provide accurate cascade-level
7
+ * impact analysis on the condensed (acyclic) dependency graph.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.computeSCC = computeSCC;
11
+ exports.condenseGraph = condenseGraph;
12
+ exports.topologicalSort = topologicalSort;
13
+ // ── Tarjan's SCC Algorithm ──
14
+ /**
15
+ * Compute all Strongly Connected Components of the dependency graph
16
+ * using Tarjan's algorithm. Returns SCCs, a file-to-SCC mapping,
17
+ * and the condensed DAG with topological ordering.
18
+ */
19
+ function computeSCC(graph) {
20
+ // Handle empty graph
21
+ if (graph.files.size === 0) {
22
+ const emptyDAG = {
23
+ nodes: [],
24
+ forward: new Map(),
25
+ reverse: new Map(),
26
+ topologicalOrder: [],
27
+ };
28
+ return {
29
+ components: [],
30
+ nodeToComponent: new Map(),
31
+ condensed: emptyDAG,
32
+ };
33
+ }
34
+ const components = [];
35
+ // Tarjan state
36
+ let indexCounter = 0;
37
+ const nodeIndex = new Map();
38
+ const nodeLowlink = new Map();
39
+ const onStack = new Set();
40
+ const stack = [];
41
+ function strongConnect(node) {
42
+ nodeIndex.set(node, indexCounter);
43
+ nodeLowlink.set(node, indexCounter);
44
+ indexCounter++;
45
+ stack.push(node);
46
+ onStack.add(node);
47
+ const successors = graph.forward.get(node) || [];
48
+ for (const successor of successors) {
49
+ // Only process nodes that exist in the graph
50
+ if (!graph.files.has(successor))
51
+ continue;
52
+ if (!nodeIndex.has(successor)) {
53
+ // Successor not yet visited — recurse
54
+ strongConnect(successor);
55
+ nodeLowlink.set(node, Math.min(nodeLowlink.get(node), nodeLowlink.get(successor)));
56
+ }
57
+ else if (onStack.has(successor)) {
58
+ // Successor is on the stack — part of current SCC
59
+ nodeLowlink.set(node, Math.min(nodeLowlink.get(node), nodeIndex.get(successor)));
60
+ }
61
+ }
62
+ // If node is a root of an SCC, pop the stack to form a component
63
+ if (nodeLowlink.get(node) === nodeIndex.get(node)) {
64
+ const component = [];
65
+ let w;
66
+ do {
67
+ w = stack.pop();
68
+ onStack.delete(w);
69
+ component.push(w);
70
+ } while (w !== node);
71
+ components.push(component);
72
+ }
73
+ }
74
+ // Visit all nodes (handles disconnected components)
75
+ for (const file of graph.files) {
76
+ if (!nodeIndex.has(file)) {
77
+ strongConnect(file);
78
+ }
79
+ }
80
+ // Build file-to-component mapping
81
+ const nodeToComponent = new Map();
82
+ for (let i = 0; i < components.length; i++) {
83
+ for (const file of components[i]) {
84
+ nodeToComponent.set(file, i);
85
+ }
86
+ }
87
+ // Build the condensed DAG
88
+ const condensed = condenseGraph(graph, components, nodeToComponent);
89
+ return { components, nodeToComponent, condensed };
90
+ }
91
+ // ── Graph Condensation ──
92
+ /**
93
+ * Build a DAG where each node represents one SCC.
94
+ * Edges between SCCs are derived from the original graph's edges
95
+ * between files belonging to different SCCs.
96
+ */
97
+ function condenseGraph(graph, components, nodeToComponent) {
98
+ const numComponents = components.length;
99
+ // Build condensed nodes
100
+ const nodes = components.map((files, index) => ({
101
+ index,
102
+ files,
103
+ size: files.length,
104
+ isCyclic: files.length > 1,
105
+ }));
106
+ // Build forward and reverse edges between SCCs (deduplicated)
107
+ const forwardSets = new Map();
108
+ const reverseSets = new Map();
109
+ for (let i = 0; i < numComponents; i++) {
110
+ forwardSets.set(i, new Set());
111
+ reverseSets.set(i, new Set());
112
+ }
113
+ for (const [file, deps] of graph.forward) {
114
+ const srcSCC = nodeToComponent.get(file);
115
+ if (srcSCC === undefined)
116
+ continue;
117
+ for (const dep of deps) {
118
+ const dstSCC = nodeToComponent.get(dep);
119
+ if (dstSCC === undefined)
120
+ continue;
121
+ // Skip self-edges (within the same SCC)
122
+ if (srcSCC === dstSCC)
123
+ continue;
124
+ forwardSets.get(srcSCC).add(dstSCC);
125
+ reverseSets.get(dstSCC).add(srcSCC);
126
+ }
127
+ }
128
+ // Convert sets to arrays
129
+ const forward = new Map();
130
+ const reverse = new Map();
131
+ for (const [key, set] of forwardSets) {
132
+ forward.set(key, [...set]);
133
+ }
134
+ for (const [key, set] of reverseSets) {
135
+ reverse.set(key, [...set]);
136
+ }
137
+ const dag = {
138
+ nodes,
139
+ forward,
140
+ reverse,
141
+ topologicalOrder: [],
142
+ };
143
+ // Compute topological order
144
+ dag.topologicalOrder = topologicalSort(dag);
145
+ return dag;
146
+ }
147
+ // ── Topological Sort (Kahn's Algorithm) ──
148
+ /**
149
+ * Compute a topological ordering of the condensed DAG using Kahn's algorithm.
150
+ * The condensed graph is guaranteed to be acyclic after SCC condensation.
151
+ *
152
+ * Returns SCC indices in dependency order: dependencies come before dependents.
153
+ * This uses the `forward` edges (file A imports B means A -> B in forward),
154
+ * so we process nodes with no incoming forward edges first (leaf dependencies).
155
+ */
156
+ function topologicalSort(dag) {
157
+ const numNodes = dag.nodes.length;
158
+ if (numNodes === 0)
159
+ return [];
160
+ // In-degree = number of dependencies (forward edges out of each node).
161
+ // forward[A] = [B] means A imports B, so A depends on B.
162
+ // For "dependencies first" ordering, nodes with zero dependencies come first.
163
+ const inDegree = new Map();
164
+ for (let i = 0; i < numNodes; i++) {
165
+ inDegree.set(i, 0);
166
+ }
167
+ for (const [src, dsts] of dag.forward) {
168
+ inDegree.set(src, (inDegree.get(src) || 0) + dsts.length);
169
+ }
170
+ // Start with nodes that have no dependencies (in-degree 0 in forward)
171
+ const queue = [];
172
+ for (const [node, degree] of inDegree) {
173
+ if (degree === 0) {
174
+ queue.push(node);
175
+ }
176
+ }
177
+ const order = [];
178
+ while (queue.length > 0) {
179
+ const current = queue.shift();
180
+ order.push(current);
181
+ // current has no remaining dependencies.
182
+ // For all nodes that depend on current (reverse edges: who imports current),
183
+ // decrement their in-degree.
184
+ const dependents = dag.reverse.get(current) || [];
185
+ for (const dependent of dependents) {
186
+ const newDegree = (inDegree.get(dependent) || 0) - 1;
187
+ inDegree.set(dependent, newDegree);
188
+ if (newDegree === 0) {
189
+ queue.push(dependent);
190
+ }
191
+ }
192
+ }
193
+ // If order doesn't contain all nodes, there's a bug (shouldn't happen after SCC condensation)
194
+ if (order.length !== numNodes) {
195
+ console.error(`[syke:scc] WARNING: Topological sort produced ${order.length}/${numNodes} nodes. ` +
196
+ `This indicates a bug in SCC condensation.`);
197
+ // Add remaining nodes at the end
198
+ const ordered = new Set(order);
199
+ for (let i = 0; i < numNodes; i++) {
200
+ if (!ordered.has(i)) {
201
+ order.push(i);
202
+ }
203
+ }
204
+ }
205
+ return order;
206
+ }
package/dist/graph.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { SCCResult } from "./graph/scc";
2
+ import { PageRankResult } from "./scoring/pagerank";
1
3
  export interface DependencyGraph {
2
4
  forward: Map<string, string[]>;
3
5
  reverse: Map<string, string[]>;
@@ -7,6 +9,10 @@ export interface DependencyGraph {
7
9
  sourceDirs: string[];
8
10
  /** backward compat: first source directory */
9
11
  sourceDir: string;
12
+ /** Strongly Connected Components — computed after graph build */
13
+ scc?: SCCResult;
14
+ /** PageRank importance scores — computed after graph build */
15
+ pageRank?: PageRankResult;
10
16
  }
11
17
  export declare function buildGraph(projectRoot: string, packageName?: string): DependencyGraph;
12
18
  export declare function getGraph(projectRoot: string, packageName?: string): DependencyGraph;
package/dist/graph.js CHANGED
@@ -39,6 +39,10 @@ exports.refreshGraph = refreshGraph;
39
39
  const path = __importStar(require("path"));
40
40
  const plugin_1 = require("./languages/plugin");
41
41
  const typescript_1 = require("./languages/typescript");
42
+ const scc_1 = require("./graph/scc");
43
+ const risk_scorer_1 = require("./scoring/risk-scorer");
44
+ const pagerank_1 = require("./scoring/pagerank");
45
+ const memo_cache_1 = require("./graph/memo-cache");
42
46
  let cachedGraph = null;
43
47
  function buildGraph(projectRoot, packageName) {
44
48
  const detectedPlugins = (0, plugin_1.detectLanguages)(projectRoot);
@@ -85,8 +89,17 @@ function buildGraph(projectRoot, packageName) {
85
89
  sourceDirs: allSourceDirs,
86
90
  sourceDir,
87
91
  };
92
+ // Invalidate memo cache (full rebuild means all cached BFS results are stale)
93
+ (0, memo_cache_1.resetMemoCache)();
94
+ // Compute SCC and attach to graph
95
+ const scc = (0, scc_1.computeSCC)(graph);
96
+ graph.scc = scc;
97
+ // Compute PageRank importance scores
98
+ (0, pagerank_1.invalidatePageRank)();
99
+ graph.pageRank = (0, pagerank_1.computePageRank)(graph);
100
+ const cyclicCount = scc.condensed.nodes.filter(n => n.isCyclic).length;
88
101
  cachedGraph = graph;
89
- console.error(`[syke] Graph built (${languages.join("+")}): ${files.size} files, ${countEdges(forward)} edges`);
102
+ console.error(`[syke] Graph built (${languages.join("+")}): ${files.size} files, ${countEdges(forward)} edges, ${scc.components.length} SCCs (${cyclicCount} cyclic)`);
90
103
  return graph;
91
104
  }
92
105
  function countEdges(forward) {
@@ -105,5 +118,8 @@ function getGraph(projectRoot, packageName) {
105
118
  function refreshGraph(projectRoot, packageName) {
106
119
  cachedGraph = null;
107
120
  (0, typescript_1.clearAliasCache)();
121
+ (0, risk_scorer_1.invalidateProjectMetrics)();
122
+ (0, pagerank_1.invalidatePageRank)();
123
+ (0, memo_cache_1.resetMemoCache)();
108
124
  return buildGraph(projectRoot, packageName);
109
125
  }
package/dist/index.js CHANGED
@@ -50,6 +50,9 @@ const graph_1 = require("./graph");
50
50
  const plugin_1 = require("./languages/plugin");
51
51
  const analyze_impact_1 = require("./tools/analyze-impact");
52
52
  const gate_build_1 = require("./tools/gate-build");
53
+ const change_coupling_1 = require("./git/change-coupling");
54
+ const risk_scorer_1 = require("./scoring/risk-scorer");
55
+ const pagerank_1 = require("./scoring/pagerank");
53
56
  const analyzer_1 = require("./ai/analyzer");
54
57
  const provider_1 = require("./ai/provider");
55
58
  const server_1 = require("./web/server");
@@ -248,7 +251,7 @@ async function main() {
248
251
  case "gate_build": {
249
252
  const graph = (0, graph_1.getGraph)(currentProjectRoot, currentPackageName);
250
253
  const files = args.files?.map((f) => resolveFilePath(f, currentProjectRoot, graph.sourceDir));
251
- const result = (0, gate_build_1.gateCheck)(graph, files);
254
+ const result = await (0, gate_build_1.gateCheck)(graph, files);
252
255
  return {
253
256
  content: [
254
257
  { type: "text", text: appendDashboardFooter((0, gate_build_1.formatGateResult)(result)) },
@@ -273,24 +276,84 @@ async function main() {
273
276
  if (!isFileInFreeSet(resolved, graph)) {
274
277
  return { content: [{ type: "text", text: PRO_UPGRADE_MSG }] };
275
278
  }
276
- const result = (0, analyze_impact_1.analyzeImpact)(resolved, graph);
279
+ const result = await (0, analyze_impact_1.analyzeImpact)(resolved, graph, { includeRiskScore: true, includeCoupling: true });
280
+ const cachedTag = result.fromCache ? " (cached)" : "";
277
281
  const lines = [
278
- `## Impact Analysis: ${result.relativePath}`,
282
+ `## Impact Analysis: ${result.relativePath}${cachedTag}`,
279
283
  `**Risk Level:** ${result.riskLevel}`,
280
284
  `**Total impacted files:** ${result.totalImpacted}`,
281
285
  "",
282
286
  ];
287
+ // Show composite risk score
288
+ if (result.riskScore) {
289
+ lines.push("### Composite Risk Score");
290
+ lines.push((0, risk_scorer_1.formatRiskScore)(result.riskScore));
291
+ lines.push("");
292
+ }
293
+ // Show circular dependency warning if file is in a cyclic SCC
294
+ if (result.circularCluster && result.circularCluster.length > 0) {
295
+ lines.push("### Circular Dependency Cluster");
296
+ lines.push(`This file is part of a circular dependency with ${result.circularCluster.length} other file(s):`);
297
+ for (const f of result.circularCluster) {
298
+ lines.push(`- ${f}`);
299
+ }
300
+ lines.push("**All files in this cluster are immediately affected by any change.**");
301
+ lines.push("");
302
+ }
283
303
  if (result.directDependents.length > 0) {
284
304
  lines.push(`### Direct Dependents (${result.directDependents.length})`);
285
305
  for (const d of result.directDependents) {
286
- lines.push(`- ${d}`);
306
+ const level = result.cascadeLevels?.get(d);
307
+ const levelStr = level !== undefined ? `, cascade level ${level}` : "";
308
+ // Add PageRank percentile if available
309
+ let prStr = "";
310
+ if (graph.pageRank) {
311
+ const absPath = path.normalize(path.join(graph.sourceDir, d));
312
+ const prData = (0, pagerank_1.getFileRank)(absPath, graph.pageRank);
313
+ if (prData) {
314
+ prStr = `, PageRank ${prData.percentile}th percentile`;
315
+ }
316
+ }
317
+ const annotationParts = [levelStr, prStr].filter(Boolean).join("");
318
+ lines.push(`- ${d}${annotationParts ? ` (${annotationParts.replace(/^, /, "")})` : ""}`);
287
319
  }
288
320
  }
289
321
  if (result.transitiveDependents.length > 0) {
290
322
  lines.push("");
291
323
  lines.push(`### Transitive Dependents (${result.transitiveDependents.length})`);
292
324
  for (const d of result.transitiveDependents) {
293
- lines.push(`- ${d}`);
325
+ const level = result.cascadeLevels?.get(d);
326
+ const levelStr = level !== undefined ? `, cascade level ${level}` : "";
327
+ // Add PageRank percentile if available
328
+ let prStr = "";
329
+ if (graph.pageRank) {
330
+ const absPath = path.normalize(path.join(graph.sourceDir, d));
331
+ const prData = (0, pagerank_1.getFileRank)(absPath, graph.pageRank);
332
+ if (prData) {
333
+ prStr = `, PageRank ${prData.percentile}th percentile`;
334
+ }
335
+ }
336
+ const annotationParts = [levelStr, prStr].filter(Boolean).join("");
337
+ lines.push(`- ${d}${annotationParts ? ` (${annotationParts.replace(/^, /, "")})` : ""}`);
338
+ }
339
+ }
340
+ // Show historical change coupling (hidden dependencies)
341
+ if (result.coupledFiles && result.coupledFiles.length > 0) {
342
+ lines.push("");
343
+ lines.push("### Historical Change Coupling (hidden dependencies)");
344
+ for (const cf of result.coupledFiles) {
345
+ const pct = Math.round(cf.confidence * 100);
346
+ lines.push(` - ${cf.relativePath} (confidence: ${pct}%, co-changed ${cf.coChangeCount} times)`);
347
+ }
348
+ lines.push("These files frequently change together but have no import relationship.");
349
+ }
350
+ // SCC summary stats
351
+ if (result.sccCount !== undefined) {
352
+ lines.push("");
353
+ lines.push("### Graph Structure");
354
+ lines.push(`- SCCs in project: ${result.sccCount}`);
355
+ if (result.cyclicSCCs !== undefined && result.cyclicSCCs > 0) {
356
+ lines.push(`- Circular dependency clusters: ${result.cyclicSCCs}`);
294
357
  }
295
358
  }
296
359
  return { content: [{ type: "text", text: appendDashboardFooter(lines.join("\n")) }] };
@@ -312,13 +375,37 @@ async function main() {
312
375
  if (!isFileInFreeSet(resolved, graph)) {
313
376
  return { content: [{ type: "text", text: PRO_UPGRADE_MSG }] };
314
377
  }
315
- const result = (0, analyze_impact_1.analyzeImpact)(resolved, graph);
378
+ const result = await (0, analyze_impact_1.analyzeImpact)(resolved, graph, { includeRiskScore: true, includeCoupling: true });
316
379
  const rel = path.relative(graph.sourceDir, resolved).replace(/\\/g, "/");
380
+ const safeCachedTag = result.fromCache ? " (cached)" : "";
381
+ // Enhanced output with composite risk score
382
+ let output = `${result.riskLevel} — ${rel} impacts ${result.totalImpacted} file(s)${safeCachedTag}`;
383
+ // Show file importance via PageRank
384
+ if (graph.pageRank) {
385
+ const prData = (0, pagerank_1.getFileRank)(resolved, graph.pageRank);
386
+ if (prData) {
387
+ output += `\nFile importance: rank #${prData.rank} of ${graph.files.size} files (${prData.percentile}th percentile)`;
388
+ }
389
+ }
390
+ if (result.riskScore) {
391
+ output += `\n${(0, risk_scorer_1.formatRiskScore)(result.riskScore)}`;
392
+ }
393
+ // Mention high-confidence couplings as a warning
394
+ if (result.coupledFiles && result.coupledFiles.length > 0) {
395
+ const highConf = result.coupledFiles.filter((cf) => cf.confidence >= 0.5);
396
+ if (highConf.length > 0) {
397
+ output += `\n\nHistorical coupling warning: ${highConf.length} file(s) frequently co-change with this file but have no import relationship:`;
398
+ for (const cf of highConf) {
399
+ const pct = Math.round(cf.confidence * 100);
400
+ output += `\n - ${cf.relativePath} (${pct}%, ${cf.coChangeCount} times)`;
401
+ }
402
+ }
403
+ }
317
404
  return {
318
405
  content: [
319
406
  {
320
407
  type: "text",
321
- text: appendDashboardFooter(`${result.riskLevel} — ${rel} impacts ${result.totalImpacted} file(s)`),
408
+ text: appendDashboardFooter(output),
322
409
  },
323
410
  ],
324
411
  };
@@ -368,6 +455,54 @@ async function main() {
368
455
  const requestedN = args.top_n || 10;
369
456
  const graph = (0, graph_1.getGraph)(currentProjectRoot, currentPackageName);
370
457
  const hubs = (0, analyze_impact_1.getHubFiles)(graph, requestedN);
458
+ // If PageRank is available, build enriched entries sorted by PageRank
459
+ const pageRankAvailable = !!graph.pageRank;
460
+ if (pageRankAvailable) {
461
+ // Enrich hub data with PageRank and re-sort by PageRank score
462
+ const enriched = hubs.map(h => {
463
+ const absPath = path.normalize(path.join(graph.sourceDir, h.relativePath));
464
+ const prData = graph.pageRank ? (0, pagerank_1.getFileRank)(absPath, graph.pageRank) : null;
465
+ return { ...h, prData };
466
+ });
467
+ // Sort by PageRank score descending (fallback to dependent count)
468
+ enriched.sort((a, b) => {
469
+ const scoreA = a.prData?.score ?? 0;
470
+ const scoreB = b.prData?.score ?? 0;
471
+ if (scoreB !== scoreA)
472
+ return scoreB - scoreA;
473
+ return b.dependentCount - a.dependentCount;
474
+ });
475
+ // Compute risk scores for hub files
476
+ try {
477
+ (0, risk_scorer_1.computeProjectMetrics)(graph);
478
+ }
479
+ catch { /* non-critical */ }
480
+ const lines = [
481
+ `## Hub Files (Top ${enriched.length}, ranked by PageRank)`,
482
+ "",
483
+ ];
484
+ enriched.forEach((h, i) => {
485
+ const prScore = h.prData?.score?.toFixed(6) ?? "N/A";
486
+ const prRank = h.prData?.rank ?? "?";
487
+ const prPercentile = h.prData?.percentile ?? "?";
488
+ lines.push(`**#${i + 1} ${h.relativePath}**`);
489
+ lines.push(` PageRank: ${prScore} (rank #${prRank}, ${prPercentile}th percentile)`);
490
+ lines.push(` Dependents: ${h.dependentCount} (direct)`);
491
+ // Try to get risk score
492
+ const absPath = path.normalize(path.join(graph.sourceDir, h.relativePath));
493
+ try {
494
+ const rs = (0, risk_scorer_1.getRiskScore)(absPath, graph);
495
+ lines.push(` Risk Score: ${rs.composite.toFixed(2)} (${rs.riskLevel})`);
496
+ }
497
+ catch {
498
+ lines.push(` Risk: ${h.riskLevel}`);
499
+ }
500
+ lines.push("");
501
+ });
502
+ lines.push(`Total files in graph: ${graph.files.size}`);
503
+ return { content: [{ type: "text", text: lines.join("\n") }] };
504
+ }
505
+ // Fallback: original table format (no PageRank data)
371
506
  const lines = [
372
507
  `## Hub Files (Top ${hubs.length})`,
373
508
  "",
@@ -383,11 +518,13 @@ async function main() {
383
518
  }
384
519
  case "refresh_graph": {
385
520
  const graph = (0, graph_1.refreshGraph)(currentProjectRoot, currentPackageName);
521
+ (0, change_coupling_1.invalidateCouplingCache)();
522
+ const cacheStats = (0, analyze_impact_1.getImpactMemoCache)().stats();
386
523
  return {
387
524
  content: [
388
525
  {
389
526
  type: "text",
390
- text: `Graph refreshed (${graph.languages.join("+")}): ${graph.files.size} files scanned.`,
527
+ text: `Graph refreshed (${graph.languages.join("+")}): ${graph.files.size} files scanned. Change coupling cache invalidated. Memo cache cleared (was ${cacheStats.size} entries, ${cacheStats.hits} hits / ${cacheStats.misses} misses).`,
391
528
  },
392
529
  ],
393
530
  };
@@ -416,7 +553,7 @@ async function main() {
416
553
  if (!isFileInFreeSet(resolved, graph)) {
417
554
  return { content: [{ type: "text", text: PRO_UPGRADE_MSG }] };
418
555
  }
419
- const impactResult = (0, analyze_impact_1.analyzeImpact)(resolved, graph);
556
+ const impactResult = await (0, analyze_impact_1.analyzeImpact)(resolved, graph);
420
557
  const aiResult = await (0, analyzer_1.analyzeWithAI)(resolved, impactResult, graph);
421
558
  // Free tier: append partial analysis warning
422
559
  let resultText = aiResult;
@@ -523,6 +660,7 @@ async function main() {
523
660
  // Initialize file cache (load ALL source files into memory)
524
661
  fileCache = new file_cache_1.FileCache(currentProjectRoot);
525
662
  fileCache.initialize();
663
+ fileCache.setGraph(graph); // Enable incremental graph updates on file changes
526
664
  fileCache.startWatching();
527
665
  }
528
666
  // Web server handle (set after server starts)
@@ -537,13 +675,15 @@ async function main() {
537
675
  fileCache.stop();
538
676
  fileCache = new file_cache_1.FileCache(newRoot);
539
677
  fileCache.initialize();
678
+ // Rebuild graph
679
+ const graph = (0, graph_1.refreshGraph)(newRoot, currentPackageName);
680
+ // Enable incremental updates on the new cache
681
+ fileCache.setGraph(graph);
540
682
  fileCache.startWatching();
541
683
  // Re-wire SSE events to the new FileCache
542
684
  if (webServerHandle) {
543
685
  webServerHandle.setFileCache(fileCache);
544
686
  }
545
- // Rebuild graph
546
- const graph = (0, graph_1.refreshGraph)(newRoot, currentPackageName);
547
687
  console.error(`[syke] Switched to project: ${newRoot}`);
548
688
  console.error(`[syke] Languages: ${plugins.map(p => p.name).join(", ")}`);
549
689
  console.error(`[syke] Package: ${currentPackageName}`);
@@ -0,0 +1,67 @@
1
+ /**
2
+ * PageRank scoring for SYKE dependency graphs.
3
+ *
4
+ * Uses the Power Iteration algorithm to compute recursive importance scores.
5
+ * A file imported by many important files ranks higher than one imported by
6
+ * many leaf files. This provides a more nuanced importance signal than
7
+ * simple reverse dependent count (fan-in).
8
+ *
9
+ * In dependency graph terms:
10
+ * - If A imports B, the forward edge is A -> B.
11
+ * - B receives importance from A (B is important because A depends on it).
12
+ * - So we iterate over graph.reverse to find incoming "importance links".
13
+ *
14
+ * Dangling nodes (files that import nothing) distribute their rank
15
+ * equally to all nodes, preventing rank from leaking out of the graph.
16
+ */
17
+ import { DependencyGraph } from "../graph";
18
+ export interface PageRankOptions {
19
+ dampingFactor?: number;
20
+ maxIterations?: number;
21
+ tolerance?: number;
22
+ }
23
+ export interface PageRankResult {
24
+ scores: Map<string, number>;
25
+ ranked: RankedFile[];
26
+ iterations: number;
27
+ computedAt: number;
28
+ }
29
+ export interface RankedFile {
30
+ filePath: string;
31
+ relativePath: string;
32
+ score: number;
33
+ rank: number;
34
+ percentile: number;
35
+ }
36
+ /**
37
+ * Invalidate the cached PageRank result.
38
+ * Call this when the dependency graph is rebuilt.
39
+ */
40
+ export declare function invalidatePageRank(): void;
41
+ /**
42
+ * Compute PageRank scores for all files in the dependency graph
43
+ * using the Power Iteration method.
44
+ *
45
+ * The algorithm:
46
+ * 1. Initialize rank[i] = 1/N for all N files.
47
+ * 2. Repeat until convergence or maxIterations:
48
+ * For each file i:
49
+ * newRank[i] = (1 - d) / N + d * SUM(rank[j] / outDegree[j])
50
+ * for all j that link to i (j imports i -> j is in reverse[i])
51
+ * Handle dangling nodes: files with outDegree=0 distribute rank to all.
52
+ * If max|newRank - rank| < tolerance: break
53
+ * rank = newRank
54
+ *
55
+ * Direction clarification:
56
+ * - forward[A] = [B] means "A imports B" (A -> B).
57
+ * - reverse[B] = [A] means "A imports B" — A gives importance to B.
58
+ * - outDegree of A = forward[A].length (how many files A imports).
59
+ * - When computing rank for B, sum over reverse[B]: each file A that
60
+ * imports B contributes rank[A] / outDegree[A] to B.
61
+ */
62
+ export declare function computePageRank(graph: DependencyGraph, options?: PageRankOptions): PageRankResult;
63
+ /**
64
+ * O(1) lookup of a file's PageRank data from a precomputed result.
65
+ * Returns null if the file is not in the result.
66
+ */
67
+ export declare function getFileRank(filePath: string, result: PageRankResult): RankedFile | null;