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/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: 0,
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(0),
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: 0,
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
- return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile$2(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
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 || typeof opts !== "object") return;
1395
+ if (!opts) return;
1268
1396
  const paths = opts.paths;
1269
- if (!paths || typeof paths !== "object") return;
1270
- for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
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 hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
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: hasReq || hasPyproject || hasPipfile
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 = (spec, manifest, tsAliasMatchers) => {
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 JSX_FILE_LOC_MULTIPLIER = 1.5;
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 === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
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 has ${lineCount} lines (max: ${configuredMax})`,
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 checkFunctionDiagnostics = (relativePath, fn, limits) => {
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
- if (fn.lineCount - fn.templateLines > Math.ceil(limits.maxFunctionLoc * 1.1)) results.push({
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 '${fn.name}' has ${fn.lineCount} lines (max: ${limits.maxFunctionLoc})`,
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 '${fn.name}' has nesting depth ${fn.maxNesting} (max: ${limits.maxNesting})`,
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 '${fn.name}' has ${fn.paramCount} parameters (max: ${limits.maxParams})`,
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: `${r.currentEnd - r.currentStart + 1}-line block at line ${r.currentStart} duplicates a block starting at line ${r.priorStart}. Extract a shared helper.`,
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
- context.rootDirectory
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].replace(/^a\//, "");
3677
+ const filePath = getRuffDiagnosticPath(rootDir, match[1]);
3548
3678
  diagnostics.push({
3549
- filePath: path.relative(rootDir, 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
- context.rootDirectory
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: path.relative(context.rootDirectory, d.filename),
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 defaultAuditFixCommand = (source) => source === "pnpm audit" ? "pnpm audit --fix" : "npm audit fix";
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 diagnostics = [];
4020
- for (const [key, advisory] of Object.entries(advisories)) {
4021
- const packageName = advisory.module_name ?? advisory.name ?? advisory.package ?? key;
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 diagnostics = [];
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 = `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
4046
- if (fixAvailable === false) recommendation = isDirect ? "No automatic fix — check for a newer major version" : "Transitive with no fix add an override or upgrade the parent";
4047
- else if (!isDirect && fixAvailable === true) recommendation = "Transitive dep — may need an override or parent upgrade";
4322
+ let recommendation = "";
4323
+ if (fixAvailable === false) recommendation = isDirect ? "no automatic fix" : "transitiveneeds 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 = `Upgrade to ${target.name}@${target.version}.`;
4327
+ if (target.name && target.version) recommendation = `upgrade to ${target.name}@${target.version}`;
4051
4328
  }
4052
- diagnostics.push({
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 diagnostics;
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.0";
5372
+ const APP_VERSION = "0.9.1";
5099
5373
 
5100
5374
  //#endregion
5101
5375
  //#region src/telemetry/env.ts