@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,221 @@
1
+ "use strict";
2
+ /**
3
+ * PageRank scoring for SYKE dependency graphs.
4
+ *
5
+ * Uses the Power Iteration algorithm to compute recursive importance scores.
6
+ * A file imported by many important files ranks higher than one imported by
7
+ * many leaf files. This provides a more nuanced importance signal than
8
+ * simple reverse dependent count (fan-in).
9
+ *
10
+ * In dependency graph terms:
11
+ * - If A imports B, the forward edge is A -> B.
12
+ * - B receives importance from A (B is important because A depends on it).
13
+ * - So we iterate over graph.reverse to find incoming "importance links".
14
+ *
15
+ * Dangling nodes (files that import nothing) distribute their rank
16
+ * equally to all nodes, preventing rank from leaking out of the graph.
17
+ */
18
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ var desc = Object.getOwnPropertyDescriptor(m, k);
21
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
+ desc = { enumerable: true, get: function() { return m[k]; } };
23
+ }
24
+ Object.defineProperty(o, k2, desc);
25
+ }) : (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ o[k2] = m[k];
28
+ }));
29
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
31
+ }) : function(o, v) {
32
+ o["default"] = v;
33
+ });
34
+ var __importStar = (this && this.__importStar) || (function () {
35
+ var ownKeys = function(o) {
36
+ ownKeys = Object.getOwnPropertyNames || function (o) {
37
+ var ar = [];
38
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
+ return ar;
40
+ };
41
+ return ownKeys(o);
42
+ };
43
+ return function (mod) {
44
+ if (mod && mod.__esModule) return mod;
45
+ var result = {};
46
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
+ __setModuleDefault(result, mod);
48
+ return result;
49
+ };
50
+ })();
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.invalidatePageRank = invalidatePageRank;
53
+ exports.computePageRank = computePageRank;
54
+ exports.getFileRank = getFileRank;
55
+ const path = __importStar(require("path"));
56
+ // ── Module-level Cache ──
57
+ let cachedResult = null;
58
+ /**
59
+ * Invalidate the cached PageRank result.
60
+ * Call this when the dependency graph is rebuilt.
61
+ */
62
+ function invalidatePageRank() {
63
+ cachedResult = null;
64
+ }
65
+ // ── Core Algorithm ──
66
+ /**
67
+ * Compute PageRank scores for all files in the dependency graph
68
+ * using the Power Iteration method.
69
+ *
70
+ * The algorithm:
71
+ * 1. Initialize rank[i] = 1/N for all N files.
72
+ * 2. Repeat until convergence or maxIterations:
73
+ * For each file i:
74
+ * newRank[i] = (1 - d) / N + d * SUM(rank[j] / outDegree[j])
75
+ * for all j that link to i (j imports i -> j is in reverse[i])
76
+ * Handle dangling nodes: files with outDegree=0 distribute rank to all.
77
+ * If max|newRank - rank| < tolerance: break
78
+ * rank = newRank
79
+ *
80
+ * Direction clarification:
81
+ * - forward[A] = [B] means "A imports B" (A -> B).
82
+ * - reverse[B] = [A] means "A imports B" — A gives importance to B.
83
+ * - outDegree of A = forward[A].length (how many files A imports).
84
+ * - When computing rank for B, sum over reverse[B]: each file A that
85
+ * imports B contributes rank[A] / outDegree[A] to B.
86
+ */
87
+ function computePageRank(graph, options) {
88
+ // Return cached result if available
89
+ if (cachedResult) {
90
+ return cachedResult;
91
+ }
92
+ const d = options?.dampingFactor ?? 0.85;
93
+ const maxIter = options?.maxIterations ?? 100;
94
+ const tol = options?.tolerance ?? 1e-6;
95
+ const files = [...graph.files];
96
+ const N = files.length;
97
+ // Handle empty graph
98
+ if (N === 0) {
99
+ const result = {
100
+ scores: new Map(),
101
+ ranked: [],
102
+ iterations: 0,
103
+ computedAt: Date.now(),
104
+ };
105
+ cachedResult = result;
106
+ return result;
107
+ }
108
+ // Build index maps for fast array-based iteration
109
+ const fileToIdx = new Map();
110
+ for (let i = 0; i < N; i++) {
111
+ fileToIdx.set(files[i], i);
112
+ }
113
+ // Compute out-degree for each file (number of files it imports)
114
+ const outDegree = new Float64Array(N);
115
+ for (let i = 0; i < N; i++) {
116
+ const deps = graph.forward.get(files[i]);
117
+ outDegree[i] = deps ? deps.length : 0;
118
+ }
119
+ // Identify dangling nodes (files with no outgoing edges / no imports)
120
+ const danglingNodes = [];
121
+ for (let i = 0; i < N; i++) {
122
+ if (outDegree[i] === 0) {
123
+ danglingNodes.push(i);
124
+ }
125
+ }
126
+ // Build reverse adjacency list as index arrays for performance
127
+ // reverseAdj[i] = list of indices j where file j imports file i
128
+ const reverseAdj = new Array(N);
129
+ for (let i = 0; i < N; i++) {
130
+ reverseAdj[i] = [];
131
+ }
132
+ for (const [target, sources] of graph.reverse) {
133
+ const targetIdx = fileToIdx.get(target);
134
+ if (targetIdx === undefined)
135
+ continue;
136
+ for (const source of sources) {
137
+ const sourceIdx = fileToIdx.get(source);
138
+ if (sourceIdx !== undefined) {
139
+ reverseAdj[targetIdx].push(sourceIdx);
140
+ }
141
+ }
142
+ }
143
+ // Initialize ranks
144
+ let rank = new Float64Array(N);
145
+ const initRank = 1.0 / N;
146
+ for (let i = 0; i < N; i++) {
147
+ rank[i] = initRank;
148
+ }
149
+ let iterations = 0;
150
+ const baseTeleport = (1.0 - d) / N;
151
+ for (let iter = 0; iter < maxIter; iter++) {
152
+ iterations = iter + 1;
153
+ // Compute dangling rank sum: total rank held by dangling nodes
154
+ let danglingSum = 0;
155
+ for (const idx of danglingNodes) {
156
+ danglingSum += rank[idx];
157
+ }
158
+ // Each node gets an equal share of the dangling rank (redistributed)
159
+ const danglingContribution = d * danglingSum / N;
160
+ const newRank = new Float64Array(N);
161
+ for (let i = 0; i < N; i++) {
162
+ // Sum contributions from all files that import file i
163
+ let incomingSum = 0;
164
+ for (const j of reverseAdj[i]) {
165
+ incomingSum += rank[j] / outDegree[j];
166
+ }
167
+ newRank[i] = baseTeleport + danglingContribution + d * incomingSum;
168
+ }
169
+ // Check convergence: max absolute difference
170
+ let maxDiff = 0;
171
+ for (let i = 0; i < N; i++) {
172
+ const diff = Math.abs(newRank[i] - rank[i]);
173
+ if (diff > maxDiff)
174
+ maxDiff = diff;
175
+ }
176
+ rank = newRank;
177
+ if (maxDiff < tol) {
178
+ break;
179
+ }
180
+ }
181
+ // Build scores map
182
+ const scores = new Map();
183
+ for (let i = 0; i < N; i++) {
184
+ scores.set(files[i], rank[i]);
185
+ }
186
+ // Build sorted ranked list
187
+ const indexedScores = [];
188
+ for (let i = 0; i < N; i++) {
189
+ indexedScores.push({ idx: i, score: rank[i] });
190
+ }
191
+ indexedScores.sort((a, b) => b.score - a.score);
192
+ const ranked = indexedScores.map((entry, position) => ({
193
+ filePath: files[entry.idx],
194
+ relativePath: path.relative(graph.sourceDir, files[entry.idx]).replace(/\\/g, "/"),
195
+ score: entry.score,
196
+ rank: position + 1,
197
+ percentile: Math.round(((N - 1 - position) / Math.max(1, N - 1)) * 100),
198
+ }));
199
+ const result = {
200
+ scores,
201
+ ranked,
202
+ iterations,
203
+ computedAt: Date.now(),
204
+ };
205
+ cachedResult = result;
206
+ console.error(`[syke:pagerank] Computed PageRank for ${N} files in ${iterations} iterations`);
207
+ return result;
208
+ }
209
+ // ── Lookup ──
210
+ /**
211
+ * O(1) lookup of a file's PageRank data from a precomputed result.
212
+ * Returns null if the file is not in the result.
213
+ */
214
+ function getFileRank(filePath, result) {
215
+ const score = result.scores.get(filePath);
216
+ if (score === undefined)
217
+ return null;
218
+ // Find the ranked entry (scores map guarantees it exists in ranked array)
219
+ const entry = result.ranked.find(r => r.filePath === filePath);
220
+ return entry || null;
221
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Composite Risk Scoring for SYKE.
3
+ *
4
+ * Combines multiple signals (fan-in, instability, cyclomatic complexity,
5
+ * cascade depth) into a single 0-1 risk score using weighted normalization.
6
+ *
7
+ * Based on Robert C. Martin's stability metrics and standard software
8
+ * engineering coupling/cohesion analysis.
9
+ */
10
+ import { DependencyGraph } from "../graph";
11
+ export interface CouplingMetrics {
12
+ fanIn: number;
13
+ fanOut: number;
14
+ transitiveFanIn: number;
15
+ }
16
+ export type CompositeRiskLevel = "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "SAFE";
17
+ export interface RiskScore {
18
+ composite: number;
19
+ fanIn: number;
20
+ fanOut: number;
21
+ transitiveFanIn: number;
22
+ instability: number;
23
+ complexity: number;
24
+ normalizedComplexity: number;
25
+ cascadeDepth: number;
26
+ riskLevel: CompositeRiskLevel;
27
+ pageRank?: number;
28
+ pageRankPercentile?: number;
29
+ }
30
+ export interface ProjectMetrics {
31
+ maxFanIn: number;
32
+ maxTransitiveFanIn: number;
33
+ maxComplexity: number;
34
+ maxCascadeDepth: number;
35
+ fileMetrics: Map<string, RiskScore>;
36
+ }
37
+ export declare const RISK_WEIGHTS: {
38
+ fanIn: number;
39
+ stability: number;
40
+ complexity: number;
41
+ cascadeDepth: number;
42
+ pageRank: number;
43
+ };
44
+ /**
45
+ * Invalidate cached project metrics. Call when the graph is rebuilt.
46
+ */
47
+ export declare function invalidateProjectMetrics(): void;
48
+ /**
49
+ * Compute coupling metrics for a single file from the dependency graph.
50
+ */
51
+ export declare function computeCouplingMetrics(filePath: string, graph: DependencyGraph): CouplingMetrics;
52
+ /**
53
+ * Robert C. Martin's Instability Index.
54
+ *
55
+ * I = Ce / (Ca + Ce) where Ca = fanIn, Ce = fanOut
56
+ * 0 = maximally stable (everything depends on it, dangerous to change)
57
+ * 1 = maximally unstable (leaf node, safe to change)
58
+ */
59
+ export declare function computeInstability(fanIn: number, fanOut: number): number;
60
+ /**
61
+ * Compute cyclomatic complexity of source code using regex-based
62
+ * decision point counting.
63
+ *
64
+ * Returns the raw count of decision points (base complexity of 1 is NOT added).
65
+ */
66
+ export declare function computeComplexity(content: string, language: string): number;
67
+ /**
68
+ * Compute the maximum cascade depth from the condensed DAG.
69
+ * This is the longest path from the file's SCC through the reverse edges
70
+ * of the condensed DAG (i.e., how many layers deep the impact propagates).
71
+ */
72
+ export declare function computeCascadeDepth(filePath: string, graph: DependencyGraph): number;
73
+ /**
74
+ * Map a composite score (0-1) to a risk level.
75
+ */
76
+ export declare function classifyCompositeRisk(score: number): CompositeRiskLevel;
77
+ /**
78
+ * Compute the composite risk score for a single file.
79
+ *
80
+ * If `projectMetrics` is provided, uses pre-computed normalization bounds.
81
+ * Otherwise, uses raw metrics without normalization (less accurate but functional).
82
+ */
83
+ export declare function computeRiskScore(filePath: string, graph: DependencyGraph, fileContent: string | null, projectMetrics?: ProjectMetrics): RiskScore;
84
+ /**
85
+ * Pre-compute metrics for all files in the project.
86
+ * This establishes normalization bounds and caches per-file scores.
87
+ *
88
+ * Uses lazy initialization and caches results until invalidated.
89
+ */
90
+ export declare function computeProjectMetrics(graph: DependencyGraph, getFileContent?: (path: string) => string | null): ProjectMetrics;
91
+ /**
92
+ * Get the cached risk score for a file, or compute it on the fly.
93
+ * Uses project-wide normalization when available.
94
+ */
95
+ export declare function getRiskScore(filePath: string, graph: DependencyGraph, fileContent?: string | null): RiskScore;
96
+ /**
97
+ * Format a risk score for display in MCP tool output.
98
+ */
99
+ export declare function formatRiskScore(score: RiskScore): string;