aislop 0.9.0 → 0.9.2

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-D_rqBdyj.js";
1
+ import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-C3JZkQGA.js";
2
2
  import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
3
3
  import { r as runGenericLinter, t as fixRubyLint } from "./generic-BrcWMW7E.js";
4
4
  import { n as runExpoDoctor } from "./expo-doctor-Bz0LZhQ6.js";
@@ -28,6 +28,7 @@ const DEFAULT_CONFIG = {
28
28
  "build",
29
29
  "coverage"
30
30
  ],
31
+ include: [],
31
32
  engines: {
32
33
  format: true,
33
34
  lint: true,
@@ -63,7 +64,7 @@ const DEFAULT_CONFIG = {
63
64
  smoothing: 20
64
65
  },
65
66
  ci: {
66
- failBelow: 0,
67
+ failBelow: 70,
67
68
  format: "json"
68
69
  },
69
70
  telemetry: { enabled: true }
@@ -189,7 +190,7 @@ const ScoringSchema = z.object({
189
190
  smoothing: z.number().nonnegative().default(20)
190
191
  });
191
192
  const CiSchema = z.object({
192
- failBelow: z.number().default(0),
193
+ failBelow: z.number().default(70),
193
194
  format: z.enum(["json"]).default("json")
194
195
  });
195
196
  const TelemetrySchema = z.object({ enabled: z.boolean().default(true) });
@@ -223,7 +224,7 @@ const AislopConfigSchema = z.object({
223
224
  smoothing: 20
224
225
  })),
225
226
  ci: CiSchema.default(() => ({
226
- failBelow: 0,
227
+ failBelow: 70,
227
228
  format: "json"
228
229
  })),
229
230
  telemetry: TelemetrySchema.default(() => ({ enabled: true })),
@@ -233,7 +234,8 @@ const AislopConfigSchema = z.object({
233
234
  "dist",
234
235
  "build",
235
236
  "coverage"
236
- ])
237
+ ]),
238
+ include: z.array(z.string()).default(() => [])
237
239
  });
238
240
  const defaults = AislopConfigSchema.parse({});
239
241
  /**
@@ -561,31 +563,68 @@ const EXCLUDED_DIRS = [
561
563
  "dist",
562
564
  "build",
563
565
  ".git",
566
+ ".agents",
564
567
  "vendor",
568
+ "examples",
569
+ "example",
570
+ "demos",
571
+ "demo",
572
+ "bench",
573
+ "benches",
574
+ "benchmarks",
575
+ "fixtures",
576
+ "fixture",
577
+ "samples",
578
+ "sample",
579
+ "tutorials",
580
+ "tutorial",
581
+ "code_samples",
582
+ "code-samples",
583
+ "notebooks",
565
584
  "tests",
566
585
  "test",
567
586
  "__tests__",
568
587
  "__test__",
569
588
  "spec",
570
589
  "__mocks__",
571
- "fixtures",
572
590
  "test_data",
573
591
  ".next",
574
592
  ".nuxt",
575
593
  "coverage",
576
- ".turbo"
594
+ ".turbo",
595
+ "public"
577
596
  ];
578
597
  const FIND_PRUNE_DIRS = [
579
598
  "node_modules",
580
599
  "dist",
581
600
  "build",
582
601
  ".git",
602
+ ".agents",
583
603
  "vendor",
604
+ "examples",
605
+ "example",
606
+ "demos",
607
+ "demo",
608
+ "bench",
609
+ "benches",
610
+ "benchmarks",
611
+ "fixtures",
612
+ "fixture",
613
+ "samples",
614
+ "sample",
615
+ "tutorials",
616
+ "tutorial",
617
+ "code_samples",
618
+ "code-samples",
619
+ "notebooks",
584
620
  ".next",
585
621
  ".nuxt",
586
622
  "coverage",
587
- ".turbo"
623
+ ".turbo",
624
+ "public"
588
625
  ];
626
+ const BUILD_CACHE_FILE_PATTERNS = [/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i];
627
+ const isBuildCacheFile = (filePath) => BUILD_CACHE_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
589
628
  const TEST_FILE_PATTERNS = [
590
629
  /(?:^|\/).*\.test\.[^/]+$/i,
591
630
  /(?:^|\/).*\.spec\.[^/]+$/i,
@@ -610,6 +649,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
610
649
  return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
611
650
  };
612
651
  const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
652
+ const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
613
653
  const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
614
654
  const getIgnoredPaths = (rootDirectory, files) => {
615
655
  if (files.length === 0) return /* @__PURE__ */ new Set();
@@ -668,7 +708,7 @@ const normalizeExcludePatterns = (patterns) => {
668
708
  return [p];
669
709
  });
670
710
  };
671
- const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = []) => {
711
+ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = [], include = []) => {
672
712
  const extraSet = new Set(extraExtensions);
673
713
  const normalizedFiles = files.map((file) => {
674
714
  const absolutePath = path.isAbsolute(file) ? file : path.resolve(rootDirectory, file);
@@ -683,8 +723,16 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
683
723
  if (!normalizedExcludePatterns.length) return false;
684
724
  return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
685
725
  };
726
+ const hasIncludePatterns = include.length > 0;
727
+ const isUserIncluded = (relativePath) => {
728
+ if (!hasIncludePatterns) return true;
729
+ return micromatch.isMatch(relativePath, include, { dot: true });
730
+ };
686
731
  return normalizedFiles.filter(({ absolutePath, relativePath }) => {
687
- return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile$2(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
732
+ if (!fs.existsSync(absolutePath) || !isWithinProject(relativePath) || isExcludedPath(relativePath) || isTestFile$2(relativePath) || isBuildCacheFile(relativePath) || ignoredPaths.has(relativePath)) return false;
733
+ if (!isUserIncluded(relativePath)) return false;
734
+ if (isUserExcluded(relativePath)) return false;
735
+ return hasAllowedExtension(relativePath, extraSet);
688
736
  }).map(({ absolutePath }) => absolutePath);
689
737
  };
690
738
  const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
@@ -1869,6 +1917,86 @@ const PYTHON_IMPORT_TO_PIP = {
1869
1917
  redis: "redis"
1870
1918
  };
1871
1919
 
1920
+ //#endregion
1921
+ //#region src/engines/ai-slop/python-manifest.ts
1922
+ const addPyDep = (pyDeps, name) => {
1923
+ const normalized = name.toLowerCase().replace(/_/g, "-");
1924
+ pyDeps.add(normalized);
1925
+ };
1926
+ const collectFromRequirementsTxt = (rootDir, pyDeps) => {
1927
+ const reqPath = path.join(rootDir, "requirements.txt");
1928
+ if (!fs.existsSync(reqPath)) return false;
1929
+ try {
1930
+ const content = fs.readFileSync(reqPath, "utf-8");
1931
+ for (const line of content.split("\n")) {
1932
+ const trimmed = line.trim();
1933
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
1934
+ const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
1935
+ if (match) addPyDep(pyDeps, match[1]);
1936
+ }
1937
+ return true;
1938
+ } catch {
1939
+ return false;
1940
+ }
1941
+ };
1942
+ const collectFromPyproject = (rootDir, pyDeps) => {
1943
+ const pyprojPath = path.join(rootDir, "pyproject.toml");
1944
+ if (!fs.existsSync(pyprojPath)) return false;
1945
+ try {
1946
+ const content = fs.readFileSync(pyprojPath, "utf-8");
1947
+ const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1948
+ if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
1949
+ const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1950
+ if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
1951
+ const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
1952
+ if (pep621) for (const line of pep621[1].split("\n")) {
1953
+ const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
1954
+ if (m) addPyDep(pyDeps, m[1]);
1955
+ }
1956
+ const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
1957
+ let match = poetryRe.exec(content);
1958
+ while (match !== null) {
1959
+ for (const line of match[1].split("\n")) {
1960
+ const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1961
+ if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
1962
+ }
1963
+ match = poetryRe.exec(content);
1964
+ }
1965
+ return true;
1966
+ } catch {
1967
+ return false;
1968
+ }
1969
+ };
1970
+ const collectFromPipfile = (rootDir, pyDeps) => {
1971
+ const pipfilePath = path.join(rootDir, "Pipfile");
1972
+ if (!fs.existsSync(pipfilePath)) return false;
1973
+ try {
1974
+ const content = fs.readFileSync(pipfilePath, "utf-8");
1975
+ const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
1976
+ let match = sectionRe.exec(content);
1977
+ while (match !== null) {
1978
+ for (const line of match[2].split("\n")) {
1979
+ const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1980
+ if (m) addPyDep(pyDeps, m[1]);
1981
+ }
1982
+ match = sectionRe.exec(content);
1983
+ }
1984
+ return true;
1985
+ } catch {
1986
+ return false;
1987
+ }
1988
+ };
1989
+ const collectPythonDeps = (rootDir) => {
1990
+ const pyDeps = /* @__PURE__ */ new Set();
1991
+ const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
1992
+ const hasPyproject = collectFromPyproject(rootDir, pyDeps);
1993
+ const hasPipfile = collectFromPipfile(rootDir, pyDeps);
1994
+ return {
1995
+ pyDeps,
1996
+ hasPyManifest: hasReq || hasPyproject || hasPipfile
1997
+ };
1998
+ };
1999
+
1872
2000
  //#endregion
1873
2001
  //#region src/engines/ai-slop/hallucinated-imports.ts
1874
2002
  const JS_EXTENSIONS$2 = new Set([
@@ -2005,10 +2133,26 @@ const buildAliasMatcher = (key) => {
2005
2133
  };
2006
2134
  const collectAliasMatchersFromConfig = (configPath, matchers) => {
2007
2135
  const opts = readJson(configPath)?.compilerOptions;
2008
- if (!opts || typeof opts !== "object") return;
2136
+ if (!opts) return;
2009
2137
  const paths = opts.paths;
2010
- if (!paths || typeof paths !== "object") return;
2011
- for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
2138
+ if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
2139
+ const baseUrl = opts.baseUrl;
2140
+ if (typeof baseUrl === "string") {
2141
+ const baseUrlDir = path.resolve(path.dirname(configPath), baseUrl);
2142
+ let entries;
2143
+ try {
2144
+ entries = fs.readdirSync(baseUrlDir);
2145
+ } catch {
2146
+ return;
2147
+ }
2148
+ const baseSpecifiers = /* @__PURE__ */ new Set();
2149
+ for (const entry of entries) {
2150
+ if (entry.startsWith(".") || entry === "node_modules") continue;
2151
+ const base = entry.replace(/\.(?:[jt]sx?|mjs|cjs|d\.ts)$/i, "");
2152
+ if (base.length > 0) baseSpecifiers.add(base);
2153
+ }
2154
+ for (const name of baseSpecifiers) matchers.push((spec) => spec === name || spec.startsWith(`${name}/`));
2155
+ }
2012
2156
  };
2013
2157
  const collectTsPathAliases = (rootDir) => {
2014
2158
  const matchers = [];
@@ -2016,97 +2160,35 @@ const collectTsPathAliases = (rootDir) => {
2016
2160
  for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
2017
2161
  return matchers;
2018
2162
  };
2019
- const addPyDep = (pyDeps, name) => {
2020
- const normalized = name.toLowerCase().replace(/_/g, "-");
2021
- pyDeps.add(normalized);
2022
- };
2023
- const collectFromRequirementsTxt = (rootDir, pyDeps) => {
2024
- const reqPath = path.join(rootDir, "requirements.txt");
2025
- if (!fs.existsSync(reqPath)) return false;
2026
- try {
2027
- const content = fs.readFileSync(reqPath, "utf-8");
2028
- for (const line of content.split("\n")) {
2029
- const trimmed = line.trim();
2030
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
2031
- const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
2032
- if (match) addPyDep(pyDeps, match[1]);
2033
- }
2034
- return true;
2035
- } catch {
2036
- return false;
2037
- }
2038
- };
2039
- const collectFromPyproject = (rootDir, pyDeps) => {
2040
- const pyprojPath = path.join(rootDir, "pyproject.toml");
2041
- if (!fs.existsSync(pyprojPath)) return false;
2042
- try {
2043
- const content = fs.readFileSync(pyprojPath, "utf-8");
2044
- const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
2045
- if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
2046
- const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
2047
- if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
2048
- const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
2049
- if (pep621) for (const line of pep621[1].split("\n")) {
2050
- const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
2051
- if (m) addPyDep(pyDeps, m[1]);
2052
- }
2053
- const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
2054
- let match = poetryRe.exec(content);
2055
- while (match !== null) {
2056
- for (const line of match[1].split("\n")) {
2057
- const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
2058
- if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
2059
- }
2060
- match = poetryRe.exec(content);
2061
- }
2062
- return true;
2063
- } catch {
2064
- return false;
2065
- }
2066
- };
2067
- const collectFromPipfile = (rootDir, pyDeps) => {
2068
- const pipfilePath = path.join(rootDir, "Pipfile");
2069
- if (!fs.existsSync(pipfilePath)) return false;
2070
- try {
2071
- const content = fs.readFileSync(pipfilePath, "utf-8");
2072
- const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
2073
- let match = sectionRe.exec(content);
2074
- while (match !== null) {
2075
- for (const line of match[2].split("\n")) {
2076
- const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
2077
- if (m) addPyDep(pyDeps, m[1]);
2078
- }
2079
- match = sectionRe.exec(content);
2080
- }
2081
- return true;
2082
- } catch {
2083
- return false;
2084
- }
2085
- };
2086
2163
  const loadManifest = (rootDir) => {
2087
2164
  const jsDeps = /* @__PURE__ */ new Set();
2088
- const pyDeps = /* @__PURE__ */ new Set();
2089
2165
  const hasJsManifest = collectJsDeps(rootDir, jsDeps);
2090
- const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
2091
- const hasPyproject = collectFromPyproject(rootDir, pyDeps);
2092
- const hasPipfile = collectFromPipfile(rootDir, pyDeps);
2166
+ const { pyDeps, hasPyManifest } = collectPythonDeps(rootDir);
2093
2167
  return {
2094
2168
  jsDeps,
2095
2169
  pyDeps,
2096
2170
  hasJsManifest,
2097
- hasPyManifest: hasReq || hasPyproject || hasPipfile
2171
+ hasPyManifest
2098
2172
  };
2099
2173
  };
2100
2174
  const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
2175
+ const RUNTIME_BUILTINS = new Set(["bun"]);
2101
2176
  const isJsBuiltin = (spec) => {
2177
+ if (RUNTIME_BUILTINS.has(spec)) return true;
2102
2178
  return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
2103
2179
  };
2104
2180
  const VIRTUAL_MODULE_PREFIXES = [
2105
2181
  "astro:",
2106
2182
  "virtual:",
2107
- "bun:"
2183
+ "bun:",
2184
+ "~icons/"
2108
2185
  ];
2109
2186
  const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
2187
+ const stripImportQuery = (spec) => {
2188
+ const idx = spec.indexOf("?");
2189
+ return idx === -1 ? spec : spec.slice(0, idx);
2190
+ };
2191
+ const VIRTUAL_ASSET_FILES = { "unfonts.css": "unplugin-fonts" };
2110
2192
  const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
2111
2193
  const isLikelyRealImportSpec = (spec) => {
2112
2194
  if (spec.length === 0) return false;
@@ -2173,10 +2255,14 @@ const extractPyImports = (content) => {
2173
2255
  }
2174
2256
  return results;
2175
2257
  };
2176
- const checkJsImport = (spec, manifest, tsAliasMatchers) => {
2258
+ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
2259
+ const spec = stripImportQuery(rawSpec);
2260
+ if (spec.length === 0) return null;
2177
2261
  if (isJsRelativeOrAbsolute(spec)) return null;
2178
2262
  if (isJsBuiltin(spec)) return null;
2179
2263
  if (isJsVirtualModule(spec)) return null;
2264
+ const virtualOwner = VIRTUAL_ASSET_FILES[spec];
2265
+ if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
2180
2266
  if (tsAliasMatchers.some((m) => m(spec))) return null;
2181
2267
  const pkg = packageNameFromImport(spec);
2182
2268
  if (manifest.jsDeps.has(pkg)) return null;
@@ -3520,64 +3606,88 @@ const analyzeFunctions = (content, ext) => {
3520
3606
  }
3521
3607
  return functions;
3522
3608
  };
3523
- const JSX_FILE_LOC_MULTIPLIER = 1.5;
3609
+ const FILE_LOC_MULTIPLIERS = {
3610
+ ".tsx": 1.5,
3611
+ ".jsx": 1.5,
3612
+ ".rs": 2.5,
3613
+ ".go": 1.5
3614
+ };
3615
+ const DECLARATION_FILE_RE = /\.d\.ts$/i;
3616
+ const fileLocBudget = (ext, relativePath, base) => {
3617
+ if (DECLARATION_FILE_RE.test(relativePath)) return Number.POSITIVE_INFINITY;
3618
+ const multiplier = FILE_LOC_MULTIPLIERS[ext] ?? 1;
3619
+ return Math.ceil(base * multiplier);
3620
+ };
3524
3621
  const checkFileDiagnostics = (relativePath, content, limits) => {
3525
3622
  const results = [];
3526
3623
  const lineCount = content.split("\n").length;
3527
3624
  const ext = path.extname(relativePath).toLowerCase();
3528
3625
  if (isDataFile(content)) return results;
3529
- const configuredMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
3626
+ const configuredMax = fileLocBudget(ext, relativePath, limits.maxFileLoc);
3627
+ if (!Number.isFinite(configuredMax)) return results;
3530
3628
  if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
3531
3629
  filePath: relativePath,
3532
3630
  engine: "code-quality",
3533
3631
  rule: "complexity/file-too-large",
3534
3632
  severity: "warning",
3535
- message: `File has ${lineCount} lines (max: ${configuredMax})`,
3633
+ message: `File too large (max: ${configuredMax})`,
3536
3634
  help: "Consider splitting this file into smaller modules",
3537
3635
  line: 0,
3538
3636
  column: 0,
3539
3637
  category: "Complexity",
3540
- fixable: false
3638
+ fixable: false,
3639
+ detail: `${lineCount} lines`
3541
3640
  });
3542
3641
  return results;
3543
3642
  };
3544
- const checkFunctionDiagnostics = (relativePath, fn, limits) => {
3643
+ const JSX_EXTENSIONS = new Set([".tsx", ".jsx"]);
3644
+ const isComponentFunction = (name, ext) => JSX_EXTENSIONS.has(ext) && /^[A-Z]/.test(name);
3645
+ const functionLocBudget = (fn, ext, base) => {
3646
+ if (isComponentFunction(fn.name, ext)) return Math.ceil(base * 2);
3647
+ if (ext === ".rs") return Math.ceil(base * 1.5);
3648
+ return base;
3649
+ };
3650
+ const checkFunctionDiagnostics = (relativePath, fn, limits, ext) => {
3545
3651
  const results = [];
3546
- if (fn.lineCount - fn.templateLines > Math.ceil(limits.maxFunctionLoc * 1.1)) results.push({
3652
+ const fnMax = functionLocBudget(fn, ext, limits.maxFunctionLoc);
3653
+ if (fn.lineCount - fn.templateLines > Math.ceil(fnMax * 1.1)) results.push({
3547
3654
  filePath: relativePath,
3548
3655
  engine: "code-quality",
3549
3656
  rule: "complexity/function-too-long",
3550
3657
  severity: "warning",
3551
- message: `Function '${fn.name}' has ${fn.lineCount} lines (max: ${limits.maxFunctionLoc})`,
3658
+ message: `Function too long (max: ${fnMax})`,
3552
3659
  help: "Consider breaking this function into smaller pieces",
3553
3660
  line: fn.startLine,
3554
3661
  column: 0,
3555
3662
  category: "Complexity",
3556
- fixable: false
3663
+ fixable: false,
3664
+ detail: `${fn.name} · ${fn.lineCount} lines`
3557
3665
  });
3558
3666
  if (fn.maxNesting > limits.maxNesting) results.push({
3559
3667
  filePath: relativePath,
3560
3668
  engine: "code-quality",
3561
3669
  rule: "complexity/deep-nesting",
3562
3670
  severity: "warning",
3563
- message: `Function '${fn.name}' has nesting depth ${fn.maxNesting} (max: ${limits.maxNesting})`,
3671
+ message: `Function nested too deeply (max: ${limits.maxNesting})`,
3564
3672
  help: "Consider using early returns or extracting nested logic",
3565
3673
  line: fn.startLine,
3566
3674
  column: 0,
3567
3675
  category: "Complexity",
3568
- fixable: false
3676
+ fixable: false,
3677
+ detail: `${fn.name} · depth ${fn.maxNesting}`
3569
3678
  });
3570
3679
  if (fn.paramCount > limits.maxParams) results.push({
3571
3680
  filePath: relativePath,
3572
3681
  engine: "code-quality",
3573
3682
  rule: "complexity/too-many-params",
3574
3683
  severity: "warning",
3575
- message: `Function '${fn.name}' has ${fn.paramCount} parameters (max: ${limits.maxParams})`,
3684
+ message: `Function has too many parameters (max: ${limits.maxParams})`,
3576
3685
  help: "Consider using an options object parameter",
3577
3686
  line: fn.startLine,
3578
3687
  column: 0,
3579
3688
  category: "Complexity",
3580
- fixable: false
3689
+ fixable: false,
3690
+ detail: `${fn.name} · ${fn.paramCount} params`
3581
3691
  });
3582
3692
  return results;
3583
3693
  };
@@ -3592,7 +3702,7 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
3592
3702
  }
3593
3703
  const ext = path.extname(filePath).toLowerCase();
3594
3704
  const diagnostics = checkFileDiagnostics(relativePath, content, limits);
3595
- for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
3705
+ for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits, ext));
3596
3706
  return diagnostics;
3597
3707
  };
3598
3708
  const checkComplexity = async (context) => {
@@ -3697,17 +3807,19 @@ const findDuplicateBlocks = (content, relativePath) => {
3697
3807
  });
3698
3808
  }
3699
3809
  return reports.map((r) => {
3810
+ const span = r.currentEnd - r.currentStart + 1;
3700
3811
  return {
3701
3812
  filePath: relativePath,
3702
3813
  engine: "code-quality",
3703
3814
  rule: "code-quality/duplicate-block",
3704
3815
  severity: "warning",
3705
- message: `${r.currentEnd - r.currentStart + 1}-line block at line ${r.currentStart} duplicates a block starting at line ${r.priorStart}. Extract a shared helper.`,
3816
+ message: "Duplicate code block extract a shared helper",
3706
3817
  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.`,
3707
3818
  line: r.currentStart,
3708
3819
  column: 0,
3709
3820
  category: "Complexity",
3710
- fixable: false
3821
+ fixable: false,
3822
+ detail: `${span} lines duplicate block at L${r.priorStart}`
3711
3823
  };
3712
3824
  });
3713
3825
  };
@@ -4284,16 +4396,34 @@ const fixGofmt = async (rootDirectory) => {
4284
4396
  if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `gofmt exited with code ${result.exitCode}`);
4285
4397
  };
4286
4398
 
4399
+ //#endregion
4400
+ //#region src/engines/python-targets.ts
4401
+ const PYTHON_EXTENSIONS = new Set([".py", ".pyi"]);
4402
+ const normalizeProjectPath = (filePath) => filePath.split(path.sep).join("/");
4403
+ const getPythonTargets = (context) => {
4404
+ const targets = (context.files ?? getSourceFiles(context)).filter((filePath) => PYTHON_EXTENSIONS.has(path.extname(filePath).toLowerCase())).map((filePath) => {
4405
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(context.rootDirectory, filePath);
4406
+ return normalizeProjectPath(path.relative(context.rootDirectory, absolutePath));
4407
+ }).filter((filePath) => filePath.length > 0 && !filePath.startsWith(".."));
4408
+ return [...new Set(targets)];
4409
+ };
4410
+ const getRuffDiagnosticPath = (rootDirectory, filePath) => {
4411
+ const normalizedPath = filePath.replace(/^a\//, "");
4412
+ return normalizeProjectPath(path.isAbsolute(normalizedPath) ? path.relative(rootDirectory, normalizedPath) : normalizedPath);
4413
+ };
4414
+
4287
4415
  //#endregion
4288
4416
  //#region src/engines/format/ruff-format.ts
4289
4417
  const runRuffFormat = async (context) => {
4290
4418
  const ruffBinary = resolveToolBinary("ruff");
4419
+ const targets = getPythonTargets(context);
4420
+ if (targets.length === 0) return [];
4291
4421
  try {
4292
4422
  const result = await runSubprocess(ruffBinary, [
4293
4423
  "format",
4294
4424
  "--check",
4295
4425
  "--diff",
4296
- context.rootDirectory
4426
+ ...targets
4297
4427
  ], {
4298
4428
  cwd: context.rootDirectory,
4299
4429
  timeout: 6e4
@@ -4309,9 +4439,9 @@ const parseRuffFormatOutput = (output, rootDir) => {
4309
4439
  const filePattern = /^--- (.+)$/gm;
4310
4440
  let match;
4311
4441
  while ((match = filePattern.exec(output)) !== null) {
4312
- const filePath = match[1].replace(/^a\//, "");
4442
+ const filePath = getRuffDiagnosticPath(rootDir, match[1]);
4313
4443
  diagnostics.push({
4314
- filePath: path.relative(rootDir, filePath),
4444
+ filePath,
4315
4445
  engine: "format",
4316
4446
  rule: "python-formatting",
4317
4447
  severity: "warning",
@@ -4715,6 +4845,95 @@ const resolveOxlintBinary = () => {
4715
4845
  return "oxlint";
4716
4846
  }
4717
4847
  };
4848
+ const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
4849
+ const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
4850
+ const AMBIENT_GLOBAL_DEPS = [
4851
+ "unplugin-icons",
4852
+ "@types/bun",
4853
+ "bun-types"
4854
+ ];
4855
+ const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
4856
+ const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
4857
+ const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
4858
+ const detectAmbientSources = (rootDir) => {
4859
+ const found = /* @__PURE__ */ new Set();
4860
+ const skipDirs = new Set([
4861
+ "node_modules",
4862
+ ".git",
4863
+ "dist",
4864
+ "build",
4865
+ "out",
4866
+ "target",
4867
+ "coverage",
4868
+ ".next",
4869
+ ".turbo"
4870
+ ]);
4871
+ const walk = (dir, depth) => {
4872
+ if (depth > 4 || found.size === AMBIENT_GLOBAL_DEPS.length) return;
4873
+ let entries;
4874
+ try {
4875
+ entries = fs.readdirSync(dir, { withFileTypes: true });
4876
+ } catch {
4877
+ return;
4878
+ }
4879
+ for (const entry of entries) {
4880
+ if (found.size === AMBIENT_GLOBAL_DEPS.length) return;
4881
+ if (entry.name.startsWith(".") && entry.name !== ".github") continue;
4882
+ if (skipDirs.has(entry.name)) continue;
4883
+ const full = path.join(dir, entry.name);
4884
+ if (entry.isDirectory()) walk(full, depth + 1);
4885
+ else if (entry.name === "package.json") try {
4886
+ const pkg = JSON.parse(fs.readFileSync(full, "utf-8"));
4887
+ const allDeps = {
4888
+ ...pkg.dependencies ?? {},
4889
+ ...pkg.devDependencies ?? {},
4890
+ ...pkg.peerDependencies ?? {}
4891
+ };
4892
+ for (const dep of AMBIENT_GLOBAL_DEPS) if (dep in allDeps) found.add(dep);
4893
+ } catch {}
4894
+ }
4895
+ };
4896
+ walk(rootDir, 0);
4897
+ return found;
4898
+ };
4899
+ const extractNoUndefIdentifier = (message) => {
4900
+ return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
4901
+ };
4902
+ const isAmbientFalsePositive = (rule, message, sources) => {
4903
+ if (rule !== "eslint/no-undef") return false;
4904
+ const ident = extractNoUndefIdentifier(message);
4905
+ if (!ident) return false;
4906
+ if (sources.has("unplugin-icons") && ICON_AUTOIMPORT_RE.test(ident)) return true;
4907
+ if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
4908
+ return false;
4909
+ };
4910
+ const sstReferencedFiles = /* @__PURE__ */ new Map();
4911
+ const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
4912
+ const cached = sstReferencedFiles.get(relativeFilePath);
4913
+ if (cached !== void 0) return cached;
4914
+ const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
4915
+ let referenced = false;
4916
+ try {
4917
+ const fd = fs.openSync(absolute, "r");
4918
+ try {
4919
+ const buf = Buffer.alloc(512);
4920
+ const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
4921
+ referenced = SST_PLATFORM_REF_RE.test(buf.toString("utf-8", 0, bytesRead));
4922
+ } finally {
4923
+ fs.closeSync(fd);
4924
+ }
4925
+ } catch {
4926
+ referenced = false;
4927
+ }
4928
+ sstReferencedFiles.set(relativeFilePath, referenced);
4929
+ return referenced;
4930
+ };
4931
+ const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
4932
+ const isUnderscoreUnusedVar = (rule, message) => {
4933
+ if (rule !== "eslint/no-unused-vars") return false;
4934
+ const match = UNUSED_VAR_IDENT_RE.exec(message);
4935
+ return match ? match[1].startsWith("_") : false;
4936
+ };
4718
4937
  const parseRuleCode = (code) => {
4719
4938
  if (!code) return {
4720
4939
  plugin: "eslint",
@@ -4811,6 +5030,8 @@ const runOxlint = async (context) => {
4811
5030
  framework: context.frameworks.find((f) => f !== "none"),
4812
5031
  testFramework: detectTestFramework(context.rootDirectory)
4813
5032
  });
5033
+ const ambientSources = detectAmbientSources(context.rootDirectory);
5034
+ sstReferencedFiles.clear();
4814
5035
  try {
4815
5036
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
4816
5037
  const args = [
@@ -4850,6 +5071,11 @@ const runOxlint = async (context) => {
4850
5071
  fixable: false
4851
5072
  };
4852
5073
  }).filter((d) => {
5074
+ if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
5075
+ if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
5076
+ if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
5077
+ if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
5078
+ if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
4853
5079
  const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
4854
5080
  if (seen.has(key)) return false;
4855
5081
  seen.add(key);
@@ -4913,18 +5139,20 @@ const fixOxlint = async (context, options = {}) => {
4913
5139
  //#region src/engines/lint/ruff.ts
4914
5140
  const runRuffLint = async (context) => {
4915
5141
  const ruffBinary = resolveToolBinary("ruff");
5142
+ const targets = getPythonTargets(context);
5143
+ if (targets.length === 0) return [];
4916
5144
  try {
4917
5145
  const output = (await runSubprocess(ruffBinary, [
4918
5146
  "check",
4919
5147
  "--output-format=json",
4920
- context.rootDirectory
5148
+ ...targets
4921
5149
  ], {
4922
5150
  cwd: context.rootDirectory,
4923
5151
  timeout: 6e4
4924
5152
  })).stdout;
4925
5153
  if (!output) return [];
4926
5154
  return JSON.parse(output).map((d) => ({
4927
- filePath: path.relative(context.rootDirectory, d.filename),
5155
+ filePath: getRuffDiagnosticPath(context.rootDirectory, d.filename),
4928
5156
  engine: "lint",
4929
5157
  rule: `ruff/${d.code}`,
4930
5158
  severity: d.code.startsWith("E") || d.code.startsWith("F") ? "error" : "warning",
@@ -5038,56 +5266,94 @@ const runPnpmAuditWithFallback = async (rootDir, timeout) => {
5038
5266
  return [];
5039
5267
  }
5040
5268
  };
5269
+ const SEVERITY_RANK = {
5270
+ critical: 4,
5271
+ high: 3,
5272
+ moderate: 2,
5273
+ low: 1
5274
+ };
5041
5275
  const toSeverity = (value) => value === "critical" || value === "high" ? "error" : "warning";
5042
- const defaultAuditFixCommand = (source) => source === "pnpm audit" ? "pnpm audit --fix" : "npm audit fix";
5276
+ const upsertVuln = (bucket, packageName, severity, recommendation) => {
5277
+ const existing = bucket.get(packageName);
5278
+ if (existing) {
5279
+ existing.advisories++;
5280
+ if ((SEVERITY_RANK[severity] ?? 0) > (SEVERITY_RANK[existing.worstSeverity] ?? 0)) existing.worstSeverity = severity;
5281
+ if (recommendation) existing.recommendations.add(recommendation);
5282
+ } else bucket.set(packageName, {
5283
+ packageName,
5284
+ worstSeverity: severity,
5285
+ advisories: 1,
5286
+ recommendations: recommendation ? new Set([recommendation]) : /* @__PURE__ */ new Set()
5287
+ });
5288
+ };
5289
+ const SEMVER_RE = /(\d+)\.(\d+)\.(\d+)/;
5290
+ const cmpSemver = (a, b) => {
5291
+ const [, a1, a2, a3] = SEMVER_RE.exec(a) ?? [
5292
+ "",
5293
+ "0",
5294
+ "0",
5295
+ "0"
5296
+ ];
5297
+ const [, b1, b2, b3] = SEMVER_RE.exec(b) ?? [
5298
+ "",
5299
+ "0",
5300
+ "0",
5301
+ "0"
5302
+ ];
5303
+ if (Number(a1) !== Number(b1)) return Number(a1) - Number(b1);
5304
+ if (Number(a2) !== Number(b2)) return Number(a2) - Number(b2);
5305
+ return Number(a3) - Number(b3);
5306
+ };
5307
+ const pickBestRecommendation = (recs) => {
5308
+ if (recs.length <= 1) return recs[0] ?? "";
5309
+ const versioned = recs.filter((r) => SEMVER_RE.test(r));
5310
+ if (versioned.length === 0) return recs[0];
5311
+ return versioned.reduce((best, r) => cmpSemver(r, best) > 0 ? r : best);
5312
+ };
5313
+ const cleanRecommendation = (raw) => {
5314
+ const t = raw.trim();
5315
+ if (!t || t.toLowerCase() === "none") return "no fix available";
5316
+ return t;
5317
+ };
5318
+ const aggregateToDiagnostic = (agg, source) => {
5319
+ const best = cleanRecommendation(pickBestRecommendation([...agg.recommendations]));
5320
+ const countLabel = agg.advisories > 1 ? ` (${agg.advisories} advisories)` : "";
5321
+ const recLabel = best ? ` — ${best}` : "";
5322
+ return {
5323
+ filePath: "package.json",
5324
+ engine: "security",
5325
+ rule: "security/vulnerable-dependency",
5326
+ severity: toSeverity(agg.worstSeverity),
5327
+ message: `${agg.packageName} (${agg.worstSeverity})${recLabel}${countLabel}`,
5328
+ help: "",
5329
+ line: 0,
5330
+ column: 0,
5331
+ category: "Security",
5332
+ fixable: false,
5333
+ detail: source === "npm audit" ? "npm" : "pnpm"
5334
+ };
5335
+ };
5043
5336
  const parseLegacyAdvisories = (advisories, source) => {
5044
- const diagnostics = [];
5045
- for (const [key, advisory] of Object.entries(advisories)) {
5046
- const packageName = advisory.module_name ?? advisory.name ?? advisory.package ?? key;
5047
- const severity = (advisory.severity ?? "moderate").toLowerCase();
5048
- const recommendation = advisory.recommendation ?? advisory.title ?? `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
5049
- diagnostics.push({
5050
- filePath: "package.json",
5051
- engine: "security",
5052
- rule: "security/vulnerable-dependency",
5053
- severity: toSeverity(severity),
5054
- message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
5055
- help: withFixHint(recommendation),
5056
- line: 0,
5057
- column: 0,
5058
- category: "Security",
5059
- fixable: false
5060
- });
5061
- }
5062
- return diagnostics;
5337
+ const bucket = /* @__PURE__ */ new Map();
5338
+ 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 ?? "");
5339
+ return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
5063
5340
  };
5064
5341
  const parseModernVulnerabilities = (vulnerabilities, source) => {
5065
- const diagnostics = [];
5342
+ const bucket = /* @__PURE__ */ new Map();
5066
5343
  for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
5067
5344
  const severity = (vulnerability.severity ?? "moderate").toLowerCase();
5068
5345
  const fixAvailable = vulnerability.fixAvailable;
5069
5346
  const isDirect = vulnerability.isDirect === true;
5070
- let recommendation = `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
5071
- 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";
5072
- else if (!isDirect && fixAvailable === true) recommendation = "Transitive dep — may need an override or parent upgrade";
5347
+ let recommendation = "";
5348
+ if (fixAvailable === false) recommendation = isDirect ? "no automatic fix" : "transitiveneeds override or parent upgrade";
5349
+ else if (!isDirect && fixAvailable === true) recommendation = "transitive — may need override or parent upgrade";
5073
5350
  else if (fixAvailable && typeof fixAvailable === "object" && "name" in fixAvailable && "version" in fixAvailable) {
5074
5351
  const target = fixAvailable;
5075
- if (target.name && target.version) recommendation = `Upgrade to ${target.name}@${target.version}.`;
5352
+ if (target.name && target.version) recommendation = `upgrade to ${target.name}@${target.version}`;
5076
5353
  }
5077
- diagnostics.push({
5078
- filePath: "package.json",
5079
- engine: "security",
5080
- rule: "security/vulnerable-dependency",
5081
- severity: toSeverity(severity),
5082
- message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
5083
- help: withFixHint(recommendation),
5084
- line: 0,
5085
- column: 0,
5086
- category: "Security",
5087
- fixable: false
5088
- });
5354
+ upsertVuln(bucket, packageName, severity, recommendation);
5089
5355
  }
5090
- return diagnostics;
5356
+ return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
5091
5357
  };
5092
5358
  const parseJsAudit = (output, source) => {
5093
5359
  if (!output) return [];
@@ -6266,8 +6532,53 @@ const wrapHelpText = (text, maxWidth, indent) => {
6266
6532
  };
6267
6533
  const terminalWidth = () => {
6268
6534
  const raw = process.stdout.columns;
6269
- if (typeof raw !== "number" || raw <= 0) return 100;
6270
- return Math.min(raw, 100);
6535
+ if (typeof raw !== "number" || raw <= 0) return 120;
6536
+ return Math.min(raw, 120);
6537
+ };
6538
+ const renderRuleHeader = (first, count, lines) => {
6539
+ const level = toSeverityLabel(first.severity);
6540
+ const countLabel = count > 1 ? ` (${count})` : "";
6541
+ const status = colorBySeverity(level, first.severity);
6542
+ const fixableTag = first.fixable ? ` ${style(theme, "muted", "[auto]")}` : "";
6543
+ const fixableWidth = first.fixable ? 7 : 0;
6544
+ const badgePrefix = ` [${status}]${fixableTag} `;
6545
+ const badgePrefixWidth = 5 + level.length + 1 + fixableWidth + 1;
6546
+ const wrapped = wrapText(`${first.message}${countLabel}`, terminalWidth(), badgePrefixWidth, " ");
6547
+ lines.push(`${badgePrefix}${wrapped[0]}`);
6548
+ for (let i = 1; i < wrapped.length; i++) lines.push(wrapped[i]);
6549
+ };
6550
+ const renderLocations = (ruleDiags, verbose, lines) => {
6551
+ const unique = [];
6552
+ const seen = /* @__PURE__ */ new Set();
6553
+ for (const d of ruleDiags) {
6554
+ const label = toLocationLabel(d);
6555
+ const detail = d.detail ?? "";
6556
+ const key = `${label}|${detail}`;
6557
+ if (seen.has(key)) continue;
6558
+ seen.add(key);
6559
+ unique.push({
6560
+ label,
6561
+ detail
6562
+ });
6563
+ }
6564
+ const shown = verbose ? unique : unique.slice(0, 3);
6565
+ const maxLabel = shown.reduce((w, l) => Math.max(w, l.label.length), 0);
6566
+ for (const { label, detail } of shown) {
6567
+ const padded = detail ? `${label.padEnd(maxLabel)} ${detail}` : label;
6568
+ lines.push(style(theme, "muted", ` ${padded}`));
6569
+ }
6570
+ if (!verbose && unique.length > shown.length) lines.push(style(theme, "muted", ` +${unique.length - shown.length} more location(s), use -d for full list`));
6571
+ };
6572
+ const renderHiddenFooter = (sorted, maxRules, lines) => {
6573
+ const hidden = sorted.slice(maxRules);
6574
+ const hiddenErrors = hidden.reduce((acc, [, diags]) => acc + (diags[0].severity === "error" ? diags.length : 0), 0);
6575
+ const hiddenWarnings = hidden.reduce((acc, [, diags]) => acc + (diags[0].severity === "warning" ? diags.length : 0), 0);
6576
+ const parts = [];
6577
+ if (hiddenErrors > 0) parts.push(`${hiddenErrors} error${hiddenErrors === 1 ? "" : "s"}`);
6578
+ if (hiddenWarnings > 0) parts.push(`${hiddenWarnings} warning${hiddenWarnings === 1 ? "" : "s"}`);
6579
+ const detail = parts.length > 0 ? ` (${parts.join(", ")})` : "";
6580
+ lines.push(style(theme, "muted", ` ... and ${hidden.length} more rules hidden${detail}. Run with -v or --verbose to see full output.`));
6581
+ lines.push("");
6271
6582
  };
6272
6583
  const renderDiagnostics = (diagnostics, verbose) => {
6273
6584
  const lines = [];
@@ -6276,29 +6587,23 @@ const renderDiagnostics = (diagnostics, verbose) => {
6276
6587
  const label = getEngineLabel(engine);
6277
6588
  lines.push(` ${style(theme, "bold", `${symbols.engineActive} ${label}`)}`);
6278
6589
  const sorted = [...groupBy(engineDiags, (d) => `${d.rule}:${d.message}`).entries()].sort(([, a], [, b]) => {
6279
- return (a[0].severity === "error" ? 0 : a[0].severity === "warning" ? 1 : 2) - (b[0].severity === "error" ? 0 : b[0].severity === "warning" ? 1 : 2);
6590
+ const sa = a[0].severity === "error" ? 0 : a[0].severity === "warning" ? 1 : 2;
6591
+ const sb = b[0].severity === "error" ? 0 : b[0].severity === "warning" ? 1 : 2;
6592
+ if (sa !== sb) return sa - sb;
6593
+ return b.length - a.length;
6280
6594
  });
6281
- for (const [, ruleDiags] of sorted) {
6595
+ const maxRules = verbose ? Infinity : 40;
6596
+ for (const [, ruleDiags] of sorted.slice(0, maxRules)) {
6282
6597
  const first = ruleDiags[0];
6283
- const level = toSeverityLabel(first.severity);
6284
- const count = ruleDiags.length > 1 ? ` (${ruleDiags.length})` : "";
6285
- const status = colorBySeverity(level, first.severity);
6286
- const fixableTag = first.fixable ? ` ${style(theme, "muted", "[auto]")}` : "";
6287
- const fixableWidth = first.fixable ? 7 : 0;
6288
- const badgePrefix = ` [${status}]${fixableTag} `;
6289
- const badgePrefixWidth = 5 + level.length + 1 + fixableWidth + 1;
6290
- const wrappedMsg = wrapText(`${first.message}${count}`, terminalWidth(), badgePrefixWidth, " ");
6291
- lines.push(`${badgePrefix}${wrappedMsg[0]}`);
6292
- for (let i = 1; i < wrappedMsg.length; i++) lines.push(wrappedMsg[i]);
6293
- const locations = verbose ? ruleDiags : ruleDiags.slice(0, 3);
6294
- for (const diagnostic of locations) lines.push(style(theme, "muted", ` ${toLocationLabel(diagnostic)}`));
6295
- if (!verbose && ruleDiags.length > locations.length) lines.push(style(theme, "muted", ` +${ruleDiags.length - locations.length} more location(s), use -d for full list`));
6598
+ renderRuleHeader(first, ruleDiags.length, lines);
6599
+ renderLocations(ruleDiags, verbose, lines);
6296
6600
  if (first.help) {
6297
6601
  const wrapped = wrapHelpText(first.help, terminalWidth(), " ");
6298
6602
  for (const line of wrapped) lines.push(style(theme, "muted", line));
6299
6603
  }
6300
6604
  lines.push("");
6301
6605
  }
6606
+ if (sorted.length > maxRules) renderHiddenFooter(sorted, maxRules, lines);
6302
6607
  }
6303
6608
  return `${lines.join("\n")}\n`;
6304
6609
  };
@@ -6436,6 +6741,81 @@ var LiveGrid = class {
6436
6741
  }
6437
6742
  };
6438
6743
 
6744
+ //#endregion
6745
+ //#region src/output/rule-labels.ts
6746
+ const RULE_LABELS = {
6747
+ formatting: "Code not formatted",
6748
+ "code-quality/duplicate-block": "Duplicate code block",
6749
+ "complexity/file-too-large": "File too large",
6750
+ "complexity/function-too-long": "Function too long",
6751
+ "complexity/deep-nesting": "Deeply nested code",
6752
+ "complexity/too-many-params": "Too many parameters",
6753
+ "knip/files": "Unused file",
6754
+ "knip/dependencies": "Unused dependency",
6755
+ "knip/devDependencies": "Unused dev dependency",
6756
+ "knip/unlisted": "Used but not in package.json",
6757
+ "knip/unresolved": "Unresolved import",
6758
+ "knip/binaries": "Unused binary",
6759
+ "knip/exports": "Unused export",
6760
+ "knip/types": "Unused type",
6761
+ "ai-slop/trivial-comment": "Trivial restating comment",
6762
+ "ai-slop/swallowed-exception": "Empty catch (swallowed error)",
6763
+ "ai-slop/thin-wrapper": "Thin function wrapper",
6764
+ "ai-slop/generic-naming": "Generic/vague identifier name",
6765
+ "ai-slop/unused-import": "Unused import",
6766
+ "ai-slop/console-leftover": "console.log left in code",
6767
+ "ai-slop/todo-stub": "Unresolved TODO/FIXME",
6768
+ "ai-slop/unreachable-code": "Unreachable code",
6769
+ "ai-slop/constant-condition": "Constant condition",
6770
+ "ai-slop/empty-function": "Empty function body",
6771
+ "ai-slop/unsafe-type-assertion": "Unsafe type cast",
6772
+ "ai-slop/double-type-assertion": "Double type cast",
6773
+ "ai-slop/ts-directive": "@ts-ignore / @ts-expect-error",
6774
+ "ai-slop/narrative-comment": "Narrative comment block",
6775
+ "ai-slop/duplicate-import": "Duplicate import statement",
6776
+ "ai-slop/python-bare-except": "Bare except",
6777
+ "ai-slop/python-broad-except": "Broad except",
6778
+ "ai-slop/python-mutable-default": "Mutable default argument",
6779
+ "ai-slop/python-print-debug": "print() left in code",
6780
+ "ai-slop/go-library-panic": "panic() in Go library code",
6781
+ "ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
6782
+ "ai-slop/rust-todo-stub": "Rust todo!() stub",
6783
+ "ai-slop/hallucinated-import": "Import not in package.json",
6784
+ "security/hardcoded-secret": "Possible hardcoded secret",
6785
+ "security/vulnerable-dependency": "Vulnerable dependency",
6786
+ "security/eval": "eval() usage",
6787
+ "security/innerhtml": "innerHTML assignment",
6788
+ "security/dangerously-set-innerhtml": "dangerouslySetInnerHTML (XSS risk)",
6789
+ "security/sql-injection": "Possible SQL injection",
6790
+ "security/shell-injection": "Possible shell injection",
6791
+ "eslint/no-undef": "Undefined identifier",
6792
+ "eslint/no-unused-vars": "Unused variable",
6793
+ "eslint/no-unassigned-vars": "Variable never assigned",
6794
+ "eslint/no-empty": "Empty block statement",
6795
+ "eslint/no-unused-expressions": "Unused expression",
6796
+ "eslint/no-shadow-restricted-names": "Shadowing restricted name",
6797
+ "eslint/no-constant-binary-expression": "Constant binary expression",
6798
+ "eslint/no-unsafe-optional-chaining": "Unsafe optional chaining",
6799
+ "eslint/require-yield": "Generator with no yield",
6800
+ "import/no-duplicates": "Duplicate import path",
6801
+ "import/default": "Missing default export",
6802
+ "import/named": "Missing named export",
6803
+ "import/namespace": "Invalid namespace import",
6804
+ "typescript-eslint/triple-slash-reference": "Triple-slash reference",
6805
+ "unicorn/no-useless-fallback-in-spread": "Useless spread fallback",
6806
+ "unicorn/no-invalid-remove-event-listener": "Invalid removeEventListener",
6807
+ "unicorn/no-empty-file": "Empty file",
6808
+ "unicorn/no-useless-length-check": "Useless array length check",
6809
+ "unicorn/no-new-array": "Avoid new Array(n)",
6810
+ "unicorn/no-useless-spread": "Useless spread",
6811
+ "unicorn/no-single-promise-in-promise-methods": "Single-element Promise.all"
6812
+ };
6813
+ const prettifyFallback = (ruleId) => {
6814
+ const spaced = (ruleId.includes("/") ? ruleId.slice(ruleId.indexOf("/") + 1) : ruleId).replace(/[-_]/g, " ").replace(/\//g, " · ");
6815
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
6816
+ };
6817
+ const labelForRule = (ruleId) => RULE_LABELS[ruleId] ?? prettifyFallback(ruleId);
6818
+
6439
6819
  //#endregion
6440
6820
  //#region src/ui/summary.ts
6441
6821
  const elapsed = (ms) => ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
@@ -6462,6 +6842,34 @@ const renderSummary = (input, deps = {}) => {
6462
6842
  ` ${style(t, "muted", `${input.files} files`)} ${sep} ${style(t, "muted", `${input.engines} engines`)} ${sep} ${style(t, "muted", elapsed(input.elapsedMs))}`,
6463
6843
  ""
6464
6844
  ];
6845
+ if (input.breakdown && input.breakdown.rows.length > 0) {
6846
+ lines.push(` ${style(t, "bold", "Top findings")}`);
6847
+ const maxCountWidth = input.breakdown.rows.reduce((w, r) => Math.max(w, String(r.errors + r.warnings + r.info).length), 0);
6848
+ const labels = input.breakdown.rows.map((r) => labelForRule(r.rule));
6849
+ const maxLabelWidth = labels.reduce((w, l) => Math.max(w, l.length), 0);
6850
+ for (let i = 0; i < input.breakdown.rows.length; i++) {
6851
+ const row = input.breakdown.rows[i];
6852
+ const total = row.errors + row.warnings + row.info;
6853
+ const count = String(total).padStart(maxCountWidth);
6854
+ const label = padEnd(labels[i], maxLabelWidth);
6855
+ const tags = [];
6856
+ if (row.errors > 0) tags.push(style(t, "danger", `${row.errors} err`));
6857
+ if (row.warnings > 0) tags.push(style(t, "warn", `${row.warnings} warn`));
6858
+ if (row.info > 0) tags.push(style(t, "muted", `${row.info} info`));
6859
+ if (row.fixable > 0) tags.push(style(t, "success", `${row.fixable} fix`));
6860
+ const tagBlock = tags.length > 0 ? ` ${style(t, "muted", "·")} ${tags.join(" ")}` : "";
6861
+ const ruleHint = style(t, "muted", `(${row.rule})`);
6862
+ lines.push(` ${style(t, "muted", count)} ${label} ${ruleHint}${tagBlock}`);
6863
+ }
6864
+ if (input.breakdown.hiddenRules > 0) {
6865
+ const hiddenParts = [];
6866
+ if (input.breakdown.hiddenErrors > 0) hiddenParts.push(`${input.breakdown.hiddenErrors} error${input.breakdown.hiddenErrors === 1 ? "" : "s"}`);
6867
+ if (input.breakdown.hiddenWarnings > 0) hiddenParts.push(`${input.breakdown.hiddenWarnings} warning${input.breakdown.hiddenWarnings === 1 ? "" : "s"}`);
6868
+ const detail = hiddenParts.length > 0 ? ` (${hiddenParts.join(", ")})` : "";
6869
+ lines.push(style(t, "muted", ` +${input.breakdown.hiddenRules} more rule${input.breakdown.hiddenRules === 1 ? "" : "s"}${detail}. Run with -v for the full list.`));
6870
+ }
6871
+ lines.push("");
6872
+ }
6465
6873
  if (input.nextSteps.length > 0) {
6466
6874
  for (const step of input.nextSteps) {
6467
6875
  const glyph = step.emphasis === "primary" ? s.hint : s.bullet;
@@ -6534,6 +6942,39 @@ const getStagedFiles = (cwd) => {
6534
6942
  //#region src/commands/scan.ts
6535
6943
  const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
6536
6944
  const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
6945
+ const BREAKDOWN_TOP_N = 10;
6946
+ const computeBreakdown = (diagnostics) => {
6947
+ const byRule = /* @__PURE__ */ new Map();
6948
+ for (const d of diagnostics) {
6949
+ const row = byRule.get(d.rule) ?? {
6950
+ rule: d.rule,
6951
+ errors: 0,
6952
+ warnings: 0,
6953
+ info: 0,
6954
+ fixable: 0
6955
+ };
6956
+ if (d.severity === "error") row.errors++;
6957
+ else if (d.severity === "warning") row.warnings++;
6958
+ else row.info++;
6959
+ if (d.fixable) row.fixable++;
6960
+ byRule.set(d.rule, row);
6961
+ }
6962
+ const sorted = [...byRule.values()].sort((a, b) => {
6963
+ const aTotal = a.errors + a.warnings + a.info;
6964
+ const bTotal = b.errors + b.warnings + b.info;
6965
+ if (aTotal !== bTotal) return bTotal - aTotal;
6966
+ if (a.errors !== b.errors) return b.errors - a.errors;
6967
+ return a.rule.localeCompare(b.rule);
6968
+ });
6969
+ const rows = sorted.slice(0, BREAKDOWN_TOP_N);
6970
+ const hidden = sorted.slice(BREAKDOWN_TOP_N);
6971
+ return {
6972
+ rows,
6973
+ hiddenRules: hidden.length,
6974
+ hiddenErrors: hidden.reduce((acc, r) => acc + r.errors, 0),
6975
+ hiddenWarnings: hidden.reduce((acc, r) => acc + r.warnings, 0)
6976
+ };
6977
+ };
6537
6978
  const buildScanRender = (input) => {
6538
6979
  const deps = {
6539
6980
  theme: createTheme(),
@@ -6583,6 +7024,7 @@ const buildScanRender = (input) => {
6583
7024
  engines: input.results.length,
6584
7025
  elapsedMs: input.elapsedMs,
6585
7026
  nextSteps,
7027
+ breakdown: computeBreakdown(input.diagnostics),
6586
7028
  thresholds: input.thresholds
6587
7029
  }, deps)}`;
6588
7030
  };
@@ -6695,7 +7137,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
6695
7137
  engineTimings
6696
7138
  };
6697
7139
  if (options.json) {
6698
- const { buildJsonOutput } = await import("./json-DZfGz2xa.js");
7140
+ const { buildJsonOutput } = await import("./json-DZHn6AE3.js");
6699
7141
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
6700
7142
  console.log(JSON.stringify(jsonOut, null, 2));
6701
7143
  return completion;
@@ -8306,10 +8748,18 @@ const promptForConfigChoices = async () => {
8306
8748
  return {
8307
8749
  engines: enginesSelection,
8308
8750
  failBelow: Number(failBelowRaw),
8751
+ typecheck: DEFAULT_CONFIG.lint.typecheck,
8309
8752
  telemetryEnabled: telemetryChoice === "enabled",
8310
8753
  writeGithubWorkflow: workflowChoice === "yes"
8311
8754
  };
8312
8755
  };
8756
+ const strictChoices = () => ({
8757
+ engines: Object.keys(DEFAULT_CONFIG.engines),
8758
+ failBelow: 85,
8759
+ typecheck: true,
8760
+ telemetryEnabled: DEFAULT_CONFIG.telemetry.enabled,
8761
+ writeGithubWorkflow: true
8762
+ });
8313
8763
  const writeAislopConfig = (configDir, configPath, choices) => {
8314
8764
  const selected = new Set(choices.engines);
8315
8765
  const engines = {
@@ -8324,6 +8774,7 @@ const writeAislopConfig = (configDir, configPath, choices) => {
8324
8774
  version: DEFAULT_CONFIG.version,
8325
8775
  engines,
8326
8776
  quality: { ...DEFAULT_CONFIG.quality },
8777
+ lint: { typecheck: choices.typecheck },
8327
8778
  security: { ...DEFAULT_CONFIG.security },
8328
8779
  scoring: {
8329
8780
  weights: { ...DEFAULT_CONFIG.scoring.weights },
@@ -8376,7 +8827,7 @@ const initCommand = async (directory, options = {}) => {
8376
8827
  return;
8377
8828
  }
8378
8829
  }
8379
- const choices = await promptForConfigChoices();
8830
+ const choices = options.strict ? strictChoices() : await promptForConfigChoices();
8380
8831
  if (!choices) return;
8381
8832
  writeAislopConfig(configDir, configPath, choices);
8382
8833
  const steps = [{