@syke1/mcp-server 1.0.0
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/README.md +112 -0
- package/dist/ai/analyzer.d.ts +3 -0
- package/dist/ai/analyzer.js +120 -0
- package/dist/ai/realtime-analyzer.d.ts +20 -0
- package/dist/ai/realtime-analyzer.js +182 -0
- package/dist/graph.d.ts +13 -0
- package/dist/graph.js +105 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +518 -0
- package/dist/languages/cpp.d.ts +2 -0
- package/dist/languages/cpp.js +109 -0
- package/dist/languages/dart.d.ts +2 -0
- package/dist/languages/dart.js +162 -0
- package/dist/languages/go.d.ts +2 -0
- package/dist/languages/go.js +111 -0
- package/dist/languages/java.d.ts +2 -0
- package/dist/languages/java.js +113 -0
- package/dist/languages/plugin.d.ts +20 -0
- package/dist/languages/plugin.js +148 -0
- package/dist/languages/python.d.ts +2 -0
- package/dist/languages/python.js +129 -0
- package/dist/languages/ruby.d.ts +2 -0
- package/dist/languages/ruby.js +97 -0
- package/dist/languages/rust.d.ts +2 -0
- package/dist/languages/rust.js +121 -0
- package/dist/languages/typescript.d.ts +2 -0
- package/dist/languages/typescript.js +138 -0
- package/dist/license/validator.d.ts +23 -0
- package/dist/license/validator.js +297 -0
- package/dist/tools/analyze-impact.d.ts +23 -0
- package/dist/tools/analyze-impact.js +102 -0
- package/dist/tools/gate-build.d.ts +25 -0
- package/dist/tools/gate-build.js +243 -0
- package/dist/watcher/file-cache.d.ts +56 -0
- package/dist/watcher/file-cache.js +241 -0
- package/dist/web/public/app.js +2398 -0
- package/dist/web/public/index.html +258 -0
- package/dist/web/public/style.css +1827 -0
- package/dist/web/server.d.ts +29 -0
- package/dist/web/server.js +744 -0
- package/package.json +50 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.startHeartbeat = startHeartbeat;
|
|
37
|
+
exports.stopAndDeactivate = stopAndDeactivate;
|
|
38
|
+
exports.getDeviceId = getDeviceId;
|
|
39
|
+
exports.checkLicense = checkLicense;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const os = __importStar(require("os"));
|
|
43
|
+
const https = __importStar(require("https"));
|
|
44
|
+
const crypto = __importStar(require("crypto"));
|
|
45
|
+
const CACHE_DIR = path.join(os.homedir(), ".syke");
|
|
46
|
+
const CACHE_FILE = path.join(CACHE_DIR, ".license-cache.json");
|
|
47
|
+
const CONFIG_FILE = path.join(CACHE_DIR, "config.json");
|
|
48
|
+
// Cache durations
|
|
49
|
+
const CACHE_MAX_AGE = 24 * 60 * 60 * 1000; // 24 hours
|
|
50
|
+
const CACHE_GRACE_PERIOD = 7 * 24 * 60 * 60 * 1000; // 7 days offline grace
|
|
51
|
+
// Heartbeat interval
|
|
52
|
+
const HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
53
|
+
// Cloud Function URLs
|
|
54
|
+
const BASE_URL = "https://us-central1-syke-cloud.cloudfunctions.net";
|
|
55
|
+
const VALIDATE_URL = `${BASE_URL}/validateLicenseKey`;
|
|
56
|
+
const HEARTBEAT_URL = `${BASE_URL}/licenseHeartbeat`;
|
|
57
|
+
const DEACTIVATE_URL = `${BASE_URL}/licenseDeactivate`;
|
|
58
|
+
// Module state for heartbeat
|
|
59
|
+
let heartbeatTimer = null;
|
|
60
|
+
let currentLicenseKey = null;
|
|
61
|
+
let currentDeviceId = null;
|
|
62
|
+
/**
|
|
63
|
+
* Generate a stable device fingerprint from machine info
|
|
64
|
+
*/
|
|
65
|
+
function getDeviceFingerprint() {
|
|
66
|
+
const raw = `${os.hostname()}:${os.userInfo().username}:${os.platform()}:${os.arch()}`;
|
|
67
|
+
return crypto.createHash("sha256").update(raw).digest("hex").substring(0, 16);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Human-readable device name for dashboard display
|
|
71
|
+
*/
|
|
72
|
+
function getDeviceName() {
|
|
73
|
+
const platformNames = {
|
|
74
|
+
win32: "Windows",
|
|
75
|
+
darwin: "macOS",
|
|
76
|
+
linux: "Linux",
|
|
77
|
+
};
|
|
78
|
+
const platformName = platformNames[os.platform()] || os.platform();
|
|
79
|
+
return `${os.hostname()} (${platformName})`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Read license key from env var or config file
|
|
83
|
+
*/
|
|
84
|
+
function getLicenseKey() {
|
|
85
|
+
// 1. Environment variable
|
|
86
|
+
const envKey = process.env.SYKE_LICENSE_KEY;
|
|
87
|
+
if (envKey && envKey.startsWith("SYKE-"))
|
|
88
|
+
return envKey;
|
|
89
|
+
// 2. Config file
|
|
90
|
+
try {
|
|
91
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
92
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
93
|
+
if (config.licenseKey && config.licenseKey.startsWith("SYKE-"))
|
|
94
|
+
return config.licenseKey;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// ignore
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Read cached license validation result
|
|
104
|
+
*/
|
|
105
|
+
function readCache() {
|
|
106
|
+
try {
|
|
107
|
+
if (!fs.existsSync(CACHE_FILE))
|
|
108
|
+
return null;
|
|
109
|
+
const data = JSON.parse(fs.readFileSync(CACHE_FILE, "utf-8"));
|
|
110
|
+
if (!data || typeof data.cachedAt !== "number")
|
|
111
|
+
return null;
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Write license validation result to cache
|
|
120
|
+
*/
|
|
121
|
+
function writeCache(data) {
|
|
122
|
+
try {
|
|
123
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
124
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// silently fail — cache is optional
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* POST JSON to a URL, return parsed response
|
|
134
|
+
*/
|
|
135
|
+
function postJSON(url, body) {
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
const payload = JSON.stringify(body);
|
|
138
|
+
const parsedUrl = new URL(url);
|
|
139
|
+
const req = https.request({
|
|
140
|
+
hostname: parsedUrl.hostname,
|
|
141
|
+
path: parsedUrl.pathname,
|
|
142
|
+
method: "POST",
|
|
143
|
+
timeout: 10000,
|
|
144
|
+
headers: {
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
147
|
+
},
|
|
148
|
+
}, (res) => {
|
|
149
|
+
let data = "";
|
|
150
|
+
res.on("data", (chunk) => { data += chunk.toString(); });
|
|
151
|
+
res.on("end", () => {
|
|
152
|
+
try {
|
|
153
|
+
resolve(JSON.parse(data));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
resolve({ valid: false });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
req.on("error", () => resolve({ valid: false }));
|
|
161
|
+
req.on("timeout", () => { req.destroy(); resolve({ valid: false }); });
|
|
162
|
+
req.write(payload);
|
|
163
|
+
req.end();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Validate license online with device binding
|
|
168
|
+
*/
|
|
169
|
+
async function validateOnline(key) {
|
|
170
|
+
const deviceId = getDeviceFingerprint();
|
|
171
|
+
const deviceName = getDeviceName();
|
|
172
|
+
return postJSON(VALIDATE_URL, { key, deviceId, deviceName });
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Send heartbeat to keep session alive
|
|
176
|
+
*/
|
|
177
|
+
async function sendHeartbeat() {
|
|
178
|
+
if (!currentLicenseKey || !currentDeviceId)
|
|
179
|
+
return;
|
|
180
|
+
try {
|
|
181
|
+
await postJSON(HEARTBEAT_URL, {
|
|
182
|
+
key: currentLicenseKey,
|
|
183
|
+
deviceId: currentDeviceId,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// silently fail — next heartbeat will retry
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Deactivate session — called on graceful shutdown
|
|
192
|
+
*/
|
|
193
|
+
async function sendDeactivate() {
|
|
194
|
+
if (!currentLicenseKey || !currentDeviceId)
|
|
195
|
+
return;
|
|
196
|
+
try {
|
|
197
|
+
await postJSON(DEACTIVATE_URL, {
|
|
198
|
+
key: currentLicenseKey,
|
|
199
|
+
deviceId: currentDeviceId,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// best-effort — server will timeout the session anyway
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Start heartbeat interval (called after successful Pro validation)
|
|
208
|
+
*/
|
|
209
|
+
function startHeartbeat(key, deviceId) {
|
|
210
|
+
currentLicenseKey = key;
|
|
211
|
+
currentDeviceId = deviceId;
|
|
212
|
+
if (heartbeatTimer)
|
|
213
|
+
clearInterval(heartbeatTimer);
|
|
214
|
+
heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Stop heartbeat and deactivate session (called on shutdown)
|
|
218
|
+
*/
|
|
219
|
+
async function stopAndDeactivate() {
|
|
220
|
+
if (heartbeatTimer) {
|
|
221
|
+
clearInterval(heartbeatTimer);
|
|
222
|
+
heartbeatTimer = null;
|
|
223
|
+
}
|
|
224
|
+
await sendDeactivate();
|
|
225
|
+
currentLicenseKey = null;
|
|
226
|
+
currentDeviceId = null;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get current device fingerprint (exported for use by index.ts)
|
|
230
|
+
*/
|
|
231
|
+
function getDeviceId() {
|
|
232
|
+
return getDeviceFingerprint();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Main license validation — called on MCP server startup
|
|
236
|
+
*/
|
|
237
|
+
async function checkLicense() {
|
|
238
|
+
const key = getLicenseKey();
|
|
239
|
+
// No key → Free mode
|
|
240
|
+
if (!key) {
|
|
241
|
+
return { plan: "free", source: "default" };
|
|
242
|
+
}
|
|
243
|
+
// Check cache first
|
|
244
|
+
const cache = readCache();
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
if (cache && cache.valid && (now - cache.cachedAt) < CACHE_MAX_AGE) {
|
|
247
|
+
// Cache is fresh — still start heartbeat with cached session
|
|
248
|
+
startHeartbeat(key, getDeviceFingerprint());
|
|
249
|
+
return {
|
|
250
|
+
plan: "pro",
|
|
251
|
+
email: cache.email,
|
|
252
|
+
expiresAt: cache.expiresAt,
|
|
253
|
+
source: "cache",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// Try online validation (with device binding)
|
|
257
|
+
const result = await validateOnline(key);
|
|
258
|
+
if (result.valid) {
|
|
259
|
+
// Update cache
|
|
260
|
+
writeCache({
|
|
261
|
+
valid: true,
|
|
262
|
+
plan: result.plan,
|
|
263
|
+
email: result.email,
|
|
264
|
+
expiresAt: result.expiresAt,
|
|
265
|
+
cachedAt: now,
|
|
266
|
+
});
|
|
267
|
+
// Start heartbeat
|
|
268
|
+
startHeartbeat(key, getDeviceFingerprint());
|
|
269
|
+
return {
|
|
270
|
+
plan: "pro",
|
|
271
|
+
email: result.email,
|
|
272
|
+
expiresAt: result.expiresAt,
|
|
273
|
+
source: "online",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// Device/session error — pass through error message
|
|
277
|
+
if (result.error) {
|
|
278
|
+
return {
|
|
279
|
+
plan: "free",
|
|
280
|
+
source: "online",
|
|
281
|
+
error: result.error,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// Online validation failed — check if we have a grace-period cache
|
|
285
|
+
if (cache && cache.valid && (now - cache.cachedAt) < CACHE_GRACE_PERIOD) {
|
|
286
|
+
startHeartbeat(key, getDeviceFingerprint());
|
|
287
|
+
return {
|
|
288
|
+
plan: "pro",
|
|
289
|
+
email: cache.email,
|
|
290
|
+
expiresAt: cache.expiresAt,
|
|
291
|
+
source: "cache",
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
// No valid cache within grace period → Free mode
|
|
295
|
+
writeCache({ valid: false, cachedAt: now });
|
|
296
|
+
return { plan: "free", source: "default" };
|
|
297
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { DependencyGraph } from "../graph";
|
|
2
|
+
export type RiskLevel = "HIGH" | "MEDIUM" | "LOW" | "NONE";
|
|
3
|
+
export interface ImpactResult {
|
|
4
|
+
filePath: string;
|
|
5
|
+
relativePath: string;
|
|
6
|
+
riskLevel: RiskLevel;
|
|
7
|
+
directDependents: string[];
|
|
8
|
+
transitiveDependents: string[];
|
|
9
|
+
totalImpacted: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* BFS reverse traversal to find all files impacted by modifying `filePath`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function analyzeImpact(filePath: string, graph: DependencyGraph): ImpactResult;
|
|
15
|
+
export declare function classifyRisk(count: number): RiskLevel;
|
|
16
|
+
/**
|
|
17
|
+
* Rank files by number of reverse dependents (hub score).
|
|
18
|
+
*/
|
|
19
|
+
export declare function getHubFiles(graph: DependencyGraph, topN?: number): Array<{
|
|
20
|
+
relativePath: string;
|
|
21
|
+
dependentCount: number;
|
|
22
|
+
riskLevel: RiskLevel;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.analyzeImpact = analyzeImpact;
|
|
37
|
+
exports.classifyRisk = classifyRisk;
|
|
38
|
+
exports.getHubFiles = getHubFiles;
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
/**
|
|
41
|
+
* BFS reverse traversal to find all files impacted by modifying `filePath`.
|
|
42
|
+
*/
|
|
43
|
+
function analyzeImpact(filePath, graph) {
|
|
44
|
+
const normalized = path.normalize(filePath);
|
|
45
|
+
// Direct dependents (depth 1)
|
|
46
|
+
const directDependents = graph.reverse.get(normalized) || [];
|
|
47
|
+
// BFS for transitive dependents (all depths)
|
|
48
|
+
const visited = new Set();
|
|
49
|
+
const queue = [...directDependents];
|
|
50
|
+
for (const d of directDependents) {
|
|
51
|
+
visited.add(d);
|
|
52
|
+
}
|
|
53
|
+
while (queue.length > 0) {
|
|
54
|
+
const current = queue.shift();
|
|
55
|
+
const dependents = graph.reverse.get(current) || [];
|
|
56
|
+
for (const dep of dependents) {
|
|
57
|
+
if (!visited.has(dep) && dep !== normalized) {
|
|
58
|
+
visited.add(dep);
|
|
59
|
+
queue.push(dep);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const totalImpacted = visited.size;
|
|
64
|
+
// Transitive-only = all visited minus direct
|
|
65
|
+
const directSet = new Set(directDependents);
|
|
66
|
+
const transitiveDependents = [...visited].filter((f) => !directSet.has(f));
|
|
67
|
+
const riskLevel = classifyRisk(totalImpacted);
|
|
68
|
+
const toRelative = (f) => path.relative(graph.sourceDir, f).replace(/\\/g, "/");
|
|
69
|
+
return {
|
|
70
|
+
filePath: normalized,
|
|
71
|
+
relativePath: toRelative(normalized),
|
|
72
|
+
riskLevel,
|
|
73
|
+
directDependents: directDependents.map(toRelative),
|
|
74
|
+
transitiveDependents: transitiveDependents.map(toRelative),
|
|
75
|
+
totalImpacted,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function classifyRisk(count) {
|
|
79
|
+
if (count >= 10)
|
|
80
|
+
return "HIGH";
|
|
81
|
+
if (count >= 5)
|
|
82
|
+
return "MEDIUM";
|
|
83
|
+
if (count >= 1)
|
|
84
|
+
return "LOW";
|
|
85
|
+
return "NONE";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Rank files by number of reverse dependents (hub score).
|
|
89
|
+
*/
|
|
90
|
+
function getHubFiles(graph, topN = 10) {
|
|
91
|
+
const entries = [];
|
|
92
|
+
for (const file of graph.files) {
|
|
93
|
+
const revDeps = graph.reverse.get(file) || [];
|
|
94
|
+
entries.push({ file, count: revDeps.length });
|
|
95
|
+
}
|
|
96
|
+
entries.sort((a, b) => b.count - a.count);
|
|
97
|
+
return entries.slice(0, topN).map((e) => ({
|
|
98
|
+
relativePath: path.relative(graph.sourceDir, e.file).replace(/\\/g, "/"),
|
|
99
|
+
dependentCount: e.count,
|
|
100
|
+
riskLevel: classifyRisk(e.count),
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DependencyGraph } from "../graph";
|
|
2
|
+
export type GateVerdict = "PASS" | "WARN" | "FAIL";
|
|
3
|
+
export interface GateIssue {
|
|
4
|
+
file: string;
|
|
5
|
+
severity: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
export interface GateResult {
|
|
9
|
+
verdict: GateVerdict;
|
|
10
|
+
statusLine: string;
|
|
11
|
+
issues: GateIssue[];
|
|
12
|
+
recommendation: string;
|
|
13
|
+
stats: {
|
|
14
|
+
filesInGraph: number;
|
|
15
|
+
unresolvedWarnings: number;
|
|
16
|
+
};
|
|
17
|
+
autoAcknowledged: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Run the build gate check.
|
|
21
|
+
* If `specifiedFiles` is provided, only warnings for those files are considered.
|
|
22
|
+
* Cycle detection also starts from those files.
|
|
23
|
+
*/
|
|
24
|
+
export declare function gateCheck(graph: DependencyGraph, specifiedFiles?: string[]): GateResult;
|
|
25
|
+
export declare function formatGateResult(result: GateResult): string;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.gateCheck = gateCheck;
|
|
37
|
+
exports.formatGateResult = formatGateResult;
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const server_1 = require("../web/server");
|
|
40
|
+
const analyze_impact_1 = require("./analyze-impact");
|
|
41
|
+
/**
|
|
42
|
+
* DFS forward traversal from specified files to detect circular dependencies.
|
|
43
|
+
* Returns at most `maxCycles` cycles for performance.
|
|
44
|
+
*/
|
|
45
|
+
function detectCyclesForFiles(files, graph, maxCycles = 10) {
|
|
46
|
+
const cycles = [];
|
|
47
|
+
const globalVisited = new Set();
|
|
48
|
+
for (const startFile of files) {
|
|
49
|
+
if (!graph.files.has(startFile))
|
|
50
|
+
continue;
|
|
51
|
+
const stack = new Set();
|
|
52
|
+
const pathStack = [];
|
|
53
|
+
function dfs(file) {
|
|
54
|
+
if (cycles.length >= maxCycles)
|
|
55
|
+
return;
|
|
56
|
+
if (globalVisited.has(file))
|
|
57
|
+
return;
|
|
58
|
+
stack.add(file);
|
|
59
|
+
pathStack.push(file);
|
|
60
|
+
const deps = graph.forward.get(file) || [];
|
|
61
|
+
for (const dep of deps) {
|
|
62
|
+
if (cycles.length >= maxCycles)
|
|
63
|
+
break;
|
|
64
|
+
if (stack.has(dep)) {
|
|
65
|
+
// Back-edge found → cycle
|
|
66
|
+
const idx = pathStack.indexOf(dep);
|
|
67
|
+
if (idx >= 0) {
|
|
68
|
+
cycles.push({
|
|
69
|
+
cycle: [...pathStack.slice(idx), dep],
|
|
70
|
+
file: startFile,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (!globalVisited.has(dep)) {
|
|
75
|
+
dfs(dep);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
stack.delete(file);
|
|
79
|
+
pathStack.pop();
|
|
80
|
+
}
|
|
81
|
+
dfs(startFile);
|
|
82
|
+
// Mark all visited from this start as globally visited
|
|
83
|
+
for (const f of stack)
|
|
84
|
+
globalVisited.add(f);
|
|
85
|
+
}
|
|
86
|
+
return cycles.slice(0, maxCycles);
|
|
87
|
+
}
|
|
88
|
+
// ── Gate Check ──
|
|
89
|
+
/**
|
|
90
|
+
* Run the build gate check.
|
|
91
|
+
* If `specifiedFiles` is provided, only warnings for those files are considered.
|
|
92
|
+
* Cycle detection also starts from those files.
|
|
93
|
+
*/
|
|
94
|
+
function gateCheck(graph, specifiedFiles) {
|
|
95
|
+
const allWarnings = (0, server_1.getUnacknowledgedWarnings)();
|
|
96
|
+
const stats = {
|
|
97
|
+
filesInGraph: graph.files.size,
|
|
98
|
+
unresolvedWarnings: allWarnings.length,
|
|
99
|
+
};
|
|
100
|
+
// Filter warnings to specified files if provided
|
|
101
|
+
const warnings = specifiedFiles
|
|
102
|
+
? allWarnings.filter((w) => specifiedFiles.some((f) => w.file === f ||
|
|
103
|
+
f.endsWith(w.file) ||
|
|
104
|
+
w.file.endsWith(f.replace(/\\/g, "/"))))
|
|
105
|
+
: allWarnings;
|
|
106
|
+
const issues = [];
|
|
107
|
+
// 1. Check warnings by severity
|
|
108
|
+
for (const w of warnings) {
|
|
109
|
+
const severity = mapRiskToSeverity(w.riskLevel);
|
|
110
|
+
if (severity) {
|
|
111
|
+
issues.push({
|
|
112
|
+
file: w.file,
|
|
113
|
+
severity,
|
|
114
|
+
description: w.summary +
|
|
115
|
+
(w.brokenImports.length > 0
|
|
116
|
+
? ` (broken imports: ${w.brokenImports.join(", ")})`
|
|
117
|
+
: ""),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// 2. Detect cycles for specified files (or skip full-graph scan for perf)
|
|
122
|
+
const filesToCheck = specifiedFiles || [];
|
|
123
|
+
const cycles = detectCyclesForFiles(filesToCheck, graph, 10);
|
|
124
|
+
for (const c of cycles) {
|
|
125
|
+
const cyclePath = c.cycle
|
|
126
|
+
.map((f) => path.relative(graph.sourceDir, f).replace(/\\/g, "/"))
|
|
127
|
+
.join(" → ");
|
|
128
|
+
issues.push({
|
|
129
|
+
file: path.relative(graph.sourceDir, c.file).replace(/\\/g, "/"),
|
|
130
|
+
severity: "CRITICAL",
|
|
131
|
+
description: `Circular dependency: ${cyclePath}`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// 3. Check if hub files were modified
|
|
135
|
+
if (specifiedFiles) {
|
|
136
|
+
const hubs = (0, analyze_impact_1.getHubFiles)(graph, 5);
|
|
137
|
+
const hubPaths = new Set(hubs.map((h) => h.relativePath));
|
|
138
|
+
for (const f of specifiedFiles) {
|
|
139
|
+
const rel = path.relative(graph.sourceDir, f).replace(/\\/g, "/");
|
|
140
|
+
if (hubPaths.has(rel)) {
|
|
141
|
+
const hub = hubs.find((h) => h.relativePath === rel);
|
|
142
|
+
issues.push({
|
|
143
|
+
file: rel,
|
|
144
|
+
severity: "MEDIUM",
|
|
145
|
+
description: `Hub file modified (${hub.dependentCount} dependents, ${hub.riskLevel} risk)`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// ── Determine verdict ──
|
|
151
|
+
const hasCritical = issues.some((i) => i.severity === "CRITICAL");
|
|
152
|
+
const highIssues = issues.filter((i) => i.severity === "HIGH");
|
|
153
|
+
const hasHighWithBroken = highIssues.length > 0 &&
|
|
154
|
+
warnings.some((w) => w.riskLevel === "HIGH" && w.brokenImports.length > 0);
|
|
155
|
+
const hasCycles = cycles.length > 0;
|
|
156
|
+
let verdict;
|
|
157
|
+
let statusLine;
|
|
158
|
+
let recommendation;
|
|
159
|
+
let autoAcknowledged = 0;
|
|
160
|
+
if (hasCritical || hasHighWithBroken || hasCycles) {
|
|
161
|
+
verdict = "FAIL";
|
|
162
|
+
const reasons = [];
|
|
163
|
+
if (hasCritical)
|
|
164
|
+
reasons.push("CRITICAL warnings");
|
|
165
|
+
if (hasHighWithBroken)
|
|
166
|
+
reasons.push("HIGH warnings with broken imports");
|
|
167
|
+
if (hasCycles)
|
|
168
|
+
reasons.push(`${cycles.length} circular dependency(ies)`);
|
|
169
|
+
statusLine = `BUILD BLOCKED — ${reasons.join(", ")}`;
|
|
170
|
+
recommendation =
|
|
171
|
+
"Fix the issues above before building. Use `check_warnings` for details, then `analyze_impact` on affected files.";
|
|
172
|
+
}
|
|
173
|
+
else if (highIssues.length > 0 ||
|
|
174
|
+
issues.some((i) => i.severity === "MEDIUM")) {
|
|
175
|
+
verdict = "WARN";
|
|
176
|
+
statusLine = `PROCEED WITH CAUTION — ${issues.length} issue(s) detected`;
|
|
177
|
+
recommendation =
|
|
178
|
+
"Review the issues. If you've verified they're safe, proceed with the build. Use `check_warnings acknowledge=true` to clear warnings.";
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
verdict = "PASS";
|
|
182
|
+
statusLine = "All clear — safe to build";
|
|
183
|
+
recommendation = "No issues detected. You may proceed with build/deploy.";
|
|
184
|
+
// Auto-acknowledge warnings on PASS
|
|
185
|
+
autoAcknowledged = (0, server_1.acknowledgeWarnings)();
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
verdict,
|
|
189
|
+
statusLine,
|
|
190
|
+
issues,
|
|
191
|
+
recommendation,
|
|
192
|
+
stats,
|
|
193
|
+
autoAcknowledged,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// ── Formatting ──
|
|
197
|
+
function formatGateResult(result) {
|
|
198
|
+
const icon = result.verdict === "PASS"
|
|
199
|
+
? "✅"
|
|
200
|
+
: result.verdict === "WARN"
|
|
201
|
+
? "⚠️"
|
|
202
|
+
: "🚫";
|
|
203
|
+
const lines = [
|
|
204
|
+
`## SYKE Build Gate — ${icon} ${result.verdict}`,
|
|
205
|
+
"",
|
|
206
|
+
"### Status",
|
|
207
|
+
result.statusLine,
|
|
208
|
+
];
|
|
209
|
+
if (result.issues.length > 0) {
|
|
210
|
+
lines.push("", "### Issues");
|
|
211
|
+
for (const issue of result.issues) {
|
|
212
|
+
const issueIcon = issue.severity === "CRITICAL"
|
|
213
|
+
? "🚫"
|
|
214
|
+
: issue.severity === "HIGH"
|
|
215
|
+
? "🔴"
|
|
216
|
+
: issue.severity === "MEDIUM"
|
|
217
|
+
? "🟡"
|
|
218
|
+
: "🔵";
|
|
219
|
+
lines.push(`- ${issueIcon} **[${issue.severity}]** ${issue.file}: ${issue.description}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
lines.push("", "### Recommendation", result.recommendation);
|
|
223
|
+
lines.push("", "### Stats", `- Files in graph: ${result.stats.filesInGraph}`, `- Unresolved warnings: ${result.stats.unresolvedWarnings}`);
|
|
224
|
+
if (result.autoAcknowledged > 0) {
|
|
225
|
+
lines.push(`- Auto-acknowledged: ${result.autoAcknowledged} warning(s)`);
|
|
226
|
+
}
|
|
227
|
+
return lines.join("\n");
|
|
228
|
+
}
|
|
229
|
+
// ── Helpers ──
|
|
230
|
+
function mapRiskToSeverity(riskLevel) {
|
|
231
|
+
switch (riskLevel) {
|
|
232
|
+
case "CRITICAL":
|
|
233
|
+
return "CRITICAL";
|
|
234
|
+
case "HIGH":
|
|
235
|
+
return "HIGH";
|
|
236
|
+
case "MEDIUM":
|
|
237
|
+
return "MEDIUM";
|
|
238
|
+
case "LOW":
|
|
239
|
+
return "LOW";
|
|
240
|
+
default:
|
|
241
|
+
return null; // SAFE → no issue
|
|
242
|
+
}
|
|
243
|
+
}
|