aislop 0.9.0 → 0.9.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.
- package/README.md +1 -0
- package/dist/cli.js +662 -180
- package/dist/index.d.ts +4 -0
- package/dist/index.js +615 -164
- package/dist/{json-DZfGz2xa.js → json-B_2_Zt7I.js} +1 -1
- package/dist/mcp.js +422 -148
- package/dist/{version-D_rqBdyj.js → version-CBcgcofs.js} +1 -1
- package/package.json +2 -1
package/dist/mcp.js
CHANGED
|
@@ -4,8 +4,8 @@ import { createRequire, isBuiltin } from "node:module";
|
|
|
4
4
|
import { performance } from "node:perf_hooks";
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
-
import path from "node:path";
|
|
8
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
+
import path from "node:path";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import YAML from "yaml";
|
|
@@ -26,6 +26,7 @@ const DEFAULT_CONFIG = {
|
|
|
26
26
|
"build",
|
|
27
27
|
"coverage"
|
|
28
28
|
],
|
|
29
|
+
include: [],
|
|
29
30
|
engines: {
|
|
30
31
|
format: true,
|
|
31
32
|
lint: true,
|
|
@@ -61,7 +62,7 @@ const DEFAULT_CONFIG = {
|
|
|
61
62
|
smoothing: 20
|
|
62
63
|
},
|
|
63
64
|
ci: {
|
|
64
|
-
failBelow:
|
|
65
|
+
failBelow: 70,
|
|
65
66
|
format: "json"
|
|
66
67
|
},
|
|
67
68
|
telemetry: { enabled: true }
|
|
@@ -151,7 +152,7 @@ const ScoringSchema = z$1.object({
|
|
|
151
152
|
smoothing: z$1.number().nonnegative().default(20)
|
|
152
153
|
});
|
|
153
154
|
const CiSchema = z$1.object({
|
|
154
|
-
failBelow: z$1.number().default(
|
|
155
|
+
failBelow: z$1.number().default(70),
|
|
155
156
|
format: z$1.enum(["json"]).default("json")
|
|
156
157
|
});
|
|
157
158
|
const TelemetrySchema = z$1.object({ enabled: z$1.boolean().default(true) });
|
|
@@ -185,7 +186,7 @@ const AislopConfigSchema = z$1.object({
|
|
|
185
186
|
smoothing: 20
|
|
186
187
|
})),
|
|
187
188
|
ci: CiSchema.default(() => ({
|
|
188
|
-
failBelow:
|
|
189
|
+
failBelow: 70,
|
|
189
190
|
format: "json"
|
|
190
191
|
})),
|
|
191
192
|
telemetry: TelemetrySchema.default(() => ({ enabled: true })),
|
|
@@ -195,7 +196,8 @@ const AislopConfigSchema = z$1.object({
|
|
|
195
196
|
"dist",
|
|
196
197
|
"build",
|
|
197
198
|
"coverage"
|
|
198
|
-
])
|
|
199
|
+
]),
|
|
200
|
+
include: z$1.array(z$1.string()).default(() => [])
|
|
199
201
|
});
|
|
200
202
|
const defaults = AislopConfigSchema.parse({});
|
|
201
203
|
/**
|
|
@@ -275,31 +277,68 @@ const EXCLUDED_DIRS = [
|
|
|
275
277
|
"dist",
|
|
276
278
|
"build",
|
|
277
279
|
".git",
|
|
280
|
+
".agents",
|
|
278
281
|
"vendor",
|
|
282
|
+
"examples",
|
|
283
|
+
"example",
|
|
284
|
+
"demos",
|
|
285
|
+
"demo",
|
|
286
|
+
"bench",
|
|
287
|
+
"benches",
|
|
288
|
+
"benchmarks",
|
|
289
|
+
"fixtures",
|
|
290
|
+
"fixture",
|
|
291
|
+
"samples",
|
|
292
|
+
"sample",
|
|
293
|
+
"tutorials",
|
|
294
|
+
"tutorial",
|
|
295
|
+
"code_samples",
|
|
296
|
+
"code-samples",
|
|
297
|
+
"notebooks",
|
|
279
298
|
"tests",
|
|
280
299
|
"test",
|
|
281
300
|
"__tests__",
|
|
282
301
|
"__test__",
|
|
283
302
|
"spec",
|
|
284
303
|
"__mocks__",
|
|
285
|
-
"fixtures",
|
|
286
304
|
"test_data",
|
|
287
305
|
".next",
|
|
288
306
|
".nuxt",
|
|
289
307
|
"coverage",
|
|
290
|
-
".turbo"
|
|
308
|
+
".turbo",
|
|
309
|
+
"public"
|
|
291
310
|
];
|
|
292
311
|
const FIND_PRUNE_DIRS = [
|
|
293
312
|
"node_modules",
|
|
294
313
|
"dist",
|
|
295
314
|
"build",
|
|
296
315
|
".git",
|
|
316
|
+
".agents",
|
|
297
317
|
"vendor",
|
|
318
|
+
"examples",
|
|
319
|
+
"example",
|
|
320
|
+
"demos",
|
|
321
|
+
"demo",
|
|
322
|
+
"bench",
|
|
323
|
+
"benches",
|
|
324
|
+
"benchmarks",
|
|
325
|
+
"fixtures",
|
|
326
|
+
"fixture",
|
|
327
|
+
"samples",
|
|
328
|
+
"sample",
|
|
329
|
+
"tutorials",
|
|
330
|
+
"tutorial",
|
|
331
|
+
"code_samples",
|
|
332
|
+
"code-samples",
|
|
333
|
+
"notebooks",
|
|
298
334
|
".next",
|
|
299
335
|
".nuxt",
|
|
300
336
|
"coverage",
|
|
301
|
-
".turbo"
|
|
337
|
+
".turbo",
|
|
338
|
+
"public"
|
|
302
339
|
];
|
|
340
|
+
const BUILD_CACHE_FILE_PATTERNS = [/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i];
|
|
341
|
+
const isBuildCacheFile = (filePath) => BUILD_CACHE_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
303
342
|
const TEST_FILE_PATTERNS = [
|
|
304
343
|
/(?:^|\/).*\.test\.[^/]+$/i,
|
|
305
344
|
/(?:^|\/).*\.spec\.[^/]+$/i,
|
|
@@ -324,6 +363,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
|
324
363
|
return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
|
|
325
364
|
};
|
|
326
365
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
366
|
+
const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
|
|
327
367
|
const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
328
368
|
const getIgnoredPaths = (rootDirectory, files) => {
|
|
329
369
|
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
@@ -382,7 +422,7 @@ const normalizeExcludePatterns = (patterns) => {
|
|
|
382
422
|
return [p];
|
|
383
423
|
});
|
|
384
424
|
};
|
|
385
|
-
const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = []) => {
|
|
425
|
+
const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = [], include = []) => {
|
|
386
426
|
const extraSet = new Set(extraExtensions);
|
|
387
427
|
const normalizedFiles = files.map((file) => {
|
|
388
428
|
const absolutePath = path.isAbsolute(file) ? file : path.resolve(rootDirectory, file);
|
|
@@ -397,8 +437,16 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
|
|
|
397
437
|
if (!normalizedExcludePatterns.length) return false;
|
|
398
438
|
return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
|
|
399
439
|
};
|
|
440
|
+
const hasIncludePatterns = include.length > 0;
|
|
441
|
+
const isUserIncluded = (relativePath) => {
|
|
442
|
+
if (!hasIncludePatterns) return true;
|
|
443
|
+
return micromatch.isMatch(relativePath, include, { dot: true });
|
|
444
|
+
};
|
|
400
445
|
return normalizedFiles.filter(({ absolutePath, relativePath }) => {
|
|
401
|
-
|
|
446
|
+
if (!fs.existsSync(absolutePath) || !isWithinProject(relativePath) || isExcludedPath(relativePath) || isTestFile$2(relativePath) || ignoredPaths.has(relativePath)) return false;
|
|
447
|
+
if (!isUserIncluded(relativePath)) return false;
|
|
448
|
+
if (isUserExcluded(relativePath)) return false;
|
|
449
|
+
return hasAllowedExtension(relativePath, extraSet);
|
|
402
450
|
}).map(({ absolutePath }) => absolutePath);
|
|
403
451
|
};
|
|
404
452
|
const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
|
|
@@ -1128,6 +1176,86 @@ const PYTHON_IMPORT_TO_PIP = {
|
|
|
1128
1176
|
redis: "redis"
|
|
1129
1177
|
};
|
|
1130
1178
|
|
|
1179
|
+
//#endregion
|
|
1180
|
+
//#region src/engines/ai-slop/python-manifest.ts
|
|
1181
|
+
const addPyDep = (pyDeps, name) => {
|
|
1182
|
+
const normalized = name.toLowerCase().replace(/_/g, "-");
|
|
1183
|
+
pyDeps.add(normalized);
|
|
1184
|
+
};
|
|
1185
|
+
const collectFromRequirementsTxt = (rootDir, pyDeps) => {
|
|
1186
|
+
const reqPath = path.join(rootDir, "requirements.txt");
|
|
1187
|
+
if (!fs.existsSync(reqPath)) return false;
|
|
1188
|
+
try {
|
|
1189
|
+
const content = fs.readFileSync(reqPath, "utf-8");
|
|
1190
|
+
for (const line of content.split("\n")) {
|
|
1191
|
+
const trimmed = line.trim();
|
|
1192
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
1193
|
+
const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
|
|
1194
|
+
if (match) addPyDep(pyDeps, match[1]);
|
|
1195
|
+
}
|
|
1196
|
+
return true;
|
|
1197
|
+
} catch {
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
const collectFromPyproject = (rootDir, pyDeps) => {
|
|
1202
|
+
const pyprojPath = path.join(rootDir, "pyproject.toml");
|
|
1203
|
+
if (!fs.existsSync(pyprojPath)) return false;
|
|
1204
|
+
try {
|
|
1205
|
+
const content = fs.readFileSync(pyprojPath, "utf-8");
|
|
1206
|
+
const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1207
|
+
if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
|
|
1208
|
+
const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1209
|
+
if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
|
|
1210
|
+
const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
1211
|
+
if (pep621) for (const line of pep621[1].split("\n")) {
|
|
1212
|
+
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
1213
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
1214
|
+
}
|
|
1215
|
+
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1216
|
+
let match = poetryRe.exec(content);
|
|
1217
|
+
while (match !== null) {
|
|
1218
|
+
for (const line of match[1].split("\n")) {
|
|
1219
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1220
|
+
if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
|
|
1221
|
+
}
|
|
1222
|
+
match = poetryRe.exec(content);
|
|
1223
|
+
}
|
|
1224
|
+
return true;
|
|
1225
|
+
} catch {
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
const collectFromPipfile = (rootDir, pyDeps) => {
|
|
1230
|
+
const pipfilePath = path.join(rootDir, "Pipfile");
|
|
1231
|
+
if (!fs.existsSync(pipfilePath)) return false;
|
|
1232
|
+
try {
|
|
1233
|
+
const content = fs.readFileSync(pipfilePath, "utf-8");
|
|
1234
|
+
const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1235
|
+
let match = sectionRe.exec(content);
|
|
1236
|
+
while (match !== null) {
|
|
1237
|
+
for (const line of match[2].split("\n")) {
|
|
1238
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1239
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
1240
|
+
}
|
|
1241
|
+
match = sectionRe.exec(content);
|
|
1242
|
+
}
|
|
1243
|
+
return true;
|
|
1244
|
+
} catch {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
const collectPythonDeps = (rootDir) => {
|
|
1249
|
+
const pyDeps = /* @__PURE__ */ new Set();
|
|
1250
|
+
const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
|
|
1251
|
+
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
1252
|
+
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
1253
|
+
return {
|
|
1254
|
+
pyDeps,
|
|
1255
|
+
hasPyManifest: hasReq || hasPyproject || hasPipfile
|
|
1256
|
+
};
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1131
1259
|
//#endregion
|
|
1132
1260
|
//#region src/engines/ai-slop/hallucinated-imports.ts
|
|
1133
1261
|
const JS_EXTENSIONS$1 = new Set([
|
|
@@ -1264,10 +1392,26 @@ const buildAliasMatcher = (key) => {
|
|
|
1264
1392
|
};
|
|
1265
1393
|
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
1266
1394
|
const opts = readJson(configPath)?.compilerOptions;
|
|
1267
|
-
if (!opts
|
|
1395
|
+
if (!opts) return;
|
|
1268
1396
|
const paths = opts.paths;
|
|
1269
|
-
if (
|
|
1270
|
-
|
|
1397
|
+
if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
|
|
1398
|
+
const baseUrl = opts.baseUrl;
|
|
1399
|
+
if (typeof baseUrl === "string") {
|
|
1400
|
+
const baseUrlDir = path.resolve(path.dirname(configPath), baseUrl);
|
|
1401
|
+
let entries;
|
|
1402
|
+
try {
|
|
1403
|
+
entries = fs.readdirSync(baseUrlDir);
|
|
1404
|
+
} catch {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const baseSpecifiers = /* @__PURE__ */ new Set();
|
|
1408
|
+
for (const entry of entries) {
|
|
1409
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
1410
|
+
const base = entry.replace(/\.(?:[jt]sx?|mjs|cjs|d\.ts)$/i, "");
|
|
1411
|
+
if (base.length > 0) baseSpecifiers.add(base);
|
|
1412
|
+
}
|
|
1413
|
+
for (const name of baseSpecifiers) matchers.push((spec) => spec === name || spec.startsWith(`${name}/`));
|
|
1414
|
+
}
|
|
1271
1415
|
};
|
|
1272
1416
|
const collectTsPathAliases = (rootDir) => {
|
|
1273
1417
|
const matchers = [];
|
|
@@ -1275,97 +1419,35 @@ const collectTsPathAliases = (rootDir) => {
|
|
|
1275
1419
|
for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
|
|
1276
1420
|
return matchers;
|
|
1277
1421
|
};
|
|
1278
|
-
const addPyDep = (pyDeps, name) => {
|
|
1279
|
-
const normalized = name.toLowerCase().replace(/_/g, "-");
|
|
1280
|
-
pyDeps.add(normalized);
|
|
1281
|
-
};
|
|
1282
|
-
const collectFromRequirementsTxt = (rootDir, pyDeps) => {
|
|
1283
|
-
const reqPath = path.join(rootDir, "requirements.txt");
|
|
1284
|
-
if (!fs.existsSync(reqPath)) return false;
|
|
1285
|
-
try {
|
|
1286
|
-
const content = fs.readFileSync(reqPath, "utf-8");
|
|
1287
|
-
for (const line of content.split("\n")) {
|
|
1288
|
-
const trimmed = line.trim();
|
|
1289
|
-
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
1290
|
-
const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
|
|
1291
|
-
if (match) addPyDep(pyDeps, match[1]);
|
|
1292
|
-
}
|
|
1293
|
-
return true;
|
|
1294
|
-
} catch {
|
|
1295
|
-
return false;
|
|
1296
|
-
}
|
|
1297
|
-
};
|
|
1298
|
-
const collectFromPyproject = (rootDir, pyDeps) => {
|
|
1299
|
-
const pyprojPath = path.join(rootDir, "pyproject.toml");
|
|
1300
|
-
if (!fs.existsSync(pyprojPath)) return false;
|
|
1301
|
-
try {
|
|
1302
|
-
const content = fs.readFileSync(pyprojPath, "utf-8");
|
|
1303
|
-
const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1304
|
-
if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
|
|
1305
|
-
const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1306
|
-
if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
|
|
1307
|
-
const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
1308
|
-
if (pep621) for (const line of pep621[1].split("\n")) {
|
|
1309
|
-
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
1310
|
-
if (m) addPyDep(pyDeps, m[1]);
|
|
1311
|
-
}
|
|
1312
|
-
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1313
|
-
let match = poetryRe.exec(content);
|
|
1314
|
-
while (match !== null) {
|
|
1315
|
-
for (const line of match[1].split("\n")) {
|
|
1316
|
-
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1317
|
-
if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
|
|
1318
|
-
}
|
|
1319
|
-
match = poetryRe.exec(content);
|
|
1320
|
-
}
|
|
1321
|
-
return true;
|
|
1322
|
-
} catch {
|
|
1323
|
-
return false;
|
|
1324
|
-
}
|
|
1325
|
-
};
|
|
1326
|
-
const collectFromPipfile = (rootDir, pyDeps) => {
|
|
1327
|
-
const pipfilePath = path.join(rootDir, "Pipfile");
|
|
1328
|
-
if (!fs.existsSync(pipfilePath)) return false;
|
|
1329
|
-
try {
|
|
1330
|
-
const content = fs.readFileSync(pipfilePath, "utf-8");
|
|
1331
|
-
const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1332
|
-
let match = sectionRe.exec(content);
|
|
1333
|
-
while (match !== null) {
|
|
1334
|
-
for (const line of match[2].split("\n")) {
|
|
1335
|
-
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1336
|
-
if (m) addPyDep(pyDeps, m[1]);
|
|
1337
|
-
}
|
|
1338
|
-
match = sectionRe.exec(content);
|
|
1339
|
-
}
|
|
1340
|
-
return true;
|
|
1341
|
-
} catch {
|
|
1342
|
-
return false;
|
|
1343
|
-
}
|
|
1344
|
-
};
|
|
1345
1422
|
const loadManifest = (rootDir) => {
|
|
1346
1423
|
const jsDeps = /* @__PURE__ */ new Set();
|
|
1347
|
-
const pyDeps = /* @__PURE__ */ new Set();
|
|
1348
1424
|
const hasJsManifest = collectJsDeps(rootDir, jsDeps);
|
|
1349
|
-
const
|
|
1350
|
-
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
1351
|
-
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
1425
|
+
const { pyDeps, hasPyManifest } = collectPythonDeps(rootDir);
|
|
1352
1426
|
return {
|
|
1353
1427
|
jsDeps,
|
|
1354
1428
|
pyDeps,
|
|
1355
1429
|
hasJsManifest,
|
|
1356
|
-
hasPyManifest
|
|
1430
|
+
hasPyManifest
|
|
1357
1431
|
};
|
|
1358
1432
|
};
|
|
1359
1433
|
const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
|
|
1434
|
+
const RUNTIME_BUILTINS = new Set(["bun"]);
|
|
1360
1435
|
const isJsBuiltin = (spec) => {
|
|
1436
|
+
if (RUNTIME_BUILTINS.has(spec)) return true;
|
|
1361
1437
|
return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
|
|
1362
1438
|
};
|
|
1363
1439
|
const VIRTUAL_MODULE_PREFIXES = [
|
|
1364
1440
|
"astro:",
|
|
1365
1441
|
"virtual:",
|
|
1366
|
-
"bun:"
|
|
1442
|
+
"bun:",
|
|
1443
|
+
"~icons/"
|
|
1367
1444
|
];
|
|
1368
1445
|
const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
|
|
1446
|
+
const stripImportQuery = (spec) => {
|
|
1447
|
+
const idx = spec.indexOf("?");
|
|
1448
|
+
return idx === -1 ? spec : spec.slice(0, idx);
|
|
1449
|
+
};
|
|
1450
|
+
const VIRTUAL_ASSET_FILES = { "unfonts.css": "unplugin-fonts" };
|
|
1369
1451
|
const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
|
|
1370
1452
|
const isLikelyRealImportSpec = (spec) => {
|
|
1371
1453
|
if (spec.length === 0) return false;
|
|
@@ -1432,10 +1514,14 @@ const extractPyImports = (content) => {
|
|
|
1432
1514
|
}
|
|
1433
1515
|
return results;
|
|
1434
1516
|
};
|
|
1435
|
-
const checkJsImport = (
|
|
1517
|
+
const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
|
|
1518
|
+
const spec = stripImportQuery(rawSpec);
|
|
1519
|
+
if (spec.length === 0) return null;
|
|
1436
1520
|
if (isJsRelativeOrAbsolute(spec)) return null;
|
|
1437
1521
|
if (isJsBuiltin(spec)) return null;
|
|
1438
1522
|
if (isJsVirtualModule(spec)) return null;
|
|
1523
|
+
const virtualOwner = VIRTUAL_ASSET_FILES[spec];
|
|
1524
|
+
if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
|
|
1439
1525
|
if (tsAliasMatchers.some((m) => m(spec))) return null;
|
|
1440
1526
|
const pkg = packageNameFromImport(spec);
|
|
1441
1527
|
if (manifest.jsDeps.has(pkg)) return null;
|
|
@@ -2790,64 +2876,88 @@ const analyzeFunctions = (content, ext) => {
|
|
|
2790
2876
|
}
|
|
2791
2877
|
return functions;
|
|
2792
2878
|
};
|
|
2793
|
-
const
|
|
2879
|
+
const FILE_LOC_MULTIPLIERS = {
|
|
2880
|
+
".tsx": 1.5,
|
|
2881
|
+
".jsx": 1.5,
|
|
2882
|
+
".rs": 2.5,
|
|
2883
|
+
".go": 1.5
|
|
2884
|
+
};
|
|
2885
|
+
const DECLARATION_FILE_RE = /\.d\.ts$/i;
|
|
2886
|
+
const fileLocBudget = (ext, relativePath, base) => {
|
|
2887
|
+
if (DECLARATION_FILE_RE.test(relativePath)) return Number.POSITIVE_INFINITY;
|
|
2888
|
+
const multiplier = FILE_LOC_MULTIPLIERS[ext] ?? 1;
|
|
2889
|
+
return Math.ceil(base * multiplier);
|
|
2890
|
+
};
|
|
2794
2891
|
const checkFileDiagnostics = (relativePath, content, limits) => {
|
|
2795
2892
|
const results = [];
|
|
2796
2893
|
const lineCount = content.split("\n").length;
|
|
2797
2894
|
const ext = path.extname(relativePath).toLowerCase();
|
|
2798
2895
|
if (isDataFile(content)) return results;
|
|
2799
|
-
const configuredMax = ext
|
|
2896
|
+
const configuredMax = fileLocBudget(ext, relativePath, limits.maxFileLoc);
|
|
2897
|
+
if (!Number.isFinite(configuredMax)) return results;
|
|
2800
2898
|
if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
|
|
2801
2899
|
filePath: relativePath,
|
|
2802
2900
|
engine: "code-quality",
|
|
2803
2901
|
rule: "complexity/file-too-large",
|
|
2804
2902
|
severity: "warning",
|
|
2805
|
-
message: `File
|
|
2903
|
+
message: `File too large (max: ${configuredMax})`,
|
|
2806
2904
|
help: "Consider splitting this file into smaller modules",
|
|
2807
2905
|
line: 0,
|
|
2808
2906
|
column: 0,
|
|
2809
2907
|
category: "Complexity",
|
|
2810
|
-
fixable: false
|
|
2908
|
+
fixable: false,
|
|
2909
|
+
detail: `${lineCount} lines`
|
|
2811
2910
|
});
|
|
2812
2911
|
return results;
|
|
2813
2912
|
};
|
|
2814
|
-
const
|
|
2913
|
+
const JSX_EXTENSIONS = new Set([".tsx", ".jsx"]);
|
|
2914
|
+
const isComponentFunction = (name, ext) => JSX_EXTENSIONS.has(ext) && /^[A-Z]/.test(name);
|
|
2915
|
+
const functionLocBudget = (fn, ext, base) => {
|
|
2916
|
+
if (isComponentFunction(fn.name, ext)) return Math.ceil(base * 2);
|
|
2917
|
+
if (ext === ".rs") return Math.ceil(base * 1.5);
|
|
2918
|
+
return base;
|
|
2919
|
+
};
|
|
2920
|
+
const checkFunctionDiagnostics = (relativePath, fn, limits, ext) => {
|
|
2815
2921
|
const results = [];
|
|
2816
|
-
|
|
2922
|
+
const fnMax = functionLocBudget(fn, ext, limits.maxFunctionLoc);
|
|
2923
|
+
if (fn.lineCount - fn.templateLines > Math.ceil(fnMax * 1.1)) results.push({
|
|
2817
2924
|
filePath: relativePath,
|
|
2818
2925
|
engine: "code-quality",
|
|
2819
2926
|
rule: "complexity/function-too-long",
|
|
2820
2927
|
severity: "warning",
|
|
2821
|
-
message: `Function
|
|
2928
|
+
message: `Function too long (max: ${fnMax})`,
|
|
2822
2929
|
help: "Consider breaking this function into smaller pieces",
|
|
2823
2930
|
line: fn.startLine,
|
|
2824
2931
|
column: 0,
|
|
2825
2932
|
category: "Complexity",
|
|
2826
|
-
fixable: false
|
|
2933
|
+
fixable: false,
|
|
2934
|
+
detail: `${fn.name} · ${fn.lineCount} lines`
|
|
2827
2935
|
});
|
|
2828
2936
|
if (fn.maxNesting > limits.maxNesting) results.push({
|
|
2829
2937
|
filePath: relativePath,
|
|
2830
2938
|
engine: "code-quality",
|
|
2831
2939
|
rule: "complexity/deep-nesting",
|
|
2832
2940
|
severity: "warning",
|
|
2833
|
-
message: `Function
|
|
2941
|
+
message: `Function nested too deeply (max: ${limits.maxNesting})`,
|
|
2834
2942
|
help: "Consider using early returns or extracting nested logic",
|
|
2835
2943
|
line: fn.startLine,
|
|
2836
2944
|
column: 0,
|
|
2837
2945
|
category: "Complexity",
|
|
2838
|
-
fixable: false
|
|
2946
|
+
fixable: false,
|
|
2947
|
+
detail: `${fn.name} · depth ${fn.maxNesting}`
|
|
2839
2948
|
});
|
|
2840
2949
|
if (fn.paramCount > limits.maxParams) results.push({
|
|
2841
2950
|
filePath: relativePath,
|
|
2842
2951
|
engine: "code-quality",
|
|
2843
2952
|
rule: "complexity/too-many-params",
|
|
2844
2953
|
severity: "warning",
|
|
2845
|
-
message: `Function
|
|
2954
|
+
message: `Function has too many parameters (max: ${limits.maxParams})`,
|
|
2846
2955
|
help: "Consider using an options object parameter",
|
|
2847
2956
|
line: fn.startLine,
|
|
2848
2957
|
column: 0,
|
|
2849
2958
|
category: "Complexity",
|
|
2850
|
-
fixable: false
|
|
2959
|
+
fixable: false,
|
|
2960
|
+
detail: `${fn.name} · ${fn.paramCount} params`
|
|
2851
2961
|
});
|
|
2852
2962
|
return results;
|
|
2853
2963
|
};
|
|
@@ -2862,7 +2972,7 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
|
|
|
2862
2972
|
}
|
|
2863
2973
|
const ext = path.extname(filePath).toLowerCase();
|
|
2864
2974
|
const diagnostics = checkFileDiagnostics(relativePath, content, limits);
|
|
2865
|
-
for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
|
|
2975
|
+
for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits, ext));
|
|
2866
2976
|
return diagnostics;
|
|
2867
2977
|
};
|
|
2868
2978
|
const checkComplexity = async (context) => {
|
|
@@ -2967,17 +3077,19 @@ const findDuplicateBlocks = (content, relativePath) => {
|
|
|
2967
3077
|
});
|
|
2968
3078
|
}
|
|
2969
3079
|
return reports.map((r) => {
|
|
3080
|
+
const span = r.currentEnd - r.currentStart + 1;
|
|
2970
3081
|
return {
|
|
2971
3082
|
filePath: relativePath,
|
|
2972
3083
|
engine: "code-quality",
|
|
2973
3084
|
rule: "code-quality/duplicate-block",
|
|
2974
3085
|
severity: "warning",
|
|
2975
|
-
message:
|
|
3086
|
+
message: "Duplicate code block — extract a shared helper",
|
|
2976
3087
|
help: `Pull the shared logic into a function both sites can call. Keeps one version of the truth and makes future changes one-shot instead of N-shot.`,
|
|
2977
3088
|
line: r.currentStart,
|
|
2978
3089
|
column: 0,
|
|
2979
3090
|
category: "Complexity",
|
|
2980
|
-
fixable: false
|
|
3091
|
+
fixable: false,
|
|
3092
|
+
detail: `${span} lines duplicate block at L${r.priorStart}`
|
|
2981
3093
|
};
|
|
2982
3094
|
});
|
|
2983
3095
|
};
|
|
@@ -3519,16 +3631,34 @@ const isToolAvailable = async (toolName) => {
|
|
|
3519
3631
|
return isToolInstalled(toolName);
|
|
3520
3632
|
};
|
|
3521
3633
|
|
|
3634
|
+
//#endregion
|
|
3635
|
+
//#region src/engines/python-targets.ts
|
|
3636
|
+
const PYTHON_EXTENSIONS = new Set([".py", ".pyi"]);
|
|
3637
|
+
const normalizeProjectPath = (filePath) => filePath.split(path.sep).join("/");
|
|
3638
|
+
const getPythonTargets = (context) => {
|
|
3639
|
+
const targets = (context.files ?? getSourceFiles(context)).filter((filePath) => PYTHON_EXTENSIONS.has(path.extname(filePath).toLowerCase())).map((filePath) => {
|
|
3640
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(context.rootDirectory, filePath);
|
|
3641
|
+
return normalizeProjectPath(path.relative(context.rootDirectory, absolutePath));
|
|
3642
|
+
}).filter((filePath) => filePath.length > 0 && !filePath.startsWith(".."));
|
|
3643
|
+
return [...new Set(targets)];
|
|
3644
|
+
};
|
|
3645
|
+
const getRuffDiagnosticPath = (rootDirectory, filePath) => {
|
|
3646
|
+
const normalizedPath = filePath.replace(/^a\//, "");
|
|
3647
|
+
return normalizeProjectPath(path.isAbsolute(normalizedPath) ? path.relative(rootDirectory, normalizedPath) : normalizedPath);
|
|
3648
|
+
};
|
|
3649
|
+
|
|
3522
3650
|
//#endregion
|
|
3523
3651
|
//#region src/engines/format/ruff-format.ts
|
|
3524
3652
|
const runRuffFormat = async (context) => {
|
|
3525
3653
|
const ruffBinary = resolveToolBinary("ruff");
|
|
3654
|
+
const targets = getPythonTargets(context);
|
|
3655
|
+
if (targets.length === 0) return [];
|
|
3526
3656
|
try {
|
|
3527
3657
|
const result = await runSubprocess(ruffBinary, [
|
|
3528
3658
|
"format",
|
|
3529
3659
|
"--check",
|
|
3530
3660
|
"--diff",
|
|
3531
|
-
|
|
3661
|
+
...targets
|
|
3532
3662
|
], {
|
|
3533
3663
|
cwd: context.rootDirectory,
|
|
3534
3664
|
timeout: 6e4
|
|
@@ -3544,9 +3674,9 @@ const parseRuffFormatOutput = (output, rootDir) => {
|
|
|
3544
3674
|
const filePattern = /^--- (.+)$/gm;
|
|
3545
3675
|
let match;
|
|
3546
3676
|
while ((match = filePattern.exec(output)) !== null) {
|
|
3547
|
-
const filePath = match[1]
|
|
3677
|
+
const filePath = getRuffDiagnosticPath(rootDir, match[1]);
|
|
3548
3678
|
diagnostics.push({
|
|
3549
|
-
filePath
|
|
3679
|
+
filePath,
|
|
3550
3680
|
engine: "format",
|
|
3551
3681
|
rule: "python-formatting",
|
|
3552
3682
|
severity: "warning",
|
|
@@ -3816,6 +3946,95 @@ const resolveOxlintBinary = () => {
|
|
|
3816
3946
|
return "oxlint";
|
|
3817
3947
|
}
|
|
3818
3948
|
};
|
|
3949
|
+
const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
|
|
3950
|
+
const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
|
|
3951
|
+
const AMBIENT_GLOBAL_DEPS = [
|
|
3952
|
+
"unplugin-icons",
|
|
3953
|
+
"@types/bun",
|
|
3954
|
+
"bun-types"
|
|
3955
|
+
];
|
|
3956
|
+
const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
|
|
3957
|
+
const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
|
|
3958
|
+
const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
|
|
3959
|
+
const detectAmbientSources = (rootDir) => {
|
|
3960
|
+
const found = /* @__PURE__ */ new Set();
|
|
3961
|
+
const skipDirs = new Set([
|
|
3962
|
+
"node_modules",
|
|
3963
|
+
".git",
|
|
3964
|
+
"dist",
|
|
3965
|
+
"build",
|
|
3966
|
+
"out",
|
|
3967
|
+
"target",
|
|
3968
|
+
"coverage",
|
|
3969
|
+
".next",
|
|
3970
|
+
".turbo"
|
|
3971
|
+
]);
|
|
3972
|
+
const walk = (dir, depth) => {
|
|
3973
|
+
if (depth > 4 || found.size === AMBIENT_GLOBAL_DEPS.length) return;
|
|
3974
|
+
let entries;
|
|
3975
|
+
try {
|
|
3976
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
3977
|
+
} catch {
|
|
3978
|
+
return;
|
|
3979
|
+
}
|
|
3980
|
+
for (const entry of entries) {
|
|
3981
|
+
if (found.size === AMBIENT_GLOBAL_DEPS.length) return;
|
|
3982
|
+
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
3983
|
+
if (skipDirs.has(entry.name)) continue;
|
|
3984
|
+
const full = path.join(dir, entry.name);
|
|
3985
|
+
if (entry.isDirectory()) walk(full, depth + 1);
|
|
3986
|
+
else if (entry.name === "package.json") try {
|
|
3987
|
+
const pkg = JSON.parse(fs.readFileSync(full, "utf-8"));
|
|
3988
|
+
const allDeps = {
|
|
3989
|
+
...pkg.dependencies ?? {},
|
|
3990
|
+
...pkg.devDependencies ?? {},
|
|
3991
|
+
...pkg.peerDependencies ?? {}
|
|
3992
|
+
};
|
|
3993
|
+
for (const dep of AMBIENT_GLOBAL_DEPS) if (dep in allDeps) found.add(dep);
|
|
3994
|
+
} catch {}
|
|
3995
|
+
}
|
|
3996
|
+
};
|
|
3997
|
+
walk(rootDir, 0);
|
|
3998
|
+
return found;
|
|
3999
|
+
};
|
|
4000
|
+
const extractNoUndefIdentifier = (message) => {
|
|
4001
|
+
return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
|
|
4002
|
+
};
|
|
4003
|
+
const isAmbientFalsePositive = (rule, message, sources) => {
|
|
4004
|
+
if (rule !== "eslint/no-undef") return false;
|
|
4005
|
+
const ident = extractNoUndefIdentifier(message);
|
|
4006
|
+
if (!ident) return false;
|
|
4007
|
+
if (sources.has("unplugin-icons") && ICON_AUTOIMPORT_RE.test(ident)) return true;
|
|
4008
|
+
if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
|
|
4009
|
+
return false;
|
|
4010
|
+
};
|
|
4011
|
+
const sstReferencedFiles = /* @__PURE__ */ new Map();
|
|
4012
|
+
const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
4013
|
+
const cached = sstReferencedFiles.get(relativeFilePath);
|
|
4014
|
+
if (cached !== void 0) return cached;
|
|
4015
|
+
const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
|
|
4016
|
+
let referenced = false;
|
|
4017
|
+
try {
|
|
4018
|
+
const fd = fs.openSync(absolute, "r");
|
|
4019
|
+
try {
|
|
4020
|
+
const buf = Buffer.alloc(512);
|
|
4021
|
+
const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
|
|
4022
|
+
referenced = SST_PLATFORM_REF_RE.test(buf.toString("utf-8", 0, bytesRead));
|
|
4023
|
+
} finally {
|
|
4024
|
+
fs.closeSync(fd);
|
|
4025
|
+
}
|
|
4026
|
+
} catch {
|
|
4027
|
+
referenced = false;
|
|
4028
|
+
}
|
|
4029
|
+
sstReferencedFiles.set(relativeFilePath, referenced);
|
|
4030
|
+
return referenced;
|
|
4031
|
+
};
|
|
4032
|
+
const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
|
|
4033
|
+
const isUnderscoreUnusedVar = (rule, message) => {
|
|
4034
|
+
if (rule !== "eslint/no-unused-vars") return false;
|
|
4035
|
+
const match = UNUSED_VAR_IDENT_RE.exec(message);
|
|
4036
|
+
return match ? match[1].startsWith("_") : false;
|
|
4037
|
+
};
|
|
3819
4038
|
const parseRuleCode = (code) => {
|
|
3820
4039
|
if (!code) return {
|
|
3821
4040
|
plugin: "eslint",
|
|
@@ -3854,6 +4073,8 @@ const runOxlint = async (context) => {
|
|
|
3854
4073
|
framework: context.frameworks.find((f) => f !== "none"),
|
|
3855
4074
|
testFramework: detectTestFramework(context.rootDirectory)
|
|
3856
4075
|
});
|
|
4076
|
+
const ambientSources = detectAmbientSources(context.rootDirectory);
|
|
4077
|
+
sstReferencedFiles.clear();
|
|
3857
4078
|
try {
|
|
3858
4079
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
3859
4080
|
const args = [
|
|
@@ -3893,6 +4114,11 @@ const runOxlint = async (context) => {
|
|
|
3893
4114
|
fixable: false
|
|
3894
4115
|
};
|
|
3895
4116
|
}).filter((d) => {
|
|
4117
|
+
if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
|
|
4118
|
+
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
4119
|
+
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
4120
|
+
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
4121
|
+
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
3896
4122
|
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
3897
4123
|
if (seen.has(key)) return false;
|
|
3898
4124
|
seen.add(key);
|
|
@@ -3907,18 +4133,20 @@ const runOxlint = async (context) => {
|
|
|
3907
4133
|
//#region src/engines/lint/ruff.ts
|
|
3908
4134
|
const runRuffLint = async (context) => {
|
|
3909
4135
|
const ruffBinary = resolveToolBinary("ruff");
|
|
4136
|
+
const targets = getPythonTargets(context);
|
|
4137
|
+
if (targets.length === 0) return [];
|
|
3910
4138
|
try {
|
|
3911
4139
|
const output = (await runSubprocess(ruffBinary, [
|
|
3912
4140
|
"check",
|
|
3913
4141
|
"--output-format=json",
|
|
3914
|
-
|
|
4142
|
+
...targets
|
|
3915
4143
|
], {
|
|
3916
4144
|
cwd: context.rootDirectory,
|
|
3917
4145
|
timeout: 6e4
|
|
3918
4146
|
})).stdout;
|
|
3919
4147
|
if (!output) return [];
|
|
3920
4148
|
return JSON.parse(output).map((d) => ({
|
|
3921
|
-
filePath:
|
|
4149
|
+
filePath: getRuffDiagnosticPath(context.rootDirectory, d.filename),
|
|
3922
4150
|
engine: "lint",
|
|
3923
4151
|
rule: `ruff/${d.code}`,
|
|
3924
4152
|
severity: d.code.startsWith("E") || d.code.startsWith("F") ? "error" : "warning",
|
|
@@ -4013,56 +4241,94 @@ const runPnpmAuditWithFallback = async (rootDir, timeout) => {
|
|
|
4013
4241
|
return [];
|
|
4014
4242
|
}
|
|
4015
4243
|
};
|
|
4244
|
+
const SEVERITY_RANK = {
|
|
4245
|
+
critical: 4,
|
|
4246
|
+
high: 3,
|
|
4247
|
+
moderate: 2,
|
|
4248
|
+
low: 1
|
|
4249
|
+
};
|
|
4016
4250
|
const toSeverity = (value) => value === "critical" || value === "high" ? "error" : "warning";
|
|
4017
|
-
const
|
|
4251
|
+
const upsertVuln = (bucket, packageName, severity, recommendation) => {
|
|
4252
|
+
const existing = bucket.get(packageName);
|
|
4253
|
+
if (existing) {
|
|
4254
|
+
existing.advisories++;
|
|
4255
|
+
if ((SEVERITY_RANK[severity] ?? 0) > (SEVERITY_RANK[existing.worstSeverity] ?? 0)) existing.worstSeverity = severity;
|
|
4256
|
+
if (recommendation) existing.recommendations.add(recommendation);
|
|
4257
|
+
} else bucket.set(packageName, {
|
|
4258
|
+
packageName,
|
|
4259
|
+
worstSeverity: severity,
|
|
4260
|
+
advisories: 1,
|
|
4261
|
+
recommendations: recommendation ? new Set([recommendation]) : /* @__PURE__ */ new Set()
|
|
4262
|
+
});
|
|
4263
|
+
};
|
|
4264
|
+
const SEMVER_RE = /(\d+)\.(\d+)\.(\d+)/;
|
|
4265
|
+
const cmpSemver = (a, b) => {
|
|
4266
|
+
const [, a1, a2, a3] = SEMVER_RE.exec(a) ?? [
|
|
4267
|
+
"",
|
|
4268
|
+
"0",
|
|
4269
|
+
"0",
|
|
4270
|
+
"0"
|
|
4271
|
+
];
|
|
4272
|
+
const [, b1, b2, b3] = SEMVER_RE.exec(b) ?? [
|
|
4273
|
+
"",
|
|
4274
|
+
"0",
|
|
4275
|
+
"0",
|
|
4276
|
+
"0"
|
|
4277
|
+
];
|
|
4278
|
+
if (Number(a1) !== Number(b1)) return Number(a1) - Number(b1);
|
|
4279
|
+
if (Number(a2) !== Number(b2)) return Number(a2) - Number(b2);
|
|
4280
|
+
return Number(a3) - Number(b3);
|
|
4281
|
+
};
|
|
4282
|
+
const pickBestRecommendation = (recs) => {
|
|
4283
|
+
if (recs.length <= 1) return recs[0] ?? "";
|
|
4284
|
+
const versioned = recs.filter((r) => SEMVER_RE.test(r));
|
|
4285
|
+
if (versioned.length === 0) return recs[0];
|
|
4286
|
+
return versioned.reduce((best, r) => cmpSemver(r, best) > 0 ? r : best);
|
|
4287
|
+
};
|
|
4288
|
+
const cleanRecommendation = (raw) => {
|
|
4289
|
+
const t = raw.trim();
|
|
4290
|
+
if (!t || t.toLowerCase() === "none") return "no fix available";
|
|
4291
|
+
return t;
|
|
4292
|
+
};
|
|
4293
|
+
const aggregateToDiagnostic = (agg, source) => {
|
|
4294
|
+
const best = cleanRecommendation(pickBestRecommendation([...agg.recommendations]));
|
|
4295
|
+
const countLabel = agg.advisories > 1 ? ` (${agg.advisories} advisories)` : "";
|
|
4296
|
+
const recLabel = best ? ` — ${best}` : "";
|
|
4297
|
+
return {
|
|
4298
|
+
filePath: "package.json",
|
|
4299
|
+
engine: "security",
|
|
4300
|
+
rule: "security/vulnerable-dependency",
|
|
4301
|
+
severity: toSeverity(agg.worstSeverity),
|
|
4302
|
+
message: `${agg.packageName} (${agg.worstSeverity})${recLabel}${countLabel}`,
|
|
4303
|
+
help: "",
|
|
4304
|
+
line: 0,
|
|
4305
|
+
column: 0,
|
|
4306
|
+
category: "Security",
|
|
4307
|
+
fixable: false,
|
|
4308
|
+
detail: source === "npm audit" ? "npm" : "pnpm"
|
|
4309
|
+
};
|
|
4310
|
+
};
|
|
4018
4311
|
const parseLegacyAdvisories = (advisories, source) => {
|
|
4019
|
-
const
|
|
4020
|
-
for (const [key, advisory] of Object.entries(advisories))
|
|
4021
|
-
|
|
4022
|
-
const severity = (advisory.severity ?? "moderate").toLowerCase();
|
|
4023
|
-
const recommendation = advisory.recommendation ?? advisory.title ?? `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
|
|
4024
|
-
diagnostics.push({
|
|
4025
|
-
filePath: "package.json",
|
|
4026
|
-
engine: "security",
|
|
4027
|
-
rule: "security/vulnerable-dependency",
|
|
4028
|
-
severity: toSeverity(severity),
|
|
4029
|
-
message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
|
|
4030
|
-
help: withFixHint(recommendation),
|
|
4031
|
-
line: 0,
|
|
4032
|
-
column: 0,
|
|
4033
|
-
category: "Security",
|
|
4034
|
-
fixable: false
|
|
4035
|
-
});
|
|
4036
|
-
}
|
|
4037
|
-
return diagnostics;
|
|
4312
|
+
const bucket = /* @__PURE__ */ new Map();
|
|
4313
|
+
for (const [key, advisory] of Object.entries(advisories)) upsertVuln(bucket, advisory.module_name ?? advisory.name ?? advisory.package ?? key, (advisory.severity ?? "moderate").toLowerCase(), advisory.recommendation ?? advisory.title ?? "");
|
|
4314
|
+
return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
|
|
4038
4315
|
};
|
|
4039
4316
|
const parseModernVulnerabilities = (vulnerabilities, source) => {
|
|
4040
|
-
const
|
|
4317
|
+
const bucket = /* @__PURE__ */ new Map();
|
|
4041
4318
|
for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
|
|
4042
4319
|
const severity = (vulnerability.severity ?? "moderate").toLowerCase();
|
|
4043
4320
|
const fixAvailable = vulnerability.fixAvailable;
|
|
4044
4321
|
const isDirect = vulnerability.isDirect === true;
|
|
4045
|
-
let recommendation =
|
|
4046
|
-
if (fixAvailable === false) recommendation = isDirect ? "
|
|
4047
|
-
else if (!isDirect && fixAvailable === true) recommendation = "
|
|
4322
|
+
let recommendation = "";
|
|
4323
|
+
if (fixAvailable === false) recommendation = isDirect ? "no automatic fix" : "transitive — needs override or parent upgrade";
|
|
4324
|
+
else if (!isDirect && fixAvailable === true) recommendation = "transitive — may need override or parent upgrade";
|
|
4048
4325
|
else if (fixAvailable && typeof fixAvailable === "object" && "name" in fixAvailable && "version" in fixAvailable) {
|
|
4049
4326
|
const target = fixAvailable;
|
|
4050
|
-
if (target.name && target.version) recommendation = `
|
|
4327
|
+
if (target.name && target.version) recommendation = `upgrade to ${target.name}@${target.version}`;
|
|
4051
4328
|
}
|
|
4052
|
-
|
|
4053
|
-
filePath: "package.json",
|
|
4054
|
-
engine: "security",
|
|
4055
|
-
rule: "security/vulnerable-dependency",
|
|
4056
|
-
severity: toSeverity(severity),
|
|
4057
|
-
message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
|
|
4058
|
-
help: withFixHint(recommendation),
|
|
4059
|
-
line: 0,
|
|
4060
|
-
column: 0,
|
|
4061
|
-
category: "Security",
|
|
4062
|
-
fixable: false
|
|
4063
|
-
});
|
|
4329
|
+
upsertVuln(bucket, packageName, severity, recommendation);
|
|
4064
4330
|
}
|
|
4065
|
-
return
|
|
4331
|
+
return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
|
|
4066
4332
|
};
|
|
4067
4333
|
const parseJsAudit = (output, source) => {
|
|
4068
4334
|
if (!output) return [];
|
|
@@ -4978,10 +5244,17 @@ const runScan = async (cwd) => {
|
|
|
4978
5244
|
const config = loadConfig(cwd);
|
|
4979
5245
|
const diagnostics = (await runEngines(buildEngineContext(project.rootDirectory, project, config), enabledEnginesFromConfig(config))).flatMap((r) => r.diagnostics);
|
|
4980
5246
|
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
5247
|
+
const errorCount = diagnostics.filter((d) => d.severity === "error").length;
|
|
5248
|
+
const failBelow = config.ci.failBelow;
|
|
4981
5249
|
return {
|
|
4982
5250
|
project,
|
|
4983
5251
|
diagnostics,
|
|
4984
|
-
score
|
|
5252
|
+
score,
|
|
5253
|
+
qualityGate: {
|
|
5254
|
+
failBelow,
|
|
5255
|
+
passed: errorCount === 0 && score >= failBelow,
|
|
5256
|
+
errorCount
|
|
5257
|
+
}
|
|
4985
5258
|
};
|
|
4986
5259
|
};
|
|
4987
5260
|
const aislopScanInputSchema = z.object({ path: z.string().optional().describe("Project directory to scan. Defaults to the MCP server's cwd.") });
|
|
@@ -4991,10 +5264,11 @@ const aislopScanTool = {
|
|
|
4991
5264
|
inputSchema: aislopScanInputSchema
|
|
4992
5265
|
};
|
|
4993
5266
|
const handleAislopScan = async (input) => {
|
|
4994
|
-
const { project, diagnostics, score } = await runScan(resolveCwd(input.path));
|
|
5267
|
+
const { project, diagnostics, score, qualityGate } = await runScan(resolveCwd(input.path));
|
|
4995
5268
|
const summary = summariseDiagnostics(diagnostics, project.rootDirectory);
|
|
4996
5269
|
return {
|
|
4997
5270
|
score,
|
|
5271
|
+
qualityGate,
|
|
4998
5272
|
fileCount: project.sourceFileCount,
|
|
4999
5273
|
languages: project.languages,
|
|
5000
5274
|
frameworks: project.frameworks,
|
|
@@ -5095,7 +5369,7 @@ const handleAislopBaseline = (input) => {
|
|
|
5095
5369
|
|
|
5096
5370
|
//#endregion
|
|
5097
5371
|
//#region src/version.ts
|
|
5098
|
-
const APP_VERSION = "0.9.
|
|
5372
|
+
const APP_VERSION = "0.9.1";
|
|
5099
5373
|
|
|
5100
5374
|
//#endregion
|
|
5101
5375
|
//#region src/telemetry/env.ts
|