aislop 0.8.3 → 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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-BynHxO1X.js";
1
+ import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-CBcgcofs.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";
@@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url";
15
15
  import { performance } from "node:perf_hooks";
16
16
  import os from "node:os";
17
17
  import ts from "typescript";
18
+ import { randomUUID } from "node:crypto";
18
19
  import { isCancel, multiselect, select, text } from "@clack/prompts";
19
20
 
20
21
  //#region src/config/defaults.ts
@@ -27,6 +28,7 @@ const DEFAULT_CONFIG = {
27
28
  "build",
28
29
  "coverage"
29
30
  ],
31
+ include: [],
30
32
  engines: {
31
33
  format: true,
32
34
  lint: true,
@@ -62,7 +64,7 @@ const DEFAULT_CONFIG = {
62
64
  smoothing: 20
63
65
  },
64
66
  ci: {
65
- failBelow: 0,
67
+ failBelow: 70,
66
68
  format: "json"
67
69
  },
68
70
  telemetry: { enabled: true }
@@ -188,7 +190,7 @@ const ScoringSchema = z.object({
188
190
  smoothing: z.number().nonnegative().default(20)
189
191
  });
190
192
  const CiSchema = z.object({
191
- failBelow: z.number().default(0),
193
+ failBelow: z.number().default(70),
192
194
  format: z.enum(["json"]).default("json")
193
195
  });
194
196
  const TelemetrySchema = z.object({ enabled: z.boolean().default(true) });
@@ -222,7 +224,7 @@ const AislopConfigSchema = z.object({
222
224
  smoothing: 20
223
225
  })),
224
226
  ci: CiSchema.default(() => ({
225
- failBelow: 0,
227
+ failBelow: 70,
226
228
  format: "json"
227
229
  })),
228
230
  telemetry: TelemetrySchema.default(() => ({ enabled: true })),
@@ -232,7 +234,8 @@ const AislopConfigSchema = z.object({
232
234
  "dist",
233
235
  "build",
234
236
  "coverage"
235
- ])
237
+ ]),
238
+ include: z.array(z.string()).default(() => [])
236
239
  });
237
240
  const defaults = AislopConfigSchema.parse({});
238
241
  /**
@@ -560,31 +563,68 @@ const EXCLUDED_DIRS = [
560
563
  "dist",
561
564
  "build",
562
565
  ".git",
566
+ ".agents",
563
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",
564
584
  "tests",
565
585
  "test",
566
586
  "__tests__",
567
587
  "__test__",
568
588
  "spec",
569
589
  "__mocks__",
570
- "fixtures",
571
590
  "test_data",
572
591
  ".next",
573
592
  ".nuxt",
574
593
  "coverage",
575
- ".turbo"
594
+ ".turbo",
595
+ "public"
576
596
  ];
577
597
  const FIND_PRUNE_DIRS = [
578
598
  "node_modules",
579
599
  "dist",
580
600
  "build",
581
601
  ".git",
602
+ ".agents",
582
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",
583
620
  ".next",
584
621
  ".nuxt",
585
622
  "coverage",
586
- ".turbo"
623
+ ".turbo",
624
+ "public"
587
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));
588
628
  const TEST_FILE_PATTERNS = [
589
629
  /(?:^|\/).*\.test\.[^/]+$/i,
590
630
  /(?:^|\/).*\.spec\.[^/]+$/i,
@@ -609,6 +649,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
609
649
  return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
610
650
  };
611
651
  const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
652
+ const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
612
653
  const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
613
654
  const getIgnoredPaths = (rootDirectory, files) => {
614
655
  if (files.length === 0) return /* @__PURE__ */ new Set();
@@ -667,7 +708,7 @@ const normalizeExcludePatterns = (patterns) => {
667
708
  return [p];
668
709
  });
669
710
  };
670
- const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = []) => {
711
+ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = [], include = []) => {
671
712
  const extraSet = new Set(extraExtensions);
672
713
  const normalizedFiles = files.map((file) => {
673
714
  const absolutePath = path.isAbsolute(file) ? file : path.resolve(rootDirectory, file);
@@ -682,8 +723,16 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
682
723
  if (!normalizedExcludePatterns.length) return false;
683
724
  return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
684
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
+ };
685
731
  return normalizedFiles.filter(({ absolutePath, relativePath }) => {
686
- 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) || ignoredPaths.has(relativePath)) return false;
733
+ if (!isUserIncluded(relativePath)) return false;
734
+ if (isUserExcluded(relativePath)) return false;
735
+ return hasAllowedExtension(relativePath, extraSet);
687
736
  }).map(({ absolutePath }) => absolutePath);
688
737
  };
689
738
  const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
@@ -1868,6 +1917,86 @@ const PYTHON_IMPORT_TO_PIP = {
1868
1917
  redis: "redis"
1869
1918
  };
1870
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
+
1871
2000
  //#endregion
1872
2001
  //#region src/engines/ai-slop/hallucinated-imports.ts
1873
2002
  const JS_EXTENSIONS$2 = new Set([
@@ -2004,10 +2133,26 @@ const buildAliasMatcher = (key) => {
2004
2133
  };
2005
2134
  const collectAliasMatchersFromConfig = (configPath, matchers) => {
2006
2135
  const opts = readJson(configPath)?.compilerOptions;
2007
- if (!opts || typeof opts !== "object") return;
2136
+ if (!opts) return;
2008
2137
  const paths = opts.paths;
2009
- if (!paths || typeof paths !== "object") return;
2010
- 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
+ }
2011
2156
  };
2012
2157
  const collectTsPathAliases = (rootDir) => {
2013
2158
  const matchers = [];
@@ -2015,97 +2160,35 @@ const collectTsPathAliases = (rootDir) => {
2015
2160
  for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
2016
2161
  return matchers;
2017
2162
  };
2018
- const addPyDep = (pyDeps, name) => {
2019
- const normalized = name.toLowerCase().replace(/_/g, "-");
2020
- pyDeps.add(normalized);
2021
- };
2022
- const collectFromRequirementsTxt = (rootDir, pyDeps) => {
2023
- const reqPath = path.join(rootDir, "requirements.txt");
2024
- if (!fs.existsSync(reqPath)) return false;
2025
- try {
2026
- const content = fs.readFileSync(reqPath, "utf-8");
2027
- for (const line of content.split("\n")) {
2028
- const trimmed = line.trim();
2029
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
2030
- const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
2031
- if (match) addPyDep(pyDeps, match[1]);
2032
- }
2033
- return true;
2034
- } catch {
2035
- return false;
2036
- }
2037
- };
2038
- const collectFromPyproject = (rootDir, pyDeps) => {
2039
- const pyprojPath = path.join(rootDir, "pyproject.toml");
2040
- if (!fs.existsSync(pyprojPath)) return false;
2041
- try {
2042
- const content = fs.readFileSync(pyprojPath, "utf-8");
2043
- const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
2044
- if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
2045
- const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
2046
- if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
2047
- const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
2048
- if (pep621) for (const line of pep621[1].split("\n")) {
2049
- const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
2050
- if (m) addPyDep(pyDeps, m[1]);
2051
- }
2052
- const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
2053
- let match = poetryRe.exec(content);
2054
- while (match !== null) {
2055
- for (const line of match[1].split("\n")) {
2056
- const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
2057
- if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
2058
- }
2059
- match = poetryRe.exec(content);
2060
- }
2061
- return true;
2062
- } catch {
2063
- return false;
2064
- }
2065
- };
2066
- const collectFromPipfile = (rootDir, pyDeps) => {
2067
- const pipfilePath = path.join(rootDir, "Pipfile");
2068
- if (!fs.existsSync(pipfilePath)) return false;
2069
- try {
2070
- const content = fs.readFileSync(pipfilePath, "utf-8");
2071
- const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
2072
- let match = sectionRe.exec(content);
2073
- while (match !== null) {
2074
- for (const line of match[2].split("\n")) {
2075
- const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
2076
- if (m) addPyDep(pyDeps, m[1]);
2077
- }
2078
- match = sectionRe.exec(content);
2079
- }
2080
- return true;
2081
- } catch {
2082
- return false;
2083
- }
2084
- };
2085
2163
  const loadManifest = (rootDir) => {
2086
2164
  const jsDeps = /* @__PURE__ */ new Set();
2087
- const pyDeps = /* @__PURE__ */ new Set();
2088
2165
  const hasJsManifest = collectJsDeps(rootDir, jsDeps);
2089
- const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
2090
- const hasPyproject = collectFromPyproject(rootDir, pyDeps);
2091
- const hasPipfile = collectFromPipfile(rootDir, pyDeps);
2166
+ const { pyDeps, hasPyManifest } = collectPythonDeps(rootDir);
2092
2167
  return {
2093
2168
  jsDeps,
2094
2169
  pyDeps,
2095
2170
  hasJsManifest,
2096
- hasPyManifest: hasReq || hasPyproject || hasPipfile
2171
+ hasPyManifest
2097
2172
  };
2098
2173
  };
2099
2174
  const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
2175
+ const RUNTIME_BUILTINS = new Set(["bun"]);
2100
2176
  const isJsBuiltin = (spec) => {
2177
+ if (RUNTIME_BUILTINS.has(spec)) return true;
2101
2178
  return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
2102
2179
  };
2103
2180
  const VIRTUAL_MODULE_PREFIXES = [
2104
2181
  "astro:",
2105
2182
  "virtual:",
2106
- "bun:"
2183
+ "bun:",
2184
+ "~icons/"
2107
2185
  ];
2108
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" };
2109
2192
  const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
2110
2193
  const isLikelyRealImportSpec = (spec) => {
2111
2194
  if (spec.length === 0) return false;
@@ -2172,10 +2255,14 @@ const extractPyImports = (content) => {
2172
2255
  }
2173
2256
  return results;
2174
2257
  };
2175
- const checkJsImport = (spec, manifest, tsAliasMatchers) => {
2258
+ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
2259
+ const spec = stripImportQuery(rawSpec);
2260
+ if (spec.length === 0) return null;
2176
2261
  if (isJsRelativeOrAbsolute(spec)) return null;
2177
2262
  if (isJsBuiltin(spec)) return null;
2178
2263
  if (isJsVirtualModule(spec)) return null;
2264
+ const virtualOwner = VIRTUAL_ASSET_FILES[spec];
2265
+ if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
2179
2266
  if (tsAliasMatchers.some((m) => m(spec))) return null;
2180
2267
  const pkg = packageNameFromImport(spec);
2181
2268
  if (manifest.jsDeps.has(pkg)) return null;
@@ -3519,64 +3606,88 @@ const analyzeFunctions = (content, ext) => {
3519
3606
  }
3520
3607
  return functions;
3521
3608
  };
3522
- 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
+ };
3523
3621
  const checkFileDiagnostics = (relativePath, content, limits) => {
3524
3622
  const results = [];
3525
3623
  const lineCount = content.split("\n").length;
3526
3624
  const ext = path.extname(relativePath).toLowerCase();
3527
3625
  if (isDataFile(content)) return results;
3528
- 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;
3529
3628
  if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
3530
3629
  filePath: relativePath,
3531
3630
  engine: "code-quality",
3532
3631
  rule: "complexity/file-too-large",
3533
3632
  severity: "warning",
3534
- message: `File has ${lineCount} lines (max: ${configuredMax})`,
3633
+ message: `File too large (max: ${configuredMax})`,
3535
3634
  help: "Consider splitting this file into smaller modules",
3536
3635
  line: 0,
3537
3636
  column: 0,
3538
3637
  category: "Complexity",
3539
- fixable: false
3638
+ fixable: false,
3639
+ detail: `${lineCount} lines`
3540
3640
  });
3541
3641
  return results;
3542
3642
  };
3543
- 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) => {
3544
3651
  const results = [];
3545
- 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({
3546
3654
  filePath: relativePath,
3547
3655
  engine: "code-quality",
3548
3656
  rule: "complexity/function-too-long",
3549
3657
  severity: "warning",
3550
- message: `Function '${fn.name}' has ${fn.lineCount} lines (max: ${limits.maxFunctionLoc})`,
3658
+ message: `Function too long (max: ${fnMax})`,
3551
3659
  help: "Consider breaking this function into smaller pieces",
3552
3660
  line: fn.startLine,
3553
3661
  column: 0,
3554
3662
  category: "Complexity",
3555
- fixable: false
3663
+ fixable: false,
3664
+ detail: `${fn.name} · ${fn.lineCount} lines`
3556
3665
  });
3557
3666
  if (fn.maxNesting > limits.maxNesting) results.push({
3558
3667
  filePath: relativePath,
3559
3668
  engine: "code-quality",
3560
3669
  rule: "complexity/deep-nesting",
3561
3670
  severity: "warning",
3562
- message: `Function '${fn.name}' has nesting depth ${fn.maxNesting} (max: ${limits.maxNesting})`,
3671
+ message: `Function nested too deeply (max: ${limits.maxNesting})`,
3563
3672
  help: "Consider using early returns or extracting nested logic",
3564
3673
  line: fn.startLine,
3565
3674
  column: 0,
3566
3675
  category: "Complexity",
3567
- fixable: false
3676
+ fixable: false,
3677
+ detail: `${fn.name} · depth ${fn.maxNesting}`
3568
3678
  });
3569
3679
  if (fn.paramCount > limits.maxParams) results.push({
3570
3680
  filePath: relativePath,
3571
3681
  engine: "code-quality",
3572
3682
  rule: "complexity/too-many-params",
3573
3683
  severity: "warning",
3574
- message: `Function '${fn.name}' has ${fn.paramCount} parameters (max: ${limits.maxParams})`,
3684
+ message: `Function has too many parameters (max: ${limits.maxParams})`,
3575
3685
  help: "Consider using an options object parameter",
3576
3686
  line: fn.startLine,
3577
3687
  column: 0,
3578
3688
  category: "Complexity",
3579
- fixable: false
3689
+ fixable: false,
3690
+ detail: `${fn.name} · ${fn.paramCount} params`
3580
3691
  });
3581
3692
  return results;
3582
3693
  };
@@ -3591,7 +3702,7 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
3591
3702
  }
3592
3703
  const ext = path.extname(filePath).toLowerCase();
3593
3704
  const diagnostics = checkFileDiagnostics(relativePath, content, limits);
3594
- 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));
3595
3706
  return diagnostics;
3596
3707
  };
3597
3708
  const checkComplexity = async (context) => {
@@ -3696,17 +3807,19 @@ const findDuplicateBlocks = (content, relativePath) => {
3696
3807
  });
3697
3808
  }
3698
3809
  return reports.map((r) => {
3810
+ const span = r.currentEnd - r.currentStart + 1;
3699
3811
  return {
3700
3812
  filePath: relativePath,
3701
3813
  engine: "code-quality",
3702
3814
  rule: "code-quality/duplicate-block",
3703
3815
  severity: "warning",
3704
- 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",
3705
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.`,
3706
3818
  line: r.currentStart,
3707
3819
  column: 0,
3708
3820
  category: "Complexity",
3709
- fixable: false
3821
+ fixable: false,
3822
+ detail: `${span} lines duplicate block at L${r.priorStart}`
3710
3823
  };
3711
3824
  });
3712
3825
  };
@@ -4283,16 +4396,34 @@ const fixGofmt = async (rootDirectory) => {
4283
4396
  if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `gofmt exited with code ${result.exitCode}`);
4284
4397
  };
4285
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
+
4286
4415
  //#endregion
4287
4416
  //#region src/engines/format/ruff-format.ts
4288
4417
  const runRuffFormat = async (context) => {
4289
4418
  const ruffBinary = resolveToolBinary("ruff");
4419
+ const targets = getPythonTargets(context);
4420
+ if (targets.length === 0) return [];
4290
4421
  try {
4291
4422
  const result = await runSubprocess(ruffBinary, [
4292
4423
  "format",
4293
4424
  "--check",
4294
4425
  "--diff",
4295
- context.rootDirectory
4426
+ ...targets
4296
4427
  ], {
4297
4428
  cwd: context.rootDirectory,
4298
4429
  timeout: 6e4
@@ -4308,9 +4439,9 @@ const parseRuffFormatOutput = (output, rootDir) => {
4308
4439
  const filePattern = /^--- (.+)$/gm;
4309
4440
  let match;
4310
4441
  while ((match = filePattern.exec(output)) !== null) {
4311
- const filePath = match[1].replace(/^a\//, "");
4442
+ const filePath = getRuffDiagnosticPath(rootDir, match[1]);
4312
4443
  diagnostics.push({
4313
- filePath: path.relative(rootDir, filePath),
4444
+ filePath,
4314
4445
  engine: "format",
4315
4446
  rule: "python-formatting",
4316
4447
  severity: "warning",
@@ -4714,6 +4845,95 @@ const resolveOxlintBinary = () => {
4714
4845
  return "oxlint";
4715
4846
  }
4716
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
+ };
4717
4937
  const parseRuleCode = (code) => {
4718
4938
  if (!code) return {
4719
4939
  plugin: "eslint",
@@ -4810,6 +5030,8 @@ const runOxlint = async (context) => {
4810
5030
  framework: context.frameworks.find((f) => f !== "none"),
4811
5031
  testFramework: detectTestFramework(context.rootDirectory)
4812
5032
  });
5033
+ const ambientSources = detectAmbientSources(context.rootDirectory);
5034
+ sstReferencedFiles.clear();
4813
5035
  try {
4814
5036
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
4815
5037
  const args = [
@@ -4849,6 +5071,11 @@ const runOxlint = async (context) => {
4849
5071
  fixable: false
4850
5072
  };
4851
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;
4852
5079
  const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
4853
5080
  if (seen.has(key)) return false;
4854
5081
  seen.add(key);
@@ -4912,18 +5139,20 @@ const fixOxlint = async (context, options = {}) => {
4912
5139
  //#region src/engines/lint/ruff.ts
4913
5140
  const runRuffLint = async (context) => {
4914
5141
  const ruffBinary = resolveToolBinary("ruff");
5142
+ const targets = getPythonTargets(context);
5143
+ if (targets.length === 0) return [];
4915
5144
  try {
4916
5145
  const output = (await runSubprocess(ruffBinary, [
4917
5146
  "check",
4918
5147
  "--output-format=json",
4919
- context.rootDirectory
5148
+ ...targets
4920
5149
  ], {
4921
5150
  cwd: context.rootDirectory,
4922
5151
  timeout: 6e4
4923
5152
  })).stdout;
4924
5153
  if (!output) return [];
4925
5154
  return JSON.parse(output).map((d) => ({
4926
- filePath: path.relative(context.rootDirectory, d.filename),
5155
+ filePath: getRuffDiagnosticPath(context.rootDirectory, d.filename),
4927
5156
  engine: "lint",
4928
5157
  rule: `ruff/${d.code}`,
4929
5158
  severity: d.code.startsWith("E") || d.code.startsWith("F") ? "error" : "warning",
@@ -5037,56 +5266,94 @@ const runPnpmAuditWithFallback = async (rootDir, timeout) => {
5037
5266
  return [];
5038
5267
  }
5039
5268
  };
5269
+ const SEVERITY_RANK = {
5270
+ critical: 4,
5271
+ high: 3,
5272
+ moderate: 2,
5273
+ low: 1
5274
+ };
5040
5275
  const toSeverity = (value) => value === "critical" || value === "high" ? "error" : "warning";
5041
- 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
+ };
5042
5336
  const parseLegacyAdvisories = (advisories, source) => {
5043
- const diagnostics = [];
5044
- for (const [key, advisory] of Object.entries(advisories)) {
5045
- const packageName = advisory.module_name ?? advisory.name ?? advisory.package ?? key;
5046
- const severity = (advisory.severity ?? "moderate").toLowerCase();
5047
- const recommendation = advisory.recommendation ?? advisory.title ?? `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
5048
- diagnostics.push({
5049
- filePath: "package.json",
5050
- engine: "security",
5051
- rule: "security/vulnerable-dependency",
5052
- severity: toSeverity(severity),
5053
- message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
5054
- help: withFixHint(recommendation),
5055
- line: 0,
5056
- column: 0,
5057
- category: "Security",
5058
- fixable: false
5059
- });
5060
- }
5061
- 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));
5062
5340
  };
5063
5341
  const parseModernVulnerabilities = (vulnerabilities, source) => {
5064
- const diagnostics = [];
5342
+ const bucket = /* @__PURE__ */ new Map();
5065
5343
  for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
5066
5344
  const severity = (vulnerability.severity ?? "moderate").toLowerCase();
5067
5345
  const fixAvailable = vulnerability.fixAvailable;
5068
5346
  const isDirect = vulnerability.isDirect === true;
5069
- let recommendation = `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
5070
- 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";
5071
- 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";
5072
5350
  else if (fixAvailable && typeof fixAvailable === "object" && "name" in fixAvailable && "version" in fixAvailable) {
5073
5351
  const target = fixAvailable;
5074
- 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}`;
5075
5353
  }
5076
- diagnostics.push({
5077
- filePath: "package.json",
5078
- engine: "security",
5079
- rule: "security/vulnerable-dependency",
5080
- severity: toSeverity(severity),
5081
- message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
5082
- help: withFixHint(recommendation),
5083
- line: 0,
5084
- column: 0,
5085
- category: "Security",
5086
- fixable: false
5087
- });
5354
+ upsertVuln(bucket, packageName, severity, recommendation);
5088
5355
  }
5089
- return diagnostics;
5356
+ return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
5090
5357
  };
5091
5358
  const parseJsAudit = (output, source) => {
5092
5359
  if (!output) return [];
@@ -5874,60 +6141,348 @@ var LiveRail = class {
5874
6141
  };
5875
6142
 
5876
6143
  //#endregion
5877
- //#region src/utils/telemetry.ts
5878
- const POSTHOG_HOST = "https://eu.i.posthog.com";
5879
- const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
5880
- /**
5881
- * Returns true if telemetry should be disabled.
5882
- * Telemetry is opt-out: it runs unless explicitly disabled.
5883
- */
5884
- const isTelemetryDisabled = (configEnabled) => {
5885
- if (process.env.AISLOP_NO_TELEMETRY === "1" || process.env.DO_NOT_TRACK === "1") return true;
5886
- if (process.env.CI === "true" || process.env.CI === "1") return true;
5887
- if (configEnabled === false) return true;
5888
- return false;
5889
- };
5890
- const getScoreBucket = (score) => {
6144
+ //#region src/telemetry/env.ts
6145
+ const detectPackageManager$1 = (env = process.env) => {
6146
+ const execPath = env.npm_execpath ?? "";
6147
+ if (execPath.includes("npx")) return "npx";
6148
+ const userAgent = env.npm_config_user_agent ?? "";
6149
+ if (userAgent.startsWith("pnpm/")) return "pnpm";
6150
+ if (userAgent.startsWith("yarn/")) return "yarn";
6151
+ if (userAgent.startsWith("bun/")) return "bun";
6152
+ if (userAgent.startsWith("npm/")) return "npm";
6153
+ if (execPath.includes("pnpm")) return "pnpm";
6154
+ if (execPath.includes("yarn")) return "yarn";
6155
+ if (execPath.includes("bun")) return "bun";
6156
+ if (execPath.includes("npm")) return "npm";
6157
+ return "unknown";
6158
+ };
6159
+ const CI_ENV_KEYS = [
6160
+ "CI",
6161
+ "GITHUB_ACTIONS",
6162
+ "GITLAB_CI",
6163
+ "CIRCLECI",
6164
+ "TRAVIS",
6165
+ "BUILDKITE",
6166
+ "DRONE",
6167
+ "TEAMCITY_VERSION",
6168
+ "TF_BUILD"
6169
+ ];
6170
+ const isCiEnv = (env = process.env) => CI_ENV_KEYS.some((k) => {
6171
+ const v = env[k];
6172
+ return v === "true" || v === "1" || v != null && v.length > 0 && k !== "CI";
6173
+ }) || env.CI === "true" || env.CI === "1";
6174
+ const fileCountBucket = (count) => {
6175
+ if (count < 10) return "0-10";
6176
+ if (count < 50) return "10-50";
6177
+ if (count < 100) return "50-100";
6178
+ if (count < 500) return "100-500";
6179
+ if (count < 1e3) return "500-1000";
6180
+ return "1000+";
6181
+ };
6182
+ const scoreBucket = (score) => {
5891
6183
  if (score >= 75) return "75-100";
5892
6184
  if (score >= 50) return "50-75";
5893
6185
  if (score >= 25) return "25-50";
5894
6186
  return "0-25";
5895
6187
  };
5896
- const getAnonymousId = () => {
5897
- const raw = `${os.hostname()}-${os.platform()}-${os.arch()}`;
5898
- let hash = 5381;
5899
- for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
5900
- return `aislop_${(hash >>> 0).toString(36)}`;
6188
+
6189
+ //#endregion
6190
+ //#region src/telemetry/identity.ts
6191
+ const FILE_BASENAME = "install_id";
6192
+ const resolveInstallIdPath = (homedir = os.homedir(), env = process.env) => {
6193
+ if (process.platform === "linux" && env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "aislop", FILE_BASENAME);
6194
+ return path.join(homedir, ".aislop", FILE_BASENAME);
6195
+ };
6196
+ const ensureInstallId = (idPath = resolveInstallIdPath()) => {
6197
+ if (fs.existsSync(idPath)) {
6198
+ const existing = fs.readFileSync(idPath, "utf-8").trim();
6199
+ if (existing.length > 0) return {
6200
+ installId: existing,
6201
+ created: false
6202
+ };
6203
+ }
6204
+ const dir = path.dirname(idPath);
6205
+ fs.mkdirSync(dir, { recursive: true });
6206
+ const installId = randomUUID();
6207
+ const tmpPath = `${idPath}.${process.pid}.tmp`;
6208
+ fs.writeFileSync(tmpPath, `${installId}\n`, { mode: 384 });
6209
+ try {
6210
+ fs.renameSync(tmpPath, idPath);
6211
+ return {
6212
+ installId,
6213
+ created: true
6214
+ };
6215
+ } catch {
6216
+ fs.rmSync(tmpPath, { force: true });
6217
+ return {
6218
+ installId: fs.readFileSync(idPath, "utf-8").trim(),
6219
+ created: false
6220
+ };
6221
+ }
6222
+ };
6223
+
6224
+ //#endregion
6225
+ //#region src/telemetry/redaction.ts
6226
+ const SAFE_PROPERTY_NAMES = new Set([
6227
+ "aislop_version",
6228
+ "node_version",
6229
+ "os",
6230
+ "arch",
6231
+ "schema_version",
6232
+ "anonymous_install_id",
6233
+ "package_manager",
6234
+ "is_ci",
6235
+ "command",
6236
+ "language_summary",
6237
+ "lang_typescript",
6238
+ "lang_javascript",
6239
+ "lang_python",
6240
+ "lang_java",
6241
+ "file_count_bucket",
6242
+ "exit_code",
6243
+ "duration_ms",
6244
+ "error_kind",
6245
+ "score",
6246
+ "score_bucket",
6247
+ "finding_count",
6248
+ "error_count",
6249
+ "warning_count",
6250
+ "fixable_count",
6251
+ "fix_steps",
6252
+ "fix_resolved",
6253
+ "fix_score_delta",
6254
+ "engine_format_issues",
6255
+ "engine_format_ms",
6256
+ "engine_lint_issues",
6257
+ "engine_lint_ms",
6258
+ "engine_code_quality_issues",
6259
+ "engine_code_quality_ms",
6260
+ "engine_ai_slop_issues",
6261
+ "engine_ai_slop_ms",
6262
+ "engine_architecture_issues",
6263
+ "engine_architecture_ms",
6264
+ "engine_security_issues",
6265
+ "engine_security_ms",
6266
+ "tool",
6267
+ "ok",
6268
+ "agent",
6269
+ "score_delta"
6270
+ ]);
6271
+ const redactProperties = (props) => {
6272
+ const clean = {};
6273
+ const dropped = [];
6274
+ for (const [key, value] of Object.entries(props)) {
6275
+ if (value === void 0) continue;
6276
+ if (SAFE_PROPERTY_NAMES.has(key)) clean[key] = value;
6277
+ else dropped.push(key);
6278
+ }
6279
+ return {
6280
+ clean,
6281
+ dropped
6282
+ };
6283
+ };
6284
+
6285
+ //#endregion
6286
+ //#region src/telemetry/client.ts
6287
+ const POSTHOG_HOST = "https://eu.i.posthog.com";
6288
+ const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
6289
+ const SCHEMA_VERSION = "v2";
6290
+ const REQUEST_TIMEOUT_MS = 3e3;
6291
+ const isTelemetryDisabled = (config) => {
6292
+ const env = process.env;
6293
+ if (env.AISLOP_NO_TELEMETRY === "1" || env.DO_NOT_TRACK === "1") return true;
6294
+ if (config?.enabled === false) return true;
6295
+ if (config?.enabled === true) return false;
6296
+ if (env.CI === "true" || env.CI === "1") return true;
6297
+ return false;
5901
6298
  };
5902
- /** Pending telemetry request kept alive so Node doesn't exit before it completes. */
5903
- let pendingRequest = null;
5904
- const trackEvent = (event) => {
6299
+ const isDebug = () => process.env.AISLOP_TELEMETRY_DEBUG === "1";
6300
+ const pendingRequests = /* @__PURE__ */ new Set();
6301
+ let cachedInstallId = null;
6302
+ let installCreated = false;
6303
+ const baseProperties = (installId) => ({
6304
+ aislop_version: APP_VERSION,
6305
+ node_version: process.version,
6306
+ os: os.platform(),
6307
+ arch: os.arch(),
6308
+ schema_version: SCHEMA_VERSION,
6309
+ anonymous_install_id: installId,
6310
+ package_manager: detectPackageManager$1(),
6311
+ is_ci: isCiEnv()
6312
+ });
6313
+ const track = (input) => {
6314
+ if (isTelemetryDisabled(input.config)) return { installCreated: false };
6315
+ if (cachedInstallId == null) {
6316
+ const ensured = ensureInstallId(resolveInstallIdPath());
6317
+ cachedInstallId = ensured.installId;
6318
+ installCreated = ensured.created;
6319
+ }
6320
+ const { clean, dropped } = redactProperties({
6321
+ ...baseProperties(cachedInstallId),
6322
+ ...input.properties
6323
+ });
6324
+ if (isDebug()) {
6325
+ const compact = JSON.stringify({
6326
+ event: input.event,
6327
+ properties: clean
6328
+ });
6329
+ process.stderr.write(`[telemetry] ${compact}\n`);
6330
+ if (dropped.length > 0) for (const key of dropped) process.stderr.write(`[telemetry] dropped non-allowlisted property: ${key}\n`);
6331
+ }
6332
+ if (process.env.AISLOP_TELEMETRY_DRY_RUN === "1") return { installCreated };
5905
6333
  const payload = {
5906
6334
  api_key: POSTHOG_KEY,
5907
- event: `cli_${event.command}`,
5908
- distinct_id: getAnonymousId(),
5909
- properties: {
5910
- version: APP_VERSION,
5911
- node_version: process.version,
5912
- os: os.platform(),
5913
- arch: os.arch(),
5914
- languages: event.languages,
5915
- score_bucket: event.scoreBucket,
5916
- engine_issues: event.engineIssues,
5917
- engine_timings: event.engineTimings,
5918
- elapsed_ms: event.elapsedMs,
5919
- file_count: event.fileCount,
5920
- fix_steps: event.fixSteps,
5921
- fix_resolved: event.fixResolved
5922
- },
6335
+ event: input.event,
6336
+ distinct_id: cachedInstallId,
6337
+ properties: clean,
5923
6338
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5924
6339
  };
5925
- pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
6340
+ const request = fetch(`${POSTHOG_HOST}/capture/`, {
5926
6341
  method: "POST",
5927
6342
  headers: { "Content-Type": "application/json" },
5928
6343
  body: JSON.stringify(payload),
5929
- signal: AbortSignal.timeout(3e3)
5930
- }).then(() => {}).catch(() => {});
6344
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
6345
+ }).then(() => {}).catch(() => {}).finally(() => {
6346
+ pendingRequests.delete(request);
6347
+ });
6348
+ pendingRequests.add(request);
6349
+ return { installCreated };
6350
+ };
6351
+ const flushTelemetry = async () => {
6352
+ if (pendingRequests.size === 0) return;
6353
+ await Promise.all(pendingRequests);
6354
+ };
6355
+
6356
+ //#endregion
6357
+ //#region src/telemetry/language.ts
6358
+ const ALL_LANGUAGES = [
6359
+ "typescript",
6360
+ "javascript",
6361
+ "python",
6362
+ "java"
6363
+ ];
6364
+ const buildLanguageProperties = (detected) => {
6365
+ const present = new Set(detected);
6366
+ const summary = [...present].filter((l) => ALL_LANGUAGES.includes(l));
6367
+ summary.sort();
6368
+ return {
6369
+ language_summary: summary.join(","),
6370
+ lang_typescript: present.has("typescript"),
6371
+ lang_javascript: present.has("javascript"),
6372
+ lang_python: present.has("python"),
6373
+ lang_java: present.has("java")
6374
+ };
6375
+ };
6376
+
6377
+ //#endregion
6378
+ //#region src/telemetry/events.ts
6379
+ const buildCommandStartedProps = (input) => {
6380
+ const props = { command: input.command };
6381
+ if (input.languages) Object.assign(props, buildLanguageProperties(input.languages));
6382
+ if (typeof input.fileCount === "number") props.file_count_bucket = fileCountBucket(input.fileCount);
6383
+ return props;
6384
+ };
6385
+ const ENGINE_KEY_MAP = {
6386
+ format: "engine_format",
6387
+ lint: "engine_lint",
6388
+ "code-quality": "engine_code_quality",
6389
+ "ai-slop": "engine_ai_slop",
6390
+ architecture: "engine_architecture",
6391
+ security: "engine_security"
6392
+ };
6393
+ const flattenEngineStats = (issues, timings) => {
6394
+ const out = {};
6395
+ for (const [engine, count] of Object.entries(issues)) {
6396
+ const key = ENGINE_KEY_MAP[engine];
6397
+ if (key != null && typeof count === "number") out[`${key}_issues`] = count;
6398
+ }
6399
+ for (const [engine, ms] of Object.entries(timings)) {
6400
+ const key = ENGINE_KEY_MAP[engine];
6401
+ if (key != null && typeof ms === "number") out[`${key}_ms`] = Math.round(ms);
6402
+ }
6403
+ return out;
6404
+ };
6405
+ const buildCommandCompletedProps = (input) => {
6406
+ const props = {
6407
+ ...input.startProps,
6408
+ exit_code: input.exitCode,
6409
+ duration_ms: Math.round(input.durationMs)
6410
+ };
6411
+ if (input.errorKind) props.error_kind = input.errorKind;
6412
+ if (typeof input.score === "number") {
6413
+ props.score = input.score;
6414
+ props.score_bucket = scoreBucket(input.score);
6415
+ }
6416
+ if (typeof input.findingCount === "number") props.finding_count = input.findingCount;
6417
+ if (typeof input.errorCount === "number") props.error_count = input.errorCount;
6418
+ if (typeof input.warningCount === "number") props.warning_count = input.warningCount;
6419
+ if (typeof input.fixableCount === "number") props.fixable_count = input.fixableCount;
6420
+ if (input.engineIssues && input.engineTimings) Object.assign(props, flattenEngineStats(input.engineIssues, input.engineTimings));
6421
+ if (typeof input.fixSteps === "number") props.fix_steps = input.fixSteps;
6422
+ if (typeof input.fixResolved === "number") props.fix_resolved = input.fixResolved;
6423
+ if (typeof input.fixScoreDelta === "number") props.fix_score_delta = input.fixScoreDelta;
6424
+ return props;
6425
+ };
6426
+ const errorKindFromException = (error) => {
6427
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
6428
+ if (message.includes("timeout") || message.includes("timed out")) return "timeout";
6429
+ if (message.includes("invalid config") || message.includes("config_invalid")) return "config_invalid";
6430
+ if (message.includes("engine") && message.includes("crash")) return "engine_crash";
6431
+ return "unknown";
6432
+ };
6433
+
6434
+ //#endregion
6435
+ //#region src/telemetry/lifecycle.ts
6436
+ const withCommandLifecycle = async (start, run) => {
6437
+ const startProps = buildCommandStartedProps({
6438
+ command: start.command,
6439
+ languages: start.languages,
6440
+ fileCount: start.fileCount
6441
+ });
6442
+ track({
6443
+ event: "cli_command_started",
6444
+ properties: startProps,
6445
+ config: start.config
6446
+ });
6447
+ const startedAt = performance.now();
6448
+ try {
6449
+ const result = await run();
6450
+ const durationMs = performance.now() - startedAt;
6451
+ track({
6452
+ event: "cli_command_completed",
6453
+ properties: buildCommandCompletedProps({
6454
+ startProps,
6455
+ exitCode: result.exitCode,
6456
+ durationMs,
6457
+ score: result.score,
6458
+ findingCount: result.findingCount,
6459
+ errorCount: result.errorCount,
6460
+ warningCount: result.warningCount,
6461
+ fixableCount: result.fixableCount,
6462
+ engineIssues: result.engineIssues,
6463
+ engineTimings: result.engineTimings,
6464
+ fixSteps: result.fixSteps,
6465
+ fixResolved: result.fixResolved,
6466
+ fixScoreDelta: result.fixScoreDelta
6467
+ }),
6468
+ config: start.config
6469
+ });
6470
+ await flushTelemetry();
6471
+ return result;
6472
+ } catch (error) {
6473
+ track({
6474
+ event: "cli_command_completed",
6475
+ properties: buildCommandCompletedProps({
6476
+ startProps,
6477
+ exitCode: 1,
6478
+ durationMs: performance.now() - startedAt,
6479
+ errorKind: errorKindFromException(error)
6480
+ }),
6481
+ config: start.config
6482
+ });
6483
+ await flushTelemetry();
6484
+ throw error;
6485
+ }
5931
6486
  };
5932
6487
 
5933
6488
  //#endregion
@@ -5977,8 +6532,53 @@ const wrapHelpText = (text, maxWidth, indent) => {
5977
6532
  };
5978
6533
  const terminalWidth = () => {
5979
6534
  const raw = process.stdout.columns;
5980
- if (typeof raw !== "number" || raw <= 0) return 100;
5981
- 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("");
5982
6582
  };
5983
6583
  const renderDiagnostics = (diagnostics, verbose) => {
5984
6584
  const lines = [];
@@ -5987,29 +6587,23 @@ const renderDiagnostics = (diagnostics, verbose) => {
5987
6587
  const label = getEngineLabel(engine);
5988
6588
  lines.push(` ${style(theme, "bold", `${symbols.engineActive} ${label}`)}`);
5989
6589
  const sorted = [...groupBy(engineDiags, (d) => `${d.rule}:${d.message}`).entries()].sort(([, a], [, b]) => {
5990
- 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;
5991
6594
  });
5992
- for (const [, ruleDiags] of sorted) {
6595
+ const maxRules = verbose ? Infinity : 40;
6596
+ for (const [, ruleDiags] of sorted.slice(0, maxRules)) {
5993
6597
  const first = ruleDiags[0];
5994
- const level = toSeverityLabel(first.severity);
5995
- const count = ruleDiags.length > 1 ? ` (${ruleDiags.length})` : "";
5996
- const status = colorBySeverity(level, first.severity);
5997
- const fixableTag = first.fixable ? ` ${style(theme, "muted", "[auto]")}` : "";
5998
- const fixableWidth = first.fixable ? 7 : 0;
5999
- const badgePrefix = ` [${status}]${fixableTag} `;
6000
- const badgePrefixWidth = 5 + level.length + 1 + fixableWidth + 1;
6001
- const wrappedMsg = wrapText(`${first.message}${count}`, terminalWidth(), badgePrefixWidth, " ");
6002
- lines.push(`${badgePrefix}${wrappedMsg[0]}`);
6003
- for (let i = 1; i < wrappedMsg.length; i++) lines.push(wrappedMsg[i]);
6004
- const locations = verbose ? ruleDiags : ruleDiags.slice(0, 3);
6005
- for (const diagnostic of locations) lines.push(style(theme, "muted", ` ${toLocationLabel(diagnostic)}`));
6006
- 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);
6007
6600
  if (first.help) {
6008
6601
  const wrapped = wrapHelpText(first.help, terminalWidth(), " ");
6009
6602
  for (const line of wrapped) lines.push(style(theme, "muted", line));
6010
6603
  }
6011
6604
  lines.push("");
6012
6605
  }
6606
+ if (sorted.length > maxRules) renderHiddenFooter(sorted, maxRules, lines);
6013
6607
  }
6014
6608
  return `${lines.join("\n")}\n`;
6015
6609
  };
@@ -6147,6 +6741,81 @@ var LiveGrid = class {
6147
6741
  }
6148
6742
  };
6149
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
+
6150
6819
  //#endregion
6151
6820
  //#region src/ui/summary.ts
6152
6821
  const elapsed = (ms) => ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
@@ -6173,6 +6842,34 @@ const renderSummary = (input, deps = {}) => {
6173
6842
  ` ${style(t, "muted", `${input.files} files`)} ${sep} ${style(t, "muted", `${input.engines} engines`)} ${sep} ${style(t, "muted", elapsed(input.elapsedMs))}`,
6174
6843
  ""
6175
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
+ }
6176
6873
  if (input.nextSteps.length > 0) {
6177
6874
  for (const step of input.nextSteps) {
6178
6875
  const glyph = step.emphasis === "primary" ? s.hint : s.bullet;
@@ -6245,6 +6942,39 @@ const getStagedFiles = (cwd) => {
6245
6942
  //#region src/commands/scan.ts
6246
6943
  const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
6247
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
+ };
6248
6978
  const buildScanRender = (input) => {
6249
6979
  const deps = {
6250
6980
  theme: createTheme(),
@@ -6294,11 +7024,11 @@ const buildScanRender = (input) => {
6294
7024
  engines: input.results.length,
6295
7025
  elapsedMs: input.elapsedMs,
6296
7026
  nextSteps,
7027
+ breakdown: computeBreakdown(input.diagnostics),
6297
7028
  thresholds: input.thresholds
6298
7029
  }, deps)}`;
6299
7030
  };
6300
7031
  const scanCommand = async (directory, config, options) => {
6301
- const startTime = performance.now();
6302
7032
  const resolvedDir = path.resolve(directory);
6303
7033
  if (!fs.existsSync(resolvedDir)) {
6304
7034
  const msg = `Path does not exist: ${resolvedDir}`;
@@ -6312,9 +7042,18 @@ const scanCommand = async (directory, config, options) => {
6312
7042
  else log.error(msg);
6313
7043
  return { exitCode: 1 };
6314
7044
  }
7045
+ const projectInfo = await discoverProject(resolvedDir);
7046
+ return withCommandLifecycle({
7047
+ command: options.command ?? "scan",
7048
+ config: config.telemetry,
7049
+ languages: projectInfo.languages,
7050
+ fileCount: projectInfo.sourceFileCount
7051
+ }, () => runScanBody(resolvedDir, config, options, projectInfo));
7052
+ };
7053
+ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
7054
+ const startTime = performance.now();
6315
7055
  const showHeader = options.showHeader !== false;
6316
7056
  const useLiveProgress = !options.json && shouldUseSpinner();
6317
- const projectInfo = await discoverProject(resolvedDir);
6318
7057
  let files;
6319
7058
  if (options.staged) {
6320
7059
  files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
@@ -6381,28 +7120,27 @@ const scanCommand = async (directory, config, options) => {
6381
7120
  const elapsedMs = performance.now() - startTime;
6382
7121
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
6383
7122
  const exitCode = allDiagnostics.some((d) => d.severity === "error") || scoreResult.score < config.ci.failBelow ? 1 : 0;
6384
- if (!isTelemetryDisabled(config.telemetry?.enabled)) {
6385
- const engineIssues = {};
6386
- const engineTimings = {};
6387
- for (const r of results) {
6388
- engineIssues[r.engine] = r.diagnostics.length;
6389
- engineTimings[r.engine] = Math.round(r.elapsed);
6390
- }
6391
- trackEvent({
6392
- command: options.command ?? "scan",
6393
- languages: projectInfo.languages,
6394
- scoreBucket: getScoreBucket(scoreResult.score),
6395
- engineIssues,
6396
- engineTimings,
6397
- elapsedMs: Math.round(elapsedMs),
6398
- fileCount: projectInfo.sourceFileCount
6399
- });
6400
- }
7123
+ const engineIssues = {};
7124
+ const engineTimings = {};
7125
+ for (const r of results) {
7126
+ engineIssues[r.engine] = r.diagnostics.length;
7127
+ engineTimings[r.engine] = Math.round(r.elapsed);
7128
+ }
7129
+ const completion = {
7130
+ exitCode,
7131
+ score: scoreResult.score,
7132
+ findingCount: allDiagnostics.length,
7133
+ errorCount: allDiagnostics.filter((d) => d.severity === "error").length,
7134
+ warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
7135
+ fixableCount: allDiagnostics.filter((d) => d.fixable).length,
7136
+ engineIssues,
7137
+ engineTimings
7138
+ };
6401
7139
  if (options.json) {
6402
- const { buildJsonOutput } = await import("./json-D8h2EZW6.js");
7140
+ const { buildJsonOutput } = await import("./json-B_2_Zt7I.js");
6403
7141
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
6404
7142
  console.log(JSON.stringify(jsonOut, null, 2));
6405
- return { exitCode };
7143
+ return completion;
6406
7144
  }
6407
7145
  const projectName = projectInfo.projectName ?? "project";
6408
7146
  const language = projectInfo.languages[0] ?? "unknown";
@@ -6419,7 +7157,7 @@ const scanCommand = async (directory, config, options) => {
6419
7157
  includeHeader: showHeader,
6420
7158
  printBrand: options.printBrand
6421
7159
  }));
6422
- return { exitCode };
7160
+ return completion;
6423
7161
  };
6424
7162
 
6425
7163
  //#endregion
@@ -7764,15 +8502,23 @@ const fixCommand = async (directory, config, options = {
7764
8502
  verbose: false,
7765
8503
  showHeader: true
7766
8504
  }) => {
7767
- const startTime = performance.now();
7768
8505
  const resolvedDir = path.resolve(directory);
7769
8506
  if (!fs.existsSync(resolvedDir) || !fs.statSync(resolvedDir).isDirectory()) {
7770
8507
  const msg = !fs.existsSync(resolvedDir) ? `Path does not exist: ${resolvedDir}` : `Not a directory: ${resolvedDir}`;
7771
8508
  log.error(msg);
7772
8509
  return;
7773
8510
  }
7774
- const showHeader = options.showHeader !== false;
7775
8511
  const projectInfo = await discoverProject(resolvedDir);
8512
+ await withCommandLifecycle({
8513
+ command: "fix",
8514
+ config: config.telemetry,
8515
+ languages: projectInfo.languages,
8516
+ fileCount: projectInfo.sourceFileCount
8517
+ }, () => runFixBody(resolvedDir, config, options, projectInfo));
8518
+ };
8519
+ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
8520
+ const startTime = performance.now();
8521
+ const showHeader = options.showHeader !== false;
7776
8522
  const projectName = projectInfo.projectName ?? "project";
7777
8523
  if (showHeader) process.stdout.write(renderHeader({
7778
8524
  version: APP_VERSION,
@@ -7809,12 +8555,6 @@ const fixCommand = async (directory, config, options = {
7809
8555
  await runFormattingStep(pipelineDeps);
7810
8556
  await runForceSteps(pipelineDeps);
7811
8557
  const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
7812
- if (!isTelemetryDisabled(config.telemetry?.enabled)) trackEvent({
7813
- command: "fix",
7814
- languages: projectInfo.languages,
7815
- fixSteps: steps.length,
7816
- fixResolved: totalResolved
7817
- });
7818
8558
  const configDir = findConfigDir(resolvedDir);
7819
8559
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
7820
8560
  const engineConfig = {
@@ -7837,7 +8577,9 @@ const fixCommand = async (directory, config, options = {
7837
8577
  });
7838
8578
  const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
7839
8579
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
7840
- const remaining = allDiagnostics.filter((d) => d.severity === "error").length + allDiagnostics.filter((d) => d.severity === "warning").length;
8580
+ const errors = allDiagnostics.filter((d) => d.severity === "error").length;
8581
+ const warnings = allDiagnostics.filter((d) => d.severity === "warning").length;
8582
+ const remaining = errors + warnings;
7841
8583
  if (steps.length === 0) rail.complete({
7842
8584
  status: "skipped",
7843
8585
  label: "No applicable auto-fixers found"
@@ -7866,12 +8608,31 @@ const fixCommand = async (directory, config, options = {
7866
8608
  }
7867
8609
  if (options.agent) {
7868
8610
  launchAgent(options.agent, resolvedDir, allDiagnostics, scoreResult.score);
7869
- return;
8611
+ return {
8612
+ exitCode: 0,
8613
+ score: scoreResult.score,
8614
+ fixSteps: steps.length,
8615
+ fixResolved: totalResolved
8616
+ };
7870
8617
  }
7871
8618
  if (options.prompt) {
7872
8619
  printPrompt(resolvedDir, allDiagnostics, scoreResult.score);
7873
- return;
8620
+ return {
8621
+ exitCode: 0,
8622
+ score: scoreResult.score,
8623
+ fixSteps: steps.length,
8624
+ fixResolved: totalResolved
8625
+ };
7874
8626
  }
8627
+ return {
8628
+ exitCode: 0,
8629
+ score: scoreResult.score,
8630
+ findingCount: allDiagnostics.length,
8631
+ errorCount: errors,
8632
+ warningCount: warnings,
8633
+ fixSteps: steps.length,
8634
+ fixResolved: totalResolved
8635
+ };
7875
8636
  };
7876
8637
 
7877
8638
  //#endregion
@@ -7987,10 +8748,18 @@ const promptForConfigChoices = async () => {
7987
8748
  return {
7988
8749
  engines: enginesSelection,
7989
8750
  failBelow: Number(failBelowRaw),
8751
+ typecheck: DEFAULT_CONFIG.lint.typecheck,
7990
8752
  telemetryEnabled: telemetryChoice === "enabled",
7991
8753
  writeGithubWorkflow: workflowChoice === "yes"
7992
8754
  };
7993
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
+ });
7994
8763
  const writeAislopConfig = (configDir, configPath, choices) => {
7995
8764
  const selected = new Set(choices.engines);
7996
8765
  const engines = {
@@ -8005,6 +8774,7 @@ const writeAislopConfig = (configDir, configPath, choices) => {
8005
8774
  version: DEFAULT_CONFIG.version,
8006
8775
  engines,
8007
8776
  quality: { ...DEFAULT_CONFIG.quality },
8777
+ lint: { typecheck: choices.typecheck },
8008
8778
  security: { ...DEFAULT_CONFIG.security },
8009
8779
  scoring: {
8010
8780
  weights: { ...DEFAULT_CONFIG.scoring.weights },
@@ -8057,7 +8827,7 @@ const initCommand = async (directory, options = {}) => {
8057
8827
  return;
8058
8828
  }
8059
8829
  }
8060
- const choices = await promptForConfigChoices();
8830
+ const choices = options.strict ? strictChoices() : await promptForConfigChoices();
8061
8831
  if (!choices) return;
8062
8832
  writeAislopConfig(configDir, configPath, choices);
8063
8833
  const steps = [{