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/cli.js CHANGED
@@ -34,7 +34,7 @@ var __exportAll = (all, no_symbols) => {
34
34
 
35
35
  //#endregion
36
36
  //#region src/version.ts
37
- const APP_VERSION = "0.9.0";
37
+ const APP_VERSION = "0.9.2";
38
38
 
39
39
  //#endregion
40
40
  //#region src/telemetry/env.ts
@@ -464,7 +464,21 @@ const buildSuggestedActions = (diagnostics, findings, regressed, delta) => {
464
464
  });
465
465
  return actions;
466
466
  };
467
- const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
467
+ const buildAccountability = (meta, findings, regressed, newSinceBaseline) => {
468
+ if (!meta?.agent && (!meta?.touchedFiles || meta.touchedFiles.length === 0)) return void 0;
469
+ const touchedFiles = Array.from(new Set(meta.touchedFiles ?? []));
470
+ const newFindingCount = newSinceBaseline?.length ?? findings.length;
471
+ const mustFixBeforeDone = regressed || findings.some((f) => f.severity === "error");
472
+ const reason = mustFixBeforeDone ? regressed ? "Score regressed against the captured baseline. The agent should fix or justify the new findings before finishing." : "Error-severity findings remain in files touched by this agent turn." : "No blocking regression detected for this agent turn.";
473
+ return {
474
+ agent: meta.agent,
475
+ touchedFiles,
476
+ newFindingCount,
477
+ mustFixBeforeDone,
478
+ reason
479
+ };
480
+ };
481
+ const buildFeedback = (diagnostics, score, rootDirectory, baseline, meta) => {
468
482
  const all = diagnostics.map((d) => toFinding(d, rootDirectory)).filter((x) => x !== null);
469
483
  const capped = all.slice(0, MAX_FINDINGS);
470
484
  const elided = all.length > MAX_FINDINGS ? all.length - MAX_FINDINGS : void 0;
@@ -492,6 +506,7 @@ const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
492
506
  baseline: baselineScore,
493
507
  delta,
494
508
  regressed,
509
+ accountability: buildAccountability(meta, capped, regressed, newSinceBaseline),
495
510
  counts,
496
511
  findings: capped,
497
512
  elided,
@@ -548,6 +563,7 @@ const DEFAULT_CONFIG = {
548
563
  "build",
549
564
  "coverage"
550
565
  ],
566
+ include: [],
551
567
  engines: {
552
568
  format: true,
553
569
  lint: true,
@@ -583,7 +599,7 @@ const DEFAULT_CONFIG = {
583
599
  smoothing: 20
584
600
  },
585
601
  ci: {
586
- failBelow: 0,
602
+ failBelow: 70,
587
603
  format: "json"
588
604
  },
589
605
  telemetry: { enabled: true }
@@ -709,7 +725,7 @@ const ScoringSchema = z.object({
709
725
  smoothing: z.number().nonnegative().default(20)
710
726
  });
711
727
  const CiSchema = z.object({
712
- failBelow: z.number().default(0),
728
+ failBelow: z.number().default(70),
713
729
  format: z.enum(["json"]).default("json")
714
730
  });
715
731
  const TelemetrySchema = z.object({ enabled: z.boolean().default(true) });
@@ -743,7 +759,7 @@ const AislopConfigSchema = z.object({
743
759
  smoothing: 20
744
760
  })),
745
761
  ci: CiSchema.default(() => ({
746
- failBelow: 0,
762
+ failBelow: 70,
747
763
  format: "json"
748
764
  })),
749
765
  telemetry: TelemetrySchema.default(() => ({ enabled: true })),
@@ -753,7 +769,8 @@ const AislopConfigSchema = z.object({
753
769
  "dist",
754
770
  "build",
755
771
  "coverage"
756
- ])
772
+ ]),
773
+ include: z.array(z.string()).default(() => [])
757
774
  });
758
775
  const defaults = AislopConfigSchema.parse({});
759
776
  /**
@@ -833,31 +850,68 @@ const EXCLUDED_DIRS = [
833
850
  "dist",
834
851
  "build",
835
852
  ".git",
853
+ ".agents",
836
854
  "vendor",
855
+ "examples",
856
+ "example",
857
+ "demos",
858
+ "demo",
859
+ "bench",
860
+ "benches",
861
+ "benchmarks",
862
+ "fixtures",
863
+ "fixture",
864
+ "samples",
865
+ "sample",
866
+ "tutorials",
867
+ "tutorial",
868
+ "code_samples",
869
+ "code-samples",
870
+ "notebooks",
837
871
  "tests",
838
872
  "test",
839
873
  "__tests__",
840
874
  "__test__",
841
875
  "spec",
842
876
  "__mocks__",
843
- "fixtures",
844
877
  "test_data",
845
878
  ".next",
846
879
  ".nuxt",
847
880
  "coverage",
848
- ".turbo"
881
+ ".turbo",
882
+ "public"
849
883
  ];
850
884
  const FIND_PRUNE_DIRS = [
851
885
  "node_modules",
852
886
  "dist",
853
887
  "build",
854
888
  ".git",
889
+ ".agents",
855
890
  "vendor",
891
+ "examples",
892
+ "example",
893
+ "demos",
894
+ "demo",
895
+ "bench",
896
+ "benches",
897
+ "benchmarks",
898
+ "fixtures",
899
+ "fixture",
900
+ "samples",
901
+ "sample",
902
+ "tutorials",
903
+ "tutorial",
904
+ "code_samples",
905
+ "code-samples",
906
+ "notebooks",
856
907
  ".next",
857
908
  ".nuxt",
858
909
  "coverage",
859
- ".turbo"
910
+ ".turbo",
911
+ "public"
860
912
  ];
913
+ const BUILD_CACHE_FILE_PATTERNS = [/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i];
914
+ const isBuildCacheFile = (filePath) => BUILD_CACHE_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
861
915
  const TEST_FILE_PATTERNS = [
862
916
  /(?:^|\/).*\.test\.[^/]+$/i,
863
917
  /(?:^|\/).*\.spec\.[^/]+$/i,
@@ -882,6 +936,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
882
936
  return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
883
937
  };
884
938
  const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
939
+ const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
885
940
  const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
886
941
  const getIgnoredPaths = (rootDirectory, files) => {
887
942
  if (files.length === 0) return /* @__PURE__ */ new Set();
@@ -940,7 +995,7 @@ const normalizeExcludePatterns = (patterns) => {
940
995
  return [p];
941
996
  });
942
997
  };
943
- const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = []) => {
998
+ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = [], include = []) => {
944
999
  const extraSet = new Set(extraExtensions);
945
1000
  const normalizedFiles = files.map((file) => {
946
1001
  const absolutePath = path.isAbsolute(file) ? file : path.resolve(rootDirectory, file);
@@ -955,8 +1010,16 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
955
1010
  if (!normalizedExcludePatterns.length) return false;
956
1011
  return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
957
1012
  };
1013
+ const hasIncludePatterns = include.length > 0;
1014
+ const isUserIncluded = (relativePath) => {
1015
+ if (!hasIncludePatterns) return true;
1016
+ return micromatch.isMatch(relativePath, include, { dot: true });
1017
+ };
958
1018
  return normalizedFiles.filter(({ absolutePath, relativePath }) => {
959
- return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile$2(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
1019
+ if (!fs.existsSync(absolutePath) || !isWithinProject(relativePath) || isExcludedPath(relativePath) || isTestFile$2(relativePath) || isBuildCacheFile(relativePath) || ignoredPaths.has(relativePath)) return false;
1020
+ if (!isUserIncluded(relativePath)) return false;
1021
+ if (isUserExcluded(relativePath)) return false;
1022
+ return hasAllowedExtension(relativePath, extraSet);
960
1023
  }).map(({ absolutePath }) => absolutePath);
961
1024
  };
962
1025
  const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
@@ -1686,6 +1749,86 @@ const PYTHON_IMPORT_TO_PIP = {
1686
1749
  redis: "redis"
1687
1750
  };
1688
1751
 
1752
+ //#endregion
1753
+ //#region src/engines/ai-slop/python-manifest.ts
1754
+ const addPyDep = (pyDeps, name) => {
1755
+ const normalized = name.toLowerCase().replace(/_/g, "-");
1756
+ pyDeps.add(normalized);
1757
+ };
1758
+ const collectFromRequirementsTxt = (rootDir, pyDeps) => {
1759
+ const reqPath = path.join(rootDir, "requirements.txt");
1760
+ if (!fs.existsSync(reqPath)) return false;
1761
+ try {
1762
+ const content = fs.readFileSync(reqPath, "utf-8");
1763
+ for (const line of content.split("\n")) {
1764
+ const trimmed = line.trim();
1765
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
1766
+ const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
1767
+ if (match) addPyDep(pyDeps, match[1]);
1768
+ }
1769
+ return true;
1770
+ } catch {
1771
+ return false;
1772
+ }
1773
+ };
1774
+ const collectFromPyproject = (rootDir, pyDeps) => {
1775
+ const pyprojPath = path.join(rootDir, "pyproject.toml");
1776
+ if (!fs.existsSync(pyprojPath)) return false;
1777
+ try {
1778
+ const content = fs.readFileSync(pyprojPath, "utf-8");
1779
+ const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1780
+ if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
1781
+ const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1782
+ if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
1783
+ const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
1784
+ if (pep621) for (const line of pep621[1].split("\n")) {
1785
+ const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
1786
+ if (m) addPyDep(pyDeps, m[1]);
1787
+ }
1788
+ const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
1789
+ let match = poetryRe.exec(content);
1790
+ while (match !== null) {
1791
+ for (const line of match[1].split("\n")) {
1792
+ const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1793
+ if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
1794
+ }
1795
+ match = poetryRe.exec(content);
1796
+ }
1797
+ return true;
1798
+ } catch {
1799
+ return false;
1800
+ }
1801
+ };
1802
+ const collectFromPipfile = (rootDir, pyDeps) => {
1803
+ const pipfilePath = path.join(rootDir, "Pipfile");
1804
+ if (!fs.existsSync(pipfilePath)) return false;
1805
+ try {
1806
+ const content = fs.readFileSync(pipfilePath, "utf-8");
1807
+ const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
1808
+ let match = sectionRe.exec(content);
1809
+ while (match !== null) {
1810
+ for (const line of match[2].split("\n")) {
1811
+ const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1812
+ if (m) addPyDep(pyDeps, m[1]);
1813
+ }
1814
+ match = sectionRe.exec(content);
1815
+ }
1816
+ return true;
1817
+ } catch {
1818
+ return false;
1819
+ }
1820
+ };
1821
+ const collectPythonDeps = (rootDir) => {
1822
+ const pyDeps = /* @__PURE__ */ new Set();
1823
+ const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
1824
+ const hasPyproject = collectFromPyproject(rootDir, pyDeps);
1825
+ const hasPipfile = collectFromPipfile(rootDir, pyDeps);
1826
+ return {
1827
+ pyDeps,
1828
+ hasPyManifest: hasReq || hasPyproject || hasPipfile
1829
+ };
1830
+ };
1831
+
1689
1832
  //#endregion
1690
1833
  //#region src/engines/ai-slop/hallucinated-imports.ts
1691
1834
  const JS_EXTENSIONS$2 = new Set([
@@ -1822,10 +1965,26 @@ const buildAliasMatcher = (key) => {
1822
1965
  };
1823
1966
  const collectAliasMatchersFromConfig = (configPath, matchers) => {
1824
1967
  const opts = readJson(configPath)?.compilerOptions;
1825
- if (!opts || typeof opts !== "object") return;
1968
+ if (!opts) return;
1826
1969
  const paths = opts.paths;
1827
- if (!paths || typeof paths !== "object") return;
1828
- for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
1970
+ if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
1971
+ const baseUrl = opts.baseUrl;
1972
+ if (typeof baseUrl === "string") {
1973
+ const baseUrlDir = path.resolve(path.dirname(configPath), baseUrl);
1974
+ let entries;
1975
+ try {
1976
+ entries = fs.readdirSync(baseUrlDir);
1977
+ } catch {
1978
+ return;
1979
+ }
1980
+ const baseSpecifiers = /* @__PURE__ */ new Set();
1981
+ for (const entry of entries) {
1982
+ if (entry.startsWith(".") || entry === "node_modules") continue;
1983
+ const base = entry.replace(/\.(?:[jt]sx?|mjs|cjs|d\.ts)$/i, "");
1984
+ if (base.length > 0) baseSpecifiers.add(base);
1985
+ }
1986
+ for (const name of baseSpecifiers) matchers.push((spec) => spec === name || spec.startsWith(`${name}/`));
1987
+ }
1829
1988
  };
1830
1989
  const collectTsPathAliases = (rootDir) => {
1831
1990
  const matchers = [];
@@ -1833,97 +1992,35 @@ const collectTsPathAliases = (rootDir) => {
1833
1992
  for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
1834
1993
  return matchers;
1835
1994
  };
1836
- const addPyDep = (pyDeps, name) => {
1837
- const normalized = name.toLowerCase().replace(/_/g, "-");
1838
- pyDeps.add(normalized);
1839
- };
1840
- const collectFromRequirementsTxt = (rootDir, pyDeps) => {
1841
- const reqPath = path.join(rootDir, "requirements.txt");
1842
- if (!fs.existsSync(reqPath)) return false;
1843
- try {
1844
- const content = fs.readFileSync(reqPath, "utf-8");
1845
- for (const line of content.split("\n")) {
1846
- const trimmed = line.trim();
1847
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
1848
- const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
1849
- if (match) addPyDep(pyDeps, match[1]);
1850
- }
1851
- return true;
1852
- } catch {
1853
- return false;
1854
- }
1855
- };
1856
- const collectFromPyproject = (rootDir, pyDeps) => {
1857
- const pyprojPath = path.join(rootDir, "pyproject.toml");
1858
- if (!fs.existsSync(pyprojPath)) return false;
1859
- try {
1860
- const content = fs.readFileSync(pyprojPath, "utf-8");
1861
- const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1862
- if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
1863
- const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1864
- if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
1865
- const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
1866
- if (pep621) for (const line of pep621[1].split("\n")) {
1867
- const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
1868
- if (m) addPyDep(pyDeps, m[1]);
1869
- }
1870
- const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
1871
- let match = poetryRe.exec(content);
1872
- while (match !== null) {
1873
- for (const line of match[1].split("\n")) {
1874
- const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1875
- if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
1876
- }
1877
- match = poetryRe.exec(content);
1878
- }
1879
- return true;
1880
- } catch {
1881
- return false;
1882
- }
1883
- };
1884
- const collectFromPipfile = (rootDir, pyDeps) => {
1885
- const pipfilePath = path.join(rootDir, "Pipfile");
1886
- if (!fs.existsSync(pipfilePath)) return false;
1887
- try {
1888
- const content = fs.readFileSync(pipfilePath, "utf-8");
1889
- const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
1890
- let match = sectionRe.exec(content);
1891
- while (match !== null) {
1892
- for (const line of match[2].split("\n")) {
1893
- const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1894
- if (m) addPyDep(pyDeps, m[1]);
1895
- }
1896
- match = sectionRe.exec(content);
1897
- }
1898
- return true;
1899
- } catch {
1900
- return false;
1901
- }
1902
- };
1903
1995
  const loadManifest = (rootDir) => {
1904
1996
  const jsDeps = /* @__PURE__ */ new Set();
1905
- const pyDeps = /* @__PURE__ */ new Set();
1906
1997
  const hasJsManifest = collectJsDeps(rootDir, jsDeps);
1907
- const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
1908
- const hasPyproject = collectFromPyproject(rootDir, pyDeps);
1909
- const hasPipfile = collectFromPipfile(rootDir, pyDeps);
1998
+ const { pyDeps, hasPyManifest } = collectPythonDeps(rootDir);
1910
1999
  return {
1911
2000
  jsDeps,
1912
2001
  pyDeps,
1913
2002
  hasJsManifest,
1914
- hasPyManifest: hasReq || hasPyproject || hasPipfile
2003
+ hasPyManifest
1915
2004
  };
1916
2005
  };
1917
2006
  const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
2007
+ const RUNTIME_BUILTINS = new Set(["bun"]);
1918
2008
  const isJsBuiltin = (spec) => {
2009
+ if (RUNTIME_BUILTINS.has(spec)) return true;
1919
2010
  return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
1920
2011
  };
1921
2012
  const VIRTUAL_MODULE_PREFIXES = [
1922
2013
  "astro:",
1923
2014
  "virtual:",
1924
- "bun:"
2015
+ "bun:",
2016
+ "~icons/"
1925
2017
  ];
1926
2018
  const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
2019
+ const stripImportQuery = (spec) => {
2020
+ const idx = spec.indexOf("?");
2021
+ return idx === -1 ? spec : spec.slice(0, idx);
2022
+ };
2023
+ const VIRTUAL_ASSET_FILES = { "unfonts.css": "unplugin-fonts" };
1927
2024
  const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
1928
2025
  const isLikelyRealImportSpec = (spec) => {
1929
2026
  if (spec.length === 0) return false;
@@ -1990,10 +2087,14 @@ const extractPyImports = (content) => {
1990
2087
  }
1991
2088
  return results;
1992
2089
  };
1993
- const checkJsImport = (spec, manifest, tsAliasMatchers) => {
2090
+ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
2091
+ const spec = stripImportQuery(rawSpec);
2092
+ if (spec.length === 0) return null;
1994
2093
  if (isJsRelativeOrAbsolute(spec)) return null;
1995
2094
  if (isJsBuiltin(spec)) return null;
1996
2095
  if (isJsVirtualModule(spec)) return null;
2096
+ const virtualOwner = VIRTUAL_ASSET_FILES[spec];
2097
+ if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
1997
2098
  if (tsAliasMatchers.some((m) => m(spec))) return null;
1998
2099
  const pkg = packageNameFromImport(spec);
1999
2100
  if (manifest.jsDeps.has(pkg)) return null;
@@ -3349,64 +3450,88 @@ const analyzeFunctions = (content, ext) => {
3349
3450
  }
3350
3451
  return functions;
3351
3452
  };
3352
- const JSX_FILE_LOC_MULTIPLIER = 1.5;
3453
+ const FILE_LOC_MULTIPLIERS = {
3454
+ ".tsx": 1.5,
3455
+ ".jsx": 1.5,
3456
+ ".rs": 2.5,
3457
+ ".go": 1.5
3458
+ };
3459
+ const DECLARATION_FILE_RE = /\.d\.ts$/i;
3460
+ const fileLocBudget = (ext, relativePath, base) => {
3461
+ if (DECLARATION_FILE_RE.test(relativePath)) return Number.POSITIVE_INFINITY;
3462
+ const multiplier = FILE_LOC_MULTIPLIERS[ext] ?? 1;
3463
+ return Math.ceil(base * multiplier);
3464
+ };
3353
3465
  const checkFileDiagnostics = (relativePath, content, limits) => {
3354
3466
  const results = [];
3355
3467
  const lineCount = content.split("\n").length;
3356
3468
  const ext = path.extname(relativePath).toLowerCase();
3357
3469
  if (isDataFile(content)) return results;
3358
- const configuredMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
3470
+ const configuredMax = fileLocBudget(ext, relativePath, limits.maxFileLoc);
3471
+ if (!Number.isFinite(configuredMax)) return results;
3359
3472
  if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
3360
3473
  filePath: relativePath,
3361
3474
  engine: "code-quality",
3362
3475
  rule: "complexity/file-too-large",
3363
3476
  severity: "warning",
3364
- message: `File has ${lineCount} lines (max: ${configuredMax})`,
3477
+ message: `File too large (max: ${configuredMax})`,
3365
3478
  help: "Consider splitting this file into smaller modules",
3366
3479
  line: 0,
3367
3480
  column: 0,
3368
3481
  category: "Complexity",
3369
- fixable: false
3482
+ fixable: false,
3483
+ detail: `${lineCount} lines`
3370
3484
  });
3371
3485
  return results;
3372
3486
  };
3373
- const checkFunctionDiagnostics = (relativePath, fn, limits) => {
3487
+ const JSX_EXTENSIONS = new Set([".tsx", ".jsx"]);
3488
+ const isComponentFunction = (name, ext) => JSX_EXTENSIONS.has(ext) && /^[A-Z]/.test(name);
3489
+ const functionLocBudget = (fn, ext, base) => {
3490
+ if (isComponentFunction(fn.name, ext)) return Math.ceil(base * 2);
3491
+ if (ext === ".rs") return Math.ceil(base * 1.5);
3492
+ return base;
3493
+ };
3494
+ const checkFunctionDiagnostics = (relativePath, fn, limits, ext) => {
3374
3495
  const results = [];
3375
- if (fn.lineCount - fn.templateLines > Math.ceil(limits.maxFunctionLoc * 1.1)) results.push({
3496
+ const fnMax = functionLocBudget(fn, ext, limits.maxFunctionLoc);
3497
+ if (fn.lineCount - fn.templateLines > Math.ceil(fnMax * 1.1)) results.push({
3376
3498
  filePath: relativePath,
3377
3499
  engine: "code-quality",
3378
3500
  rule: "complexity/function-too-long",
3379
3501
  severity: "warning",
3380
- message: `Function '${fn.name}' has ${fn.lineCount} lines (max: ${limits.maxFunctionLoc})`,
3502
+ message: `Function too long (max: ${fnMax})`,
3381
3503
  help: "Consider breaking this function into smaller pieces",
3382
3504
  line: fn.startLine,
3383
3505
  column: 0,
3384
3506
  category: "Complexity",
3385
- fixable: false
3507
+ fixable: false,
3508
+ detail: `${fn.name} · ${fn.lineCount} lines`
3386
3509
  });
3387
3510
  if (fn.maxNesting > limits.maxNesting) results.push({
3388
3511
  filePath: relativePath,
3389
3512
  engine: "code-quality",
3390
3513
  rule: "complexity/deep-nesting",
3391
3514
  severity: "warning",
3392
- message: `Function '${fn.name}' has nesting depth ${fn.maxNesting} (max: ${limits.maxNesting})`,
3515
+ message: `Function nested too deeply (max: ${limits.maxNesting})`,
3393
3516
  help: "Consider using early returns or extracting nested logic",
3394
3517
  line: fn.startLine,
3395
3518
  column: 0,
3396
3519
  category: "Complexity",
3397
- fixable: false
3520
+ fixable: false,
3521
+ detail: `${fn.name} · depth ${fn.maxNesting}`
3398
3522
  });
3399
3523
  if (fn.paramCount > limits.maxParams) results.push({
3400
3524
  filePath: relativePath,
3401
3525
  engine: "code-quality",
3402
3526
  rule: "complexity/too-many-params",
3403
3527
  severity: "warning",
3404
- message: `Function '${fn.name}' has ${fn.paramCount} parameters (max: ${limits.maxParams})`,
3528
+ message: `Function has too many parameters (max: ${limits.maxParams})`,
3405
3529
  help: "Consider using an options object parameter",
3406
3530
  line: fn.startLine,
3407
3531
  column: 0,
3408
3532
  category: "Complexity",
3409
- fixable: false
3533
+ fixable: false,
3534
+ detail: `${fn.name} · ${fn.paramCount} params`
3410
3535
  });
3411
3536
  return results;
3412
3537
  };
@@ -3421,7 +3546,7 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
3421
3546
  }
3422
3547
  const ext = path.extname(filePath).toLowerCase();
3423
3548
  const diagnostics = checkFileDiagnostics(relativePath, content, limits);
3424
- for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
3549
+ for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits, ext));
3425
3550
  return diagnostics;
3426
3551
  };
3427
3552
  const checkComplexity = async (context) => {
@@ -3526,17 +3651,19 @@ const findDuplicateBlocks = (content, relativePath) => {
3526
3651
  });
3527
3652
  }
3528
3653
  return reports.map((r) => {
3654
+ const span = r.currentEnd - r.currentStart + 1;
3529
3655
  return {
3530
3656
  filePath: relativePath,
3531
3657
  engine: "code-quality",
3532
3658
  rule: "code-quality/duplicate-block",
3533
3659
  severity: "warning",
3534
- message: `${r.currentEnd - r.currentStart + 1}-line block at line ${r.currentStart} duplicates a block starting at line ${r.priorStart}. Extract a shared helper.`,
3660
+ message: "Duplicate code block extract a shared helper",
3535
3661
  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.`,
3536
3662
  line: r.currentStart,
3537
3663
  column: 0,
3538
3664
  category: "Complexity",
3539
- fixable: false
3665
+ fixable: false,
3666
+ detail: `${span} lines duplicate block at L${r.priorStart}`
3540
3667
  };
3541
3668
  });
3542
3669
  };
@@ -4202,16 +4329,34 @@ const isToolAvailable = async (toolName) => {
4202
4329
  return isToolInstalled(toolName);
4203
4330
  };
4204
4331
 
4332
+ //#endregion
4333
+ //#region src/engines/python-targets.ts
4334
+ const PYTHON_EXTENSIONS = new Set([".py", ".pyi"]);
4335
+ const normalizeProjectPath = (filePath) => filePath.split(path.sep).join("/");
4336
+ const getPythonTargets = (context) => {
4337
+ const targets = (context.files ?? getSourceFiles(context)).filter((filePath) => PYTHON_EXTENSIONS.has(path.extname(filePath).toLowerCase())).map((filePath) => {
4338
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(context.rootDirectory, filePath);
4339
+ return normalizeProjectPath(path.relative(context.rootDirectory, absolutePath));
4340
+ }).filter((filePath) => filePath.length > 0 && !filePath.startsWith(".."));
4341
+ return [...new Set(targets)];
4342
+ };
4343
+ const getRuffDiagnosticPath = (rootDirectory, filePath) => {
4344
+ const normalizedPath = filePath.replace(/^a\//, "");
4345
+ return normalizeProjectPath(path.isAbsolute(normalizedPath) ? path.relative(rootDirectory, normalizedPath) : normalizedPath);
4346
+ };
4347
+
4205
4348
  //#endregion
4206
4349
  //#region src/engines/format/ruff-format.ts
4207
4350
  const runRuffFormat = async (context) => {
4208
4351
  const ruffBinary = resolveToolBinary("ruff");
4352
+ const targets = getPythonTargets(context);
4353
+ if (targets.length === 0) return [];
4209
4354
  try {
4210
4355
  const result = await runSubprocess(ruffBinary, [
4211
4356
  "format",
4212
4357
  "--check",
4213
4358
  "--diff",
4214
- context.rootDirectory
4359
+ ...targets
4215
4360
  ], {
4216
4361
  cwd: context.rootDirectory,
4217
4362
  timeout: 6e4
@@ -4227,9 +4372,9 @@ const parseRuffFormatOutput = (output, rootDir) => {
4227
4372
  const filePattern = /^--- (.+)$/gm;
4228
4373
  let match;
4229
4374
  while ((match = filePattern.exec(output)) !== null) {
4230
- const filePath = match[1].replace(/^a\//, "");
4375
+ const filePath = getRuffDiagnosticPath(rootDir, match[1]);
4231
4376
  diagnostics.push({
4232
- filePath: path.relative(rootDir, filePath),
4377
+ filePath,
4233
4378
  engine: "format",
4234
4379
  rule: "python-formatting",
4235
4380
  severity: "warning",
@@ -4738,6 +4883,95 @@ const resolveOxlintBinary = () => {
4738
4883
  return "oxlint";
4739
4884
  }
4740
4885
  };
4886
+ const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
4887
+ const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
4888
+ const AMBIENT_GLOBAL_DEPS = [
4889
+ "unplugin-icons",
4890
+ "@types/bun",
4891
+ "bun-types"
4892
+ ];
4893
+ const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
4894
+ const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
4895
+ const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
4896
+ const detectAmbientSources = (rootDir) => {
4897
+ const found = /* @__PURE__ */ new Set();
4898
+ const skipDirs = new Set([
4899
+ "node_modules",
4900
+ ".git",
4901
+ "dist",
4902
+ "build",
4903
+ "out",
4904
+ "target",
4905
+ "coverage",
4906
+ ".next",
4907
+ ".turbo"
4908
+ ]);
4909
+ const walk = (dir, depth) => {
4910
+ if (depth > 4 || found.size === AMBIENT_GLOBAL_DEPS.length) return;
4911
+ let entries;
4912
+ try {
4913
+ entries = fs.readdirSync(dir, { withFileTypes: true });
4914
+ } catch {
4915
+ return;
4916
+ }
4917
+ for (const entry of entries) {
4918
+ if (found.size === AMBIENT_GLOBAL_DEPS.length) return;
4919
+ if (entry.name.startsWith(".") && entry.name !== ".github") continue;
4920
+ if (skipDirs.has(entry.name)) continue;
4921
+ const full = path.join(dir, entry.name);
4922
+ if (entry.isDirectory()) walk(full, depth + 1);
4923
+ else if (entry.name === "package.json") try {
4924
+ const pkg = JSON.parse(fs.readFileSync(full, "utf-8"));
4925
+ const allDeps = {
4926
+ ...pkg.dependencies ?? {},
4927
+ ...pkg.devDependencies ?? {},
4928
+ ...pkg.peerDependencies ?? {}
4929
+ };
4930
+ for (const dep of AMBIENT_GLOBAL_DEPS) if (dep in allDeps) found.add(dep);
4931
+ } catch {}
4932
+ }
4933
+ };
4934
+ walk(rootDir, 0);
4935
+ return found;
4936
+ };
4937
+ const extractNoUndefIdentifier = (message) => {
4938
+ return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
4939
+ };
4940
+ const isAmbientFalsePositive = (rule, message, sources) => {
4941
+ if (rule !== "eslint/no-undef") return false;
4942
+ const ident = extractNoUndefIdentifier(message);
4943
+ if (!ident) return false;
4944
+ if (sources.has("unplugin-icons") && ICON_AUTOIMPORT_RE.test(ident)) return true;
4945
+ if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
4946
+ return false;
4947
+ };
4948
+ const sstReferencedFiles = /* @__PURE__ */ new Map();
4949
+ const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
4950
+ const cached = sstReferencedFiles.get(relativeFilePath);
4951
+ if (cached !== void 0) return cached;
4952
+ const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
4953
+ let referenced = false;
4954
+ try {
4955
+ const fd = fs.openSync(absolute, "r");
4956
+ try {
4957
+ const buf = Buffer.alloc(512);
4958
+ const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
4959
+ referenced = SST_PLATFORM_REF_RE.test(buf.toString("utf-8", 0, bytesRead));
4960
+ } finally {
4961
+ fs.closeSync(fd);
4962
+ }
4963
+ } catch {
4964
+ referenced = false;
4965
+ }
4966
+ sstReferencedFiles.set(relativeFilePath, referenced);
4967
+ return referenced;
4968
+ };
4969
+ const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
4970
+ const isUnderscoreUnusedVar = (rule, message) => {
4971
+ if (rule !== "eslint/no-unused-vars") return false;
4972
+ const match = UNUSED_VAR_IDENT_RE.exec(message);
4973
+ return match ? match[1].startsWith("_") : false;
4974
+ };
4741
4975
  const parseRuleCode = (code) => {
4742
4976
  if (!code) return {
4743
4977
  plugin: "eslint",
@@ -4834,6 +5068,8 @@ const runOxlint = async (context) => {
4834
5068
  framework: context.frameworks.find((f) => f !== "none"),
4835
5069
  testFramework: detectTestFramework(context.rootDirectory)
4836
5070
  });
5071
+ const ambientSources = detectAmbientSources(context.rootDirectory);
5072
+ sstReferencedFiles.clear();
4837
5073
  try {
4838
5074
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
4839
5075
  const args = [
@@ -4873,6 +5109,11 @@ const runOxlint = async (context) => {
4873
5109
  fixable: false
4874
5110
  };
4875
5111
  }).filter((d) => {
5112
+ if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
5113
+ if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
5114
+ if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
5115
+ if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
5116
+ if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
4876
5117
  const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
4877
5118
  if (seen.has(key)) return false;
4878
5119
  seen.add(key);
@@ -4936,18 +5177,20 @@ const fixOxlint = async (context, options = {}) => {
4936
5177
  //#region src/engines/lint/ruff.ts
4937
5178
  const runRuffLint = async (context) => {
4938
5179
  const ruffBinary = resolveToolBinary("ruff");
5180
+ const targets = getPythonTargets(context);
5181
+ if (targets.length === 0) return [];
4939
5182
  try {
4940
5183
  const output = (await runSubprocess(ruffBinary, [
4941
5184
  "check",
4942
5185
  "--output-format=json",
4943
- context.rootDirectory
5186
+ ...targets
4944
5187
  ], {
4945
5188
  cwd: context.rootDirectory,
4946
5189
  timeout: 6e4
4947
5190
  })).stdout;
4948
5191
  if (!output) return [];
4949
5192
  return JSON.parse(output).map((d) => ({
4950
- filePath: path.relative(context.rootDirectory, d.filename),
5193
+ filePath: getRuffDiagnosticPath(context.rootDirectory, d.filename),
4951
5194
  engine: "lint",
4952
5195
  rule: `ruff/${d.code}`,
4953
5196
  severity: d.code.startsWith("E") || d.code.startsWith("F") ? "error" : "warning",
@@ -5065,56 +5308,94 @@ const runPnpmAuditWithFallback = async (rootDir, timeout) => {
5065
5308
  return [];
5066
5309
  }
5067
5310
  };
5311
+ const SEVERITY_RANK = {
5312
+ critical: 4,
5313
+ high: 3,
5314
+ moderate: 2,
5315
+ low: 1
5316
+ };
5068
5317
  const toSeverity = (value) => value === "critical" || value === "high" ? "error" : "warning";
5069
- const defaultAuditFixCommand = (source) => source === "pnpm audit" ? "pnpm audit --fix" : "npm audit fix";
5318
+ const upsertVuln = (bucket, packageName, severity, recommendation) => {
5319
+ const existing = bucket.get(packageName);
5320
+ if (existing) {
5321
+ existing.advisories++;
5322
+ if ((SEVERITY_RANK[severity] ?? 0) > (SEVERITY_RANK[existing.worstSeverity] ?? 0)) existing.worstSeverity = severity;
5323
+ if (recommendation) existing.recommendations.add(recommendation);
5324
+ } else bucket.set(packageName, {
5325
+ packageName,
5326
+ worstSeverity: severity,
5327
+ advisories: 1,
5328
+ recommendations: recommendation ? new Set([recommendation]) : /* @__PURE__ */ new Set()
5329
+ });
5330
+ };
5331
+ const SEMVER_RE = /(\d+)\.(\d+)\.(\d+)/;
5332
+ const cmpSemver = (a, b) => {
5333
+ const [, a1, a2, a3] = SEMVER_RE.exec(a) ?? [
5334
+ "",
5335
+ "0",
5336
+ "0",
5337
+ "0"
5338
+ ];
5339
+ const [, b1, b2, b3] = SEMVER_RE.exec(b) ?? [
5340
+ "",
5341
+ "0",
5342
+ "0",
5343
+ "0"
5344
+ ];
5345
+ if (Number(a1) !== Number(b1)) return Number(a1) - Number(b1);
5346
+ if (Number(a2) !== Number(b2)) return Number(a2) - Number(b2);
5347
+ return Number(a3) - Number(b3);
5348
+ };
5349
+ const pickBestRecommendation = (recs) => {
5350
+ if (recs.length <= 1) return recs[0] ?? "";
5351
+ const versioned = recs.filter((r) => SEMVER_RE.test(r));
5352
+ if (versioned.length === 0) return recs[0];
5353
+ return versioned.reduce((best, r) => cmpSemver(r, best) > 0 ? r : best);
5354
+ };
5355
+ const cleanRecommendation = (raw) => {
5356
+ const t = raw.trim();
5357
+ if (!t || t.toLowerCase() === "none") return "no fix available";
5358
+ return t;
5359
+ };
5360
+ const aggregateToDiagnostic = (agg, source) => {
5361
+ const best = cleanRecommendation(pickBestRecommendation([...agg.recommendations]));
5362
+ const countLabel = agg.advisories > 1 ? ` (${agg.advisories} advisories)` : "";
5363
+ const recLabel = best ? ` — ${best}` : "";
5364
+ return {
5365
+ filePath: "package.json",
5366
+ engine: "security",
5367
+ rule: "security/vulnerable-dependency",
5368
+ severity: toSeverity(agg.worstSeverity),
5369
+ message: `${agg.packageName} (${agg.worstSeverity})${recLabel}${countLabel}`,
5370
+ help: "",
5371
+ line: 0,
5372
+ column: 0,
5373
+ category: "Security",
5374
+ fixable: false,
5375
+ detail: source === "npm audit" ? "npm" : "pnpm"
5376
+ };
5377
+ };
5070
5378
  const parseLegacyAdvisories = (advisories, source) => {
5071
- const diagnostics = [];
5072
- for (const [key, advisory] of Object.entries(advisories)) {
5073
- const packageName = advisory.module_name ?? advisory.name ?? advisory.package ?? key;
5074
- const severity = (advisory.severity ?? "moderate").toLowerCase();
5075
- const recommendation = advisory.recommendation ?? advisory.title ?? `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
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
- });
5088
- }
5089
- return diagnostics;
5379
+ const bucket = /* @__PURE__ */ new Map();
5380
+ 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 ?? "");
5381
+ return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
5090
5382
  };
5091
5383
  const parseModernVulnerabilities = (vulnerabilities, source) => {
5092
- const diagnostics = [];
5384
+ const bucket = /* @__PURE__ */ new Map();
5093
5385
  for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
5094
5386
  const severity = (vulnerability.severity ?? "moderate").toLowerCase();
5095
5387
  const fixAvailable = vulnerability.fixAvailable;
5096
5388
  const isDirect = vulnerability.isDirect === true;
5097
- let recommendation = `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
5098
- 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";
5099
- else if (!isDirect && fixAvailable === true) recommendation = "Transitive dep — may need an override or parent upgrade";
5389
+ let recommendation = "";
5390
+ if (fixAvailable === false) recommendation = isDirect ? "no automatic fix" : "transitiveneeds override or parent upgrade";
5391
+ else if (!isDirect && fixAvailable === true) recommendation = "transitive — may need override or parent upgrade";
5100
5392
  else if (fixAvailable && typeof fixAvailable === "object" && "name" in fixAvailable && "version" in fixAvailable) {
5101
5393
  const target = fixAvailable;
5102
- if (target.name && target.version) recommendation = `Upgrade to ${target.name}@${target.version}.`;
5394
+ if (target.name && target.version) recommendation = `upgrade to ${target.name}@${target.version}`;
5103
5395
  }
5104
- diagnostics.push({
5105
- filePath: "package.json",
5106
- engine: "security",
5107
- rule: "security/vulnerable-dependency",
5108
- severity: toSeverity(severity),
5109
- message: `Vulnerable dependency (${source}): ${packageName} (${severity})`,
5110
- help: withFixHint(recommendation),
5111
- line: 0,
5112
- column: 0,
5113
- category: "Security",
5114
- fixable: false
5115
- });
5396
+ upsertVuln(bucket, packageName, severity, recommendation);
5116
5397
  }
5117
- return diagnostics;
5398
+ return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
5118
5399
  };
5119
5400
  const parseJsAudit = (output, source) => {
5120
5401
  if (!output) return [];
@@ -6214,7 +6495,10 @@ const runClaudeHook = async (deps = {}) => {
6214
6495
  const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline ? {
6215
6496
  score: baseline.score,
6216
6497
  findingFingerprints: baseline.findingFingerprints
6217
- } : void 0);
6498
+ } : void 0, {
6499
+ agent: "claude",
6500
+ touchedFiles: files
6501
+ });
6218
6502
  track({
6219
6503
  event: "hook_scan_completed",
6220
6504
  properties: buildHookScanCompletedProps({
@@ -6295,6 +6579,9 @@ const runClaudeStopHook = async (deps = {}) => {
6295
6579
  const feedback = buildFeedback(diagnostics, score, rootDirectory, {
6296
6580
  score: baseline.score,
6297
6581
  findingFingerprints: baseline.findingFingerprints
6582
+ }, {
6583
+ agent: "claude",
6584
+ touchedFiles: sessionFiles
6298
6585
  });
6299
6586
  if (!feedback.regressed) {
6300
6587
  clearSessionFiles(cwd);
@@ -6355,7 +6642,10 @@ const runCursorHook = async (deps = {}) => {
6355
6642
  if (!release) return 0;
6356
6643
  try {
6357
6644
  const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
6358
- const feedback = buildFeedback(diagnostics, score, rootDirectory);
6645
+ const feedback = buildFeedback(diagnostics, score, rootDirectory, void 0, {
6646
+ agent: "cursor",
6647
+ touchedFiles: files
6648
+ });
6359
6649
  track({
6360
6650
  event: "hook_scan_completed",
6361
6651
  properties: buildHookScanCompletedProps({
@@ -6414,7 +6704,10 @@ const runGeminiHook = async (deps = {}) => {
6414
6704
  if (!release) return 0;
6415
6705
  try {
6416
6706
  const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
6417
- const feedback = buildFeedback(diagnostics, score, rootDirectory);
6707
+ const feedback = buildFeedback(diagnostics, score, rootDirectory, void 0, {
6708
+ agent: "gemini",
6709
+ touchedFiles: files
6710
+ });
6418
6711
  track({
6419
6712
  event: "hook_scan_completed",
6420
6713
  properties: buildHookScanCompletedProps({
@@ -7745,8 +8038,53 @@ const wrapHelpText = (text, maxWidth, indent) => {
7745
8038
  };
7746
8039
  const terminalWidth = () => {
7747
8040
  const raw = process.stdout.columns;
7748
- if (typeof raw !== "number" || raw <= 0) return 100;
7749
- return Math.min(raw, 100);
8041
+ if (typeof raw !== "number" || raw <= 0) return 120;
8042
+ return Math.min(raw, 120);
8043
+ };
8044
+ const renderRuleHeader = (first, count, lines) => {
8045
+ const level = toSeverityLabel(first.severity);
8046
+ const countLabel = count > 1 ? ` (${count})` : "";
8047
+ const status = colorBySeverity(level, first.severity);
8048
+ const fixableTag = first.fixable ? ` ${style(theme, "muted", "[auto]")}` : "";
8049
+ const fixableWidth = first.fixable ? 7 : 0;
8050
+ const badgePrefix = ` [${status}]${fixableTag} `;
8051
+ const badgePrefixWidth = 5 + level.length + 1 + fixableWidth + 1;
8052
+ const wrapped = wrapText(`${first.message}${countLabel}`, terminalWidth(), badgePrefixWidth, " ");
8053
+ lines.push(`${badgePrefix}${wrapped[0]}`);
8054
+ for (let i = 1; i < wrapped.length; i++) lines.push(wrapped[i]);
8055
+ };
8056
+ const renderLocations = (ruleDiags, verbose, lines) => {
8057
+ const unique = [];
8058
+ const seen = /* @__PURE__ */ new Set();
8059
+ for (const d of ruleDiags) {
8060
+ const label = toLocationLabel(d);
8061
+ const detail = d.detail ?? "";
8062
+ const key = `${label}|${detail}`;
8063
+ if (seen.has(key)) continue;
8064
+ seen.add(key);
8065
+ unique.push({
8066
+ label,
8067
+ detail
8068
+ });
8069
+ }
8070
+ const shown = verbose ? unique : unique.slice(0, 3);
8071
+ const maxLabel = shown.reduce((w, l) => Math.max(w, l.label.length), 0);
8072
+ for (const { label, detail } of shown) {
8073
+ const padded = detail ? `${label.padEnd(maxLabel)} ${detail}` : label;
8074
+ lines.push(style(theme, "muted", ` ${padded}`));
8075
+ }
8076
+ if (!verbose && unique.length > shown.length) lines.push(style(theme, "muted", ` +${unique.length - shown.length} more location(s), use -d for full list`));
8077
+ };
8078
+ const renderHiddenFooter = (sorted, maxRules, lines) => {
8079
+ const hidden = sorted.slice(maxRules);
8080
+ const hiddenErrors = hidden.reduce((acc, [, diags]) => acc + (diags[0].severity === "error" ? diags.length : 0), 0);
8081
+ const hiddenWarnings = hidden.reduce((acc, [, diags]) => acc + (diags[0].severity === "warning" ? diags.length : 0), 0);
8082
+ const parts = [];
8083
+ if (hiddenErrors > 0) parts.push(`${hiddenErrors} error${hiddenErrors === 1 ? "" : "s"}`);
8084
+ if (hiddenWarnings > 0) parts.push(`${hiddenWarnings} warning${hiddenWarnings === 1 ? "" : "s"}`);
8085
+ const detail = parts.length > 0 ? ` (${parts.join(", ")})` : "";
8086
+ lines.push(style(theme, "muted", ` ... and ${hidden.length} more rules hidden${detail}. Run with -v or --verbose to see full output.`));
8087
+ lines.push("");
7750
8088
  };
7751
8089
  const renderDiagnostics = (diagnostics, verbose) => {
7752
8090
  const lines = [];
@@ -7755,29 +8093,23 @@ const renderDiagnostics = (diagnostics, verbose) => {
7755
8093
  const label = getEngineLabel(engine);
7756
8094
  lines.push(` ${style(theme, "bold", `${symbols.engineActive} ${label}`)}`);
7757
8095
  const sorted = [...groupBy(engineDiags, (d) => `${d.rule}:${d.message}`).entries()].sort(([, a], [, b]) => {
7758
- return (a[0].severity === "error" ? 0 : a[0].severity === "warning" ? 1 : 2) - (b[0].severity === "error" ? 0 : b[0].severity === "warning" ? 1 : 2);
8096
+ const sa = a[0].severity === "error" ? 0 : a[0].severity === "warning" ? 1 : 2;
8097
+ const sb = b[0].severity === "error" ? 0 : b[0].severity === "warning" ? 1 : 2;
8098
+ if (sa !== sb) return sa - sb;
8099
+ return b.length - a.length;
7759
8100
  });
7760
- for (const [, ruleDiags] of sorted) {
8101
+ const maxRules = verbose ? Infinity : 40;
8102
+ for (const [, ruleDiags] of sorted.slice(0, maxRules)) {
7761
8103
  const first = ruleDiags[0];
7762
- const level = toSeverityLabel(first.severity);
7763
- const count = ruleDiags.length > 1 ? ` (${ruleDiags.length})` : "";
7764
- const status = colorBySeverity(level, first.severity);
7765
- const fixableTag = first.fixable ? ` ${style(theme, "muted", "[auto]")}` : "";
7766
- const fixableWidth = first.fixable ? 7 : 0;
7767
- const badgePrefix = ` [${status}]${fixableTag} `;
7768
- const badgePrefixWidth = 5 + level.length + 1 + fixableWidth + 1;
7769
- const wrappedMsg = wrapText(`${first.message}${count}`, terminalWidth(), badgePrefixWidth, " ");
7770
- lines.push(`${badgePrefix}${wrappedMsg[0]}`);
7771
- for (let i = 1; i < wrappedMsg.length; i++) lines.push(wrappedMsg[i]);
7772
- const locations = verbose ? ruleDiags : ruleDiags.slice(0, 3);
7773
- for (const diagnostic of locations) lines.push(style(theme, "muted", ` ${toLocationLabel(diagnostic)}`));
7774
- if (!verbose && ruleDiags.length > locations.length) lines.push(style(theme, "muted", ` +${ruleDiags.length - locations.length} more location(s), use -d for full list`));
8104
+ renderRuleHeader(first, ruleDiags.length, lines);
8105
+ renderLocations(ruleDiags, verbose, lines);
7775
8106
  if (first.help) {
7776
8107
  const wrapped = wrapHelpText(first.help, terminalWidth(), " ");
7777
8108
  for (const line of wrapped) lines.push(style(theme, "muted", line));
7778
8109
  }
7779
8110
  lines.push("");
7780
8111
  }
8112
+ if (sorted.length > maxRules) renderHiddenFooter(sorted, maxRules, lines);
7781
8113
  }
7782
8114
  return `${lines.join("\n")}\n`;
7783
8115
  };
@@ -7956,6 +8288,81 @@ var LiveGrid = class {
7956
8288
  }
7957
8289
  };
7958
8290
 
8291
+ //#endregion
8292
+ //#region src/output/rule-labels.ts
8293
+ const RULE_LABELS = {
8294
+ formatting: "Code not formatted",
8295
+ "code-quality/duplicate-block": "Duplicate code block",
8296
+ "complexity/file-too-large": "File too large",
8297
+ "complexity/function-too-long": "Function too long",
8298
+ "complexity/deep-nesting": "Deeply nested code",
8299
+ "complexity/too-many-params": "Too many parameters",
8300
+ "knip/files": "Unused file",
8301
+ "knip/dependencies": "Unused dependency",
8302
+ "knip/devDependencies": "Unused dev dependency",
8303
+ "knip/unlisted": "Used but not in package.json",
8304
+ "knip/unresolved": "Unresolved import",
8305
+ "knip/binaries": "Unused binary",
8306
+ "knip/exports": "Unused export",
8307
+ "knip/types": "Unused type",
8308
+ "ai-slop/trivial-comment": "Trivial restating comment",
8309
+ "ai-slop/swallowed-exception": "Empty catch (swallowed error)",
8310
+ "ai-slop/thin-wrapper": "Thin function wrapper",
8311
+ "ai-slop/generic-naming": "Generic/vague identifier name",
8312
+ "ai-slop/unused-import": "Unused import",
8313
+ "ai-slop/console-leftover": "console.log left in code",
8314
+ "ai-slop/todo-stub": "Unresolved TODO/FIXME",
8315
+ "ai-slop/unreachable-code": "Unreachable code",
8316
+ "ai-slop/constant-condition": "Constant condition",
8317
+ "ai-slop/empty-function": "Empty function body",
8318
+ "ai-slop/unsafe-type-assertion": "Unsafe type cast",
8319
+ "ai-slop/double-type-assertion": "Double type cast",
8320
+ "ai-slop/ts-directive": "@ts-ignore / @ts-expect-error",
8321
+ "ai-slop/narrative-comment": "Narrative comment block",
8322
+ "ai-slop/duplicate-import": "Duplicate import statement",
8323
+ "ai-slop/python-bare-except": "Bare except",
8324
+ "ai-slop/python-broad-except": "Broad except",
8325
+ "ai-slop/python-mutable-default": "Mutable default argument",
8326
+ "ai-slop/python-print-debug": "print() left in code",
8327
+ "ai-slop/go-library-panic": "panic() in Go library code",
8328
+ "ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
8329
+ "ai-slop/rust-todo-stub": "Rust todo!() stub",
8330
+ "ai-slop/hallucinated-import": "Import not in package.json",
8331
+ "security/hardcoded-secret": "Possible hardcoded secret",
8332
+ "security/vulnerable-dependency": "Vulnerable dependency",
8333
+ "security/eval": "eval() usage",
8334
+ "security/innerhtml": "innerHTML assignment",
8335
+ "security/dangerously-set-innerhtml": "dangerouslySetInnerHTML (XSS risk)",
8336
+ "security/sql-injection": "Possible SQL injection",
8337
+ "security/shell-injection": "Possible shell injection",
8338
+ "eslint/no-undef": "Undefined identifier",
8339
+ "eslint/no-unused-vars": "Unused variable",
8340
+ "eslint/no-unassigned-vars": "Variable never assigned",
8341
+ "eslint/no-empty": "Empty block statement",
8342
+ "eslint/no-unused-expressions": "Unused expression",
8343
+ "eslint/no-shadow-restricted-names": "Shadowing restricted name",
8344
+ "eslint/no-constant-binary-expression": "Constant binary expression",
8345
+ "eslint/no-unsafe-optional-chaining": "Unsafe optional chaining",
8346
+ "eslint/require-yield": "Generator with no yield",
8347
+ "import/no-duplicates": "Duplicate import path",
8348
+ "import/default": "Missing default export",
8349
+ "import/named": "Missing named export",
8350
+ "import/namespace": "Invalid namespace import",
8351
+ "typescript-eslint/triple-slash-reference": "Triple-slash reference",
8352
+ "unicorn/no-useless-fallback-in-spread": "Useless spread fallback",
8353
+ "unicorn/no-invalid-remove-event-listener": "Invalid removeEventListener",
8354
+ "unicorn/no-empty-file": "Empty file",
8355
+ "unicorn/no-useless-length-check": "Useless array length check",
8356
+ "unicorn/no-new-array": "Avoid new Array(n)",
8357
+ "unicorn/no-useless-spread": "Useless spread",
8358
+ "unicorn/no-single-promise-in-promise-methods": "Single-element Promise.all"
8359
+ };
8360
+ const prettifyFallback = (ruleId) => {
8361
+ const spaced = (ruleId.includes("/") ? ruleId.slice(ruleId.indexOf("/") + 1) : ruleId).replace(/[-_]/g, " ").replace(/\//g, " · ");
8362
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
8363
+ };
8364
+ const labelForRule = (ruleId) => RULE_LABELS[ruleId] ?? prettifyFallback(ruleId);
8365
+
7959
8366
  //#endregion
7960
8367
  //#region src/ui/summary.ts
7961
8368
  const elapsed = (ms) => ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
@@ -7982,6 +8389,34 @@ const renderSummary = (input, deps = {}) => {
7982
8389
  ` ${style(t, "muted", `${input.files} files`)} ${sep} ${style(t, "muted", `${input.engines} engines`)} ${sep} ${style(t, "muted", elapsed(input.elapsedMs))}`,
7983
8390
  ""
7984
8391
  ];
8392
+ if (input.breakdown && input.breakdown.rows.length > 0) {
8393
+ lines.push(` ${style(t, "bold", "Top findings")}`);
8394
+ const maxCountWidth = input.breakdown.rows.reduce((w, r) => Math.max(w, String(r.errors + r.warnings + r.info).length), 0);
8395
+ const labels = input.breakdown.rows.map((r) => labelForRule(r.rule));
8396
+ const maxLabelWidth = labels.reduce((w, l) => Math.max(w, l.length), 0);
8397
+ for (let i = 0; i < input.breakdown.rows.length; i++) {
8398
+ const row = input.breakdown.rows[i];
8399
+ const total = row.errors + row.warnings + row.info;
8400
+ const count = String(total).padStart(maxCountWidth);
8401
+ const label = padEnd(labels[i], maxLabelWidth);
8402
+ const tags = [];
8403
+ if (row.errors > 0) tags.push(style(t, "danger", `${row.errors} err`));
8404
+ if (row.warnings > 0) tags.push(style(t, "warn", `${row.warnings} warn`));
8405
+ if (row.info > 0) tags.push(style(t, "muted", `${row.info} info`));
8406
+ if (row.fixable > 0) tags.push(style(t, "success", `${row.fixable} fix`));
8407
+ const tagBlock = tags.length > 0 ? ` ${style(t, "muted", "·")} ${tags.join(" ")}` : "";
8408
+ const ruleHint = style(t, "muted", `(${row.rule})`);
8409
+ lines.push(` ${style(t, "muted", count)} ${label} ${ruleHint}${tagBlock}`);
8410
+ }
8411
+ if (input.breakdown.hiddenRules > 0) {
8412
+ const hiddenParts = [];
8413
+ if (input.breakdown.hiddenErrors > 0) hiddenParts.push(`${input.breakdown.hiddenErrors} error${input.breakdown.hiddenErrors === 1 ? "" : "s"}`);
8414
+ if (input.breakdown.hiddenWarnings > 0) hiddenParts.push(`${input.breakdown.hiddenWarnings} warning${input.breakdown.hiddenWarnings === 1 ? "" : "s"}`);
8415
+ const detail = hiddenParts.length > 0 ? ` (${hiddenParts.join(", ")})` : "";
8416
+ lines.push(style(t, "muted", ` +${input.breakdown.hiddenRules} more rule${input.breakdown.hiddenRules === 1 ? "" : "s"}${detail}. Run with -v for the full list.`));
8417
+ }
8418
+ lines.push("");
8419
+ }
7985
8420
  if (input.nextSteps.length > 0) {
7986
8421
  for (const step of input.nextSteps) {
7987
8422
  const glyph = step.emphasis === "primary" ? s.hint : s.bullet;
@@ -8008,6 +8443,39 @@ const renderCleanRun = (input, deps = {}) => {
8008
8443
  //#region src/commands/scan.ts
8009
8444
  const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
8010
8445
  const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
8446
+ const BREAKDOWN_TOP_N = 10;
8447
+ const computeBreakdown = (diagnostics) => {
8448
+ const byRule = /* @__PURE__ */ new Map();
8449
+ for (const d of diagnostics) {
8450
+ const row = byRule.get(d.rule) ?? {
8451
+ rule: d.rule,
8452
+ errors: 0,
8453
+ warnings: 0,
8454
+ info: 0,
8455
+ fixable: 0
8456
+ };
8457
+ if (d.severity === "error") row.errors++;
8458
+ else if (d.severity === "warning") row.warnings++;
8459
+ else row.info++;
8460
+ if (d.fixable) row.fixable++;
8461
+ byRule.set(d.rule, row);
8462
+ }
8463
+ const sorted = [...byRule.values()].sort((a, b) => {
8464
+ const aTotal = a.errors + a.warnings + a.info;
8465
+ const bTotal = b.errors + b.warnings + b.info;
8466
+ if (aTotal !== bTotal) return bTotal - aTotal;
8467
+ if (a.errors !== b.errors) return b.errors - a.errors;
8468
+ return a.rule.localeCompare(b.rule);
8469
+ });
8470
+ const rows = sorted.slice(0, BREAKDOWN_TOP_N);
8471
+ const hidden = sorted.slice(BREAKDOWN_TOP_N);
8472
+ return {
8473
+ rows,
8474
+ hiddenRules: hidden.length,
8475
+ hiddenErrors: hidden.reduce((acc, r) => acc + r.errors, 0),
8476
+ hiddenWarnings: hidden.reduce((acc, r) => acc + r.warnings, 0)
8477
+ };
8478
+ };
8011
8479
  const buildScanRender = (input) => {
8012
8480
  const deps = {
8013
8481
  theme: createTheme(),
@@ -8057,6 +8525,7 @@ const buildScanRender = (input) => {
8057
8525
  engines: input.results.length,
8058
8526
  elapsedMs: input.elapsedMs,
8059
8527
  nextSteps,
8528
+ breakdown: computeBreakdown(input.diagnostics),
8060
8529
  thresholds: input.thresholds
8061
8530
  }, deps)}`;
8062
8531
  };
@@ -10383,10 +10852,18 @@ const promptForConfigChoices = async () => {
10383
10852
  return {
10384
10853
  engines: enginesSelection,
10385
10854
  failBelow: Number(failBelowRaw),
10855
+ typecheck: DEFAULT_CONFIG.lint.typecheck,
10386
10856
  telemetryEnabled: telemetryChoice === "enabled",
10387
10857
  writeGithubWorkflow: workflowChoice === "yes"
10388
10858
  };
10389
10859
  };
10860
+ const strictChoices = () => ({
10861
+ engines: Object.keys(DEFAULT_CONFIG.engines),
10862
+ failBelow: 85,
10863
+ typecheck: true,
10864
+ telemetryEnabled: DEFAULT_CONFIG.telemetry.enabled,
10865
+ writeGithubWorkflow: true
10866
+ });
10390
10867
  const writeAislopConfig = (configDir, configPath, choices) => {
10391
10868
  const selected = new Set(choices.engines);
10392
10869
  const engines = {
@@ -10401,6 +10878,7 @@ const writeAislopConfig = (configDir, configPath, choices) => {
10401
10878
  version: DEFAULT_CONFIG.version,
10402
10879
  engines,
10403
10880
  quality: { ...DEFAULT_CONFIG.quality },
10881
+ lint: { typecheck: choices.typecheck },
10404
10882
  security: { ...DEFAULT_CONFIG.security },
10405
10883
  scoring: {
10406
10884
  weights: { ...DEFAULT_CONFIG.scoring.weights },
@@ -10453,7 +10931,7 @@ const initCommand = async (directory, options = {}) => {
10453
10931
  return;
10454
10932
  }
10455
10933
  }
10456
- const choices = await promptForConfigChoices();
10934
+ const choices = options.strict ? strictChoices() : await promptForConfigChoices();
10457
10935
  if (!choices) return;
10458
10936
  writeAislopConfig(configDir, configPath, choices);
10459
10937
  const steps = [{
@@ -10757,29 +11235,31 @@ const fireInstalledOnce = () => {
10757
11235
  config: loadConfig(process.cwd()).telemetry
10758
11236
  });
10759
11237
  };
10760
- const excludeParser = (value, previous = []) => {
11238
+ const commaSeparatedParser = (value, previous = []) => {
10761
11239
  const parts = value.split(",").map((v) => v.trim()).filter(Boolean);
10762
11240
  return [...previous, ...parts];
10763
11241
  };
10764
11242
  const runScan = async (directory, flags) => {
10765
11243
  const config = loadConfig(directory);
10766
- const { exitCode } = await scanCommand(directory, flags.exclude?.length ? {
11244
+ const { exitCode } = await scanCommand(directory, {
10767
11245
  ...config,
10768
- exclude: [...config.exclude ?? [], ...flags.exclude]
10769
- } : config, {
11246
+ exclude: [...config.exclude ?? [], ...flags.exclude ?? []],
11247
+ include: [...config.include ?? [], ...flags.include ?? []]
11248
+ }, {
10770
11249
  changes: Boolean(flags.changes),
10771
11250
  staged: Boolean(flags.staged),
10772
11251
  verbose: Boolean(flags.verbose),
10773
11252
  json: Boolean(flags.json),
10774
- exclude: flags.exclude
11253
+ exclude: flags.exclude,
11254
+ include: flags.include
10775
11255
  });
10776
11256
  if (exitCode !== 0) {
10777
11257
  await flushTelemetry();
10778
- process.exit(exitCode);
11258
+ process.exitCode = exitCode;
10779
11259
  }
10780
11260
  };
10781
- const noFlagsPassed = (flags) => !flags.changes && !flags.staged && !flags.verbose && !flags.json && !(flags.exclude && flags.exclude.length > 0);
10782
- const program = new Command().name("aislop").description("The unified code quality CLI").version(APP_VERSION, "-v, --version").argument("[directory]", "project directory to scan", ".").option("--changes", "only scan changed files (git diff)").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON instead of terminal UI").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", excludeParser, []).action(async (directory, flags) => {
11261
+ const noFlagsPassed = (flags) => !flags.changes && !flags.staged && !flags.verbose && !flags.json && !(flags.exclude && flags.exclude.length > 0) && !(flags.include && flags.include.length > 0);
11262
+ const program = new Command().name("aislop").description("The unified code quality CLI").version(APP_VERSION, "-v, --version").argument("[directory]", "project directory to scan", ".").option("--changes", "only scan changed files (git diff)").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON instead of terminal UI").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory, flags) => {
10783
11263
  if (noFlagsPassed(flags) && process.stdin.isTTY) try {
10784
11264
  await interactiveCommand(directory, loadConfig(directory));
10785
11265
  return;
@@ -10815,7 +11295,7 @@ ${style(theme, "dim", "Examples:")}
10815
11295
  npx aislop scan --exclude node_modules --exclude dist --exclude **/*.ts
10816
11296
  ${renderHintLine("Run npx aislop scan to scan your project").trimEnd()}
10817
11297
  `);
10818
- program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", excludeParser, []).action(async (directory = ".", _flags, command) => {
11298
+ program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory = ".", _flags, command) => {
10819
11299
  await runScan(directory, command.optsWithGlobals());
10820
11300
  });
10821
11301
  const FIX_AGENT_FLAGS = [
@@ -10904,12 +11384,13 @@ fixProgram.action(async (directory = ".", _flags, command) => {
10904
11384
  agent: matchFixAgent(flags)
10905
11385
  });
10906
11386
  });
10907
- program.command("init [directory]").description("Initialize aislop config in project").action(async (directory = ".") => {
11387
+ program.command("init [directory]").description("Initialize aislop config in project").option("--strict", "write an enterprise-grade default config: all engines, typecheck on, CI failBelow 85, workflow included").action(async (directory = ".", _flags, command) => {
11388
+ const flags = command.optsWithGlobals();
10908
11389
  await withCommandLifecycle({
10909
11390
  command: "init",
10910
11391
  config: loadConfig(directory).telemetry
10911
11392
  }, async () => {
10912
- await initCommand(directory);
11393
+ await initCommand(directory, { strict: Boolean(flags.strict) });
10913
11394
  return { exitCode: 0 };
10914
11395
  });
10915
11396
  });
@@ -10927,7 +11408,7 @@ program.command("ci [directory]").description("CI-friendly JSON output with exit
10927
11408
  const { exitCode } = await ciCommand(directory, loadConfig(directory), { human: Boolean(flags.human) });
10928
11409
  if (exitCode !== 0) {
10929
11410
  await flushTelemetry();
10930
- process.exit(exitCode);
11411
+ process.exitCode = exitCode;
10931
11412
  }
10932
11413
  });
10933
11414
  program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
@@ -10955,7 +11436,8 @@ program.command("badge [directory]").description("Print the public score badge U
10955
11436
  return { exitCode: 0 };
10956
11437
  });
10957
11438
  } catch (err) {
10958
- process.stderr.write(`${err?.message ?? "Failed to print badge"}\n`);
11439
+ const message = err instanceof Error ? err.message : "Failed to print badge";
11440
+ process.stderr.write(`${message}\n`);
10959
11441
  process.exit(1);
10960
11442
  }
10961
11443
  });