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/README.md +1 -0
- package/dist/cli.js +662 -180
- package/dist/index.d.ts +4 -0
- package/dist/index.js +615 -164
- package/dist/{json-DZfGz2xa.js → json-DZHn6AE3.js} +1 -1
- package/dist/mcp.js +422 -148
- package/dist/{version-D_rqBdyj.js → version-C3JZkQGA.js} +1 -1
- package/package.json +2 -1
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.
|
|
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
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
|
1968
|
+
if (!opts) return;
|
|
1826
1969
|
const paths = opts.paths;
|
|
1827
|
-
if (
|
|
1828
|
-
|
|
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
|
|
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
|
|
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 = (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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]
|
|
4375
|
+
const filePath = getRuffDiagnosticPath(rootDir, match[1]);
|
|
4231
4376
|
diagnostics.push({
|
|
4232
|
-
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
5072
|
-
for (const [key, advisory] of Object.entries(advisories))
|
|
5073
|
-
|
|
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
|
|
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 =
|
|
5098
|
-
if (fixAvailable === false) recommendation = isDirect ? "
|
|
5099
|
-
else if (!isDirect && fixAvailable === true) recommendation = "
|
|
5389
|
+
let recommendation = "";
|
|
5390
|
+
if (fixAvailable === false) recommendation = isDirect ? "no automatic fix" : "transitive — needs 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 = `
|
|
5394
|
+
if (target.name && target.version) recommendation = `upgrade to ${target.name}@${target.version}`;
|
|
5103
5395
|
}
|
|
5104
|
-
|
|
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
|
|
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
|
|
7749
|
-
return Math.min(raw,
|
|
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
|
-
|
|
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
|
-
|
|
8101
|
+
const maxRules = verbose ? Infinity : 40;
|
|
8102
|
+
for (const [, ruleDiags] of sorted.slice(0, maxRules)) {
|
|
7761
8103
|
const first = ruleDiags[0];
|
|
7762
|
-
|
|
7763
|
-
|
|
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
|
|
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,
|
|
11244
|
+
const { exitCode } = await scanCommand(directory, {
|
|
10767
11245
|
...config,
|
|
10768
|
-
exclude: [...config.exclude ?? [], ...flags.exclude]
|
|
10769
|
-
|
|
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.
|
|
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",
|
|
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",
|
|
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.
|
|
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
|
-
|
|
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
|
});
|