aislop 0.9.5 → 0.10.0
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 +120 -62
- package/dist/index.js +122 -64
- package/dist/{json-CZU3lEfE.js → json-DaFOYHcf.js} +1 -1
- package/dist/mcp.js +120 -62
- package/dist/{sarif-Cneulb6L.js → sarif-BtSQ92c6.js} +1 -1
- package/dist/version-DYg_ShBx.js +5 -0
- package/package.json +1 -1
- package/dist/version-ls3wZmOU.js +0 -5
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.
|
|
37
|
+
const APP_VERSION = "0.10.0";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/telemetry/env.ts
|
|
@@ -1111,14 +1111,16 @@ const THIN_WRAPPER_PATTERNS = [
|
|
|
1111
1111
|
const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
|
|
1112
1112
|
const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
|
|
1113
1113
|
const DUNDER_PATTERN = /^__\w+__$/;
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1114
|
+
const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
|
|
1115
|
+
const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
|
|
1116
|
+
const isIdentityForward = (matchText) => {
|
|
1117
|
+
const paramsMatch = matchText.match(/\(([^)]*)\)/);
|
|
1118
|
+
const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
|
|
1119
|
+
if (!paramsMatch || !innerMatch) return false;
|
|
1120
|
+
const params = paramNames(paramsMatch[1]);
|
|
1121
|
+
const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
|
|
1122
|
+
if (args.length === 0) return false;
|
|
1123
|
+
return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
|
|
1122
1124
|
};
|
|
1123
1125
|
const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
|
|
1124
1126
|
const detectThinWrappers = (content, relativePath, ext) => {
|
|
@@ -1138,7 +1140,7 @@ const detectThinWrappers = (content, relativePath, ext) => {
|
|
|
1138
1140
|
const prevLine = lines[lineNumber - 2]?.trim();
|
|
1139
1141
|
if (prevLine && prevLine.startsWith("@")) continue;
|
|
1140
1142
|
}
|
|
1141
|
-
if (
|
|
1143
|
+
if (!isIdentityForward(matchText)) continue;
|
|
1142
1144
|
if (isUseContextWrapper(matchText)) continue;
|
|
1143
1145
|
diagnostics.push({
|
|
1144
1146
|
filePath: relativePath,
|
|
@@ -1223,8 +1225,7 @@ const JUSTIFICATION_OPENERS = [
|
|
|
1223
1225
|
/^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
|
|
1224
1226
|
];
|
|
1225
1227
|
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
1226
|
-
const
|
|
1227
|
-
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design)\b/i;
|
|
1228
|
+
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design|ideally|however|although|even\s+though|despite|whereas|unfortunately|trade-?off|first\s+need)\b/i;
|
|
1228
1229
|
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
1229
1230
|
"deprecated",
|
|
1230
1231
|
"see",
|
|
@@ -1320,7 +1321,7 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
|
|
|
1320
1321
|
|
|
1321
1322
|
//#endregion
|
|
1322
1323
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
1323
|
-
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1324
|
+
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1324
1325
|
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
|
|
1325
1326
|
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
1326
1327
|
|
|
@@ -1329,7 +1330,7 @@ const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) ||
|
|
|
1329
1330
|
const TRIVIAL_VERB_STEMS = "Import|Defin|Initializ|Setting|Set\\s+up|Setup|Return|Check|Loop|Iterat|Creat|Updat|Delet|Remov|Handl|Get|Fetch|Increment|Decrement|Writ|Runn|Run|Pars|Execut|Extract|Sav|Load|Build|Start|Stopp|Stop|Clean(?:up|\\s+up)?|Configur|Validat|Process|Queue|Fire|Emit|Dispatch|Log|Print|Render";
|
|
1330
1331
|
const TRIVIAL_JS_COMMENT_PATTERNS = [/\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i, new RegExp(`\\/\\/\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
1331
1332
|
const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, new RegExp(`^#\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
1332
|
-
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
|
|
1333
|
+
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?|if|when|unless|until|only|except|otherwise|needs?|must|should|ensure|avoid|prevent|requires?)\b/i;
|
|
1333
1334
|
const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
1334
1335
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
1335
1336
|
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
@@ -1478,6 +1479,7 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
|
|
|
1478
1479
|
"PLACEHOLDER",
|
|
1479
1480
|
"STUB"
|
|
1480
1481
|
].join("|")})[:\\s]`);
|
|
1482
|
+
const TODO_TRACKING_RE = /https?:\/\/|#\d+|\bgh-\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|\b(?:issue|ticket|jira)\b/i;
|
|
1481
1483
|
const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
|
|
1482
1484
|
const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
1483
1485
|
const contextLines = [];
|
|
@@ -1497,7 +1499,10 @@ const detectTodoStubs = (content, relativePath) => {
|
|
|
1497
1499
|
for (let i = 0; i < lines.length; i++) {
|
|
1498
1500
|
const trimmed = lines[i].trim();
|
|
1499
1501
|
if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
|
|
1500
|
-
if (TODO_PATTERN.test(trimmed))
|
|
1502
|
+
if (TODO_PATTERN.test(trimmed)) {
|
|
1503
|
+
if (TODO_TRACKING_RE.test(trimmed)) continue;
|
|
1504
|
+
diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
|
|
1505
|
+
}
|
|
1501
1506
|
}
|
|
1502
1507
|
return diagnostics;
|
|
1503
1508
|
};
|
|
@@ -2011,11 +2016,42 @@ const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|li
|
|
|
2011
2016
|
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
2012
2017
|
const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
|
|
2013
2018
|
const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
|
|
2019
|
+
const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
|
|
2014
2020
|
const PLACEHOLDER_HOSTS = new Set([
|
|
2015
2021
|
"example.com",
|
|
2016
2022
|
"example.org",
|
|
2017
2023
|
"example.net"
|
|
2018
2024
|
]);
|
|
2025
|
+
const LOOPBACK_HOSTS = new Set([
|
|
2026
|
+
"localhost",
|
|
2027
|
+
"127.0.0.1",
|
|
2028
|
+
"0.0.0.0",
|
|
2029
|
+
"::1"
|
|
2030
|
+
]);
|
|
2031
|
+
const VENDOR_API_DOMAINS = [
|
|
2032
|
+
"github.com",
|
|
2033
|
+
"githubusercontent.com",
|
|
2034
|
+
"googleapis.com",
|
|
2035
|
+
"accounts.google.com",
|
|
2036
|
+
"stripe.com",
|
|
2037
|
+
"openai.com",
|
|
2038
|
+
"anthropic.com",
|
|
2039
|
+
"slack.com",
|
|
2040
|
+
"twilio.com",
|
|
2041
|
+
"sendgrid.com",
|
|
2042
|
+
"mailgun.net",
|
|
2043
|
+
"cloudflare.com",
|
|
2044
|
+
"discord.com",
|
|
2045
|
+
"telegram.org",
|
|
2046
|
+
"login.microsoftonline.com",
|
|
2047
|
+
"graph.microsoft.com",
|
|
2048
|
+
"twitter.com",
|
|
2049
|
+
"x.com",
|
|
2050
|
+
"twimg.com",
|
|
2051
|
+
"t.co",
|
|
2052
|
+
"api.telegram.org"
|
|
2053
|
+
];
|
|
2054
|
+
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
2019
2055
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2020
2056
|
const HARDCODED_URL_FINDING = {
|
|
2021
2057
|
rule: "ai-slop/hardcoded-url",
|
|
@@ -2059,14 +2095,18 @@ const shouldFlagUrlLiteral = (line, urlText) => {
|
|
|
2059
2095
|
const host = safeUrlHost(urlText);
|
|
2060
2096
|
if (!host) return false;
|
|
2061
2097
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2098
|
+
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
2099
|
+
if (isVendorApiHost(host)) return false;
|
|
2062
2100
|
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2063
2101
|
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2064
2102
|
};
|
|
2103
|
+
const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
|
|
2065
2104
|
const hasUsefulIdShape = (value) => {
|
|
2066
2105
|
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
2106
|
+
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
2067
2107
|
if (/^https?:\/\//i.test(value)) return false;
|
|
2068
2108
|
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
2069
|
-
return /[0-
|
|
2109
|
+
return /[0-9]/.test(value);
|
|
2070
2110
|
};
|
|
2071
2111
|
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
2072
2112
|
const diagnostics = [];
|
|
@@ -2093,6 +2133,7 @@ const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
|
2093
2133
|
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
2094
2134
|
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
2095
2135
|
if (isNonProductionPath(relativePath)) return [];
|
|
2136
|
+
if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
|
|
2096
2137
|
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
2097
2138
|
};
|
|
2098
2139
|
const detectHardcodedConfigLiterals = async (context) => {
|
|
@@ -2240,7 +2281,9 @@ const PYTHON_STDLIB = new Set([
|
|
|
2240
2281
|
"builtins",
|
|
2241
2282
|
"bz2",
|
|
2242
2283
|
"calendar",
|
|
2284
|
+
"code",
|
|
2243
2285
|
"codecs",
|
|
2286
|
+
"codeop",
|
|
2244
2287
|
"collections",
|
|
2245
2288
|
"concurrent",
|
|
2246
2289
|
"configparser",
|
|
@@ -2313,6 +2356,7 @@ const PYTHON_STDLIB = new Set([
|
|
|
2313
2356
|
"readline",
|
|
2314
2357
|
"reprlib",
|
|
2315
2358
|
"resource",
|
|
2359
|
+
"rlcompleter",
|
|
2316
2360
|
"secrets",
|
|
2317
2361
|
"select",
|
|
2318
2362
|
"selectors",
|
|
@@ -2411,6 +2455,7 @@ const PYTHON_IMPORT_TO_PIP = {
|
|
|
2411
2455
|
pptx: ["python-pptx"],
|
|
2412
2456
|
git: ["gitpython"],
|
|
2413
2457
|
socks: ["pysocks"],
|
|
2458
|
+
psycopg2: ["psycopg2-binary", "psycopg2"],
|
|
2414
2459
|
redis: ["redis"],
|
|
2415
2460
|
cairo: ["pycairo"],
|
|
2416
2461
|
serial: ["pyserial"],
|
|
@@ -2500,6 +2545,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
2500
2545
|
}
|
|
2501
2546
|
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
2502
2547
|
if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2548
|
+
const groups = content.match(/\[dependency-groups\]([\s\S]*?)(?=\n\[[^[]|$)/);
|
|
2549
|
+
if (groups) for (const m of groups[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2503
2550
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
2504
2551
|
let match = poetryRe.exec(content);
|
|
2505
2552
|
while (match !== null) {
|
|
@@ -2732,9 +2779,28 @@ const extractJsImports = (content) => {
|
|
|
2732
2779
|
const extractPyImports = (content) => {
|
|
2733
2780
|
const lines = content.split("\n");
|
|
2734
2781
|
const results = [];
|
|
2782
|
+
let inDoc = null;
|
|
2783
|
+
let typeCheckIndent = -1;
|
|
2735
2784
|
for (let i = 0; i < lines.length; i++) {
|
|
2736
|
-
const
|
|
2737
|
-
|
|
2785
|
+
const raw = lines[i];
|
|
2786
|
+
const line = raw.trim();
|
|
2787
|
+
if (inDoc) {
|
|
2788
|
+
if (line.includes(inDoc)) inDoc = null;
|
|
2789
|
+
continue;
|
|
2790
|
+
}
|
|
2791
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
2792
|
+
const triples = line.match(/"""|'''/g);
|
|
2793
|
+
if (triples) {
|
|
2794
|
+
if (triples.length % 2 === 1) inDoc = triples[triples.length - 1];
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2797
|
+
const indent = raw.length - raw.trimStart().length;
|
|
2798
|
+
if (typeCheckIndent >= 0 && indent <= typeCheckIndent) typeCheckIndent = -1;
|
|
2799
|
+
if (/^if\s+(?:[\w.]+\.)?TYPE_CHECKING\b/.test(line)) {
|
|
2800
|
+
typeCheckIndent = indent;
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
if (typeCheckIndent >= 0) continue;
|
|
2738
2804
|
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
2739
2805
|
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
2740
2806
|
results.push({
|
|
@@ -2744,8 +2810,8 @@ const extractPyImports = (content) => {
|
|
|
2744
2810
|
continue;
|
|
2745
2811
|
}
|
|
2746
2812
|
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
2747
|
-
if (importMatch) for (const
|
|
2748
|
-
const cleaned =
|
|
2813
|
+
if (importMatch) for (const part of importMatch[1].split(",")) {
|
|
2814
|
+
const cleaned = part.trim().split(/\s+as\s+/)[0];
|
|
2749
2815
|
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
2750
2816
|
spec: cleaned,
|
|
2751
2817
|
line: i + 1
|
|
@@ -2802,6 +2868,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2802
2868
|
continue;
|
|
2803
2869
|
}
|
|
2804
2870
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2871
|
+
if (isNonProductionPath(relPath)) continue;
|
|
2805
2872
|
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2806
2873
|
for (const { spec, line } of imports) {
|
|
2807
2874
|
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
@@ -2929,7 +2996,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
2929
2996
|
//#endregion
|
|
2930
2997
|
//#region src/engines/ai-slop/meta-comment.ts
|
|
2931
2998
|
const PLAN_REFERENCE_RES = [
|
|
2932
|
-
|
|
2999
|
+
/^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
|
|
2933
3000
|
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
2934
3001
|
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
2935
3002
|
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
@@ -3031,24 +3098,6 @@ const looksLikeLicenseHeader = (block) => {
|
|
|
3031
3098
|
const text = block.rawLines.join(" ").toLowerCase();
|
|
3032
3099
|
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
3033
3100
|
};
|
|
3034
|
-
const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
|
|
3035
|
-
const isBareSectionLabel = (prose) => {
|
|
3036
|
-
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
3037
|
-
if (prose.endsWith(".")) return false;
|
|
3038
|
-
if (prose.split(/\s+/).length > 3) return false;
|
|
3039
|
-
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
3040
|
-
return true;
|
|
3041
|
-
};
|
|
3042
|
-
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
3043
|
-
const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
3044
|
-
if (nextLine === null) return false;
|
|
3045
|
-
if (!DATA_ENTRY_START.test(nextLine)) return false;
|
|
3046
|
-
const trimmed = nextLine.trim();
|
|
3047
|
-
if (trimmed.startsWith("case ")) return true;
|
|
3048
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
|
|
3049
|
-
if (/^\w+\s*:/.test(trimmed)) return true;
|
|
3050
|
-
return false;
|
|
3051
|
-
};
|
|
3052
3101
|
const looksLikeSuppressDirective = (block) => block.rawLines.some((l) => /\b(biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(l));
|
|
3053
3102
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
3054
3103
|
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
@@ -3137,10 +3186,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3137
3186
|
matched: true,
|
|
3138
3187
|
reason: "phase/section header"
|
|
3139
3188
|
};
|
|
3140
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
3141
|
-
matched: true,
|
|
3142
|
-
reason: "bare section label"
|
|
3143
|
-
};
|
|
3144
3189
|
const joined = block.prose.join(" ");
|
|
3145
3190
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
3146
3191
|
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
@@ -3171,17 +3216,11 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3171
3216
|
};
|
|
3172
3217
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
3173
3218
|
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
3174
|
-
if (nonEmptyProseCount >= 5) {
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
return {
|
|
3180
|
-
matched: true,
|
|
3181
|
-
reason: "long narrative block"
|
|
3182
|
-
};
|
|
3183
|
-
}
|
|
3184
|
-
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
|
|
3219
|
+
if (nonEmptyProseCount >= 5 && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
3220
|
+
matched: true,
|
|
3221
|
+
reason: "long narrative block"
|
|
3222
|
+
};
|
|
3223
|
+
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
3185
3224
|
matched: true,
|
|
3186
3225
|
reason: "multi-line narrative prose"
|
|
3187
3226
|
};
|
|
@@ -3623,7 +3662,14 @@ const JS_EXTS$1 = new Set([
|
|
|
3623
3662
|
".mjs",
|
|
3624
3663
|
".cjs"
|
|
3625
3664
|
]);
|
|
3626
|
-
const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
|
|
3665
|
+
const CATCH_HEAD_RE = /\bcatch\s*(?:\(\s*([^)]*?)\s*\))?\s*\{/g;
|
|
3666
|
+
const isIdentifier = (s) => /^[A-Za-z_$][\w$]*$/.test(s);
|
|
3667
|
+
const recoveryDropsError = (binding, body) => {
|
|
3668
|
+
const name = binding?.trim() ?? "";
|
|
3669
|
+
if (name === "") return true;
|
|
3670
|
+
if (!isIdentifier(name)) return false;
|
|
3671
|
+
return !new RegExp(`\\b${name}\\b`).test(body);
|
|
3672
|
+
};
|
|
3627
3673
|
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3628
3674
|
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3629
3675
|
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -3673,14 +3719,15 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3673
3719
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3674
3720
|
if (body === null) continue;
|
|
3675
3721
|
if (!isLogOnlyBody(body)) continue;
|
|
3722
|
+
if (!recoveryDropsError(match[1], body)) continue;
|
|
3676
3723
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3677
3724
|
out.push({
|
|
3678
3725
|
filePath: relPath,
|
|
3679
3726
|
engine: "ai-slop",
|
|
3680
3727
|
rule: "ai-slop/silent-recovery",
|
|
3681
3728
|
severity: "warning",
|
|
3682
|
-
message: "Catch
|
|
3683
|
-
help: "
|
|
3729
|
+
message: "Catch logs without the caught error then continues; the failure cause is lost",
|
|
3730
|
+
help: "Include the caught error in the log, or rethrow / recover explicitly, so the failure stays diagnosable.",
|
|
3684
3731
|
line,
|
|
3685
3732
|
column: 0,
|
|
3686
3733
|
category: "AI Slop",
|
|
@@ -3690,6 +3737,7 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3690
3737
|
return out;
|
|
3691
3738
|
};
|
|
3692
3739
|
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3740
|
+
const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
|
|
3693
3741
|
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3694
3742
|
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3695
3743
|
const detectPySilentRecovery = (content, relPath) => {
|
|
@@ -3713,13 +3761,14 @@ const detectPySilentRecovery = (content, relPath) => {
|
|
|
3713
3761
|
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3714
3762
|
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3715
3763
|
if (!allLogs || !sawLog) continue;
|
|
3764
|
+
if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
|
|
3716
3765
|
out.push({
|
|
3717
3766
|
filePath: relPath,
|
|
3718
3767
|
engine: "ai-slop",
|
|
3719
3768
|
rule: "ai-slop/silent-recovery",
|
|
3720
3769
|
severity: "warning",
|
|
3721
|
-
message: "except
|
|
3722
|
-
help: "
|
|
3770
|
+
message: "except logs without the caught error then continues; the failure cause is lost",
|
|
3771
|
+
help: "Include the caught error in the log, or re-raise / recover explicitly, so the failure stays diagnosable.",
|
|
3723
3772
|
line: i + 1,
|
|
3724
3773
|
column: 0,
|
|
3725
3774
|
category: "AI Slop",
|
|
@@ -3822,10 +3871,11 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3822
3871
|
const importLines = /* @__PURE__ */ new Set();
|
|
3823
3872
|
for (let i = 0; i < lines.length; i++) {
|
|
3824
3873
|
const trimmed = lines[i].trim();
|
|
3825
|
-
const fromMatch = trimmed.match(/^from\s+[\w.]
|
|
3874
|
+
const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
|
|
3826
3875
|
if (fromMatch) {
|
|
3827
3876
|
importLines.add(i);
|
|
3828
|
-
|
|
3877
|
+
if (fromMatch[1] === "__future__") continue;
|
|
3878
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
3829
3879
|
if (importPart === "*") continue;
|
|
3830
3880
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
3831
3881
|
for (const item of cleaned.split(",")) {
|
|
@@ -7147,6 +7197,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
7147
7197
|
//#endregion
|
|
7148
7198
|
//#region src/scoring/index.ts
|
|
7149
7199
|
const PERFECT_SCORE = 100;
|
|
7200
|
+
const STYLE_RULES = new Set([
|
|
7201
|
+
"ai-slop/trivial-comment",
|
|
7202
|
+
"ai-slop/narrative-comment",
|
|
7203
|
+
"complexity/file-too-large",
|
|
7204
|
+
"complexity/function-too-long"
|
|
7205
|
+
]);
|
|
7206
|
+
const STYLE_WEIGHT = .5;
|
|
7150
7207
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
7151
7208
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
7152
7209
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
@@ -7161,7 +7218,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
7161
7218
|
for (const d of diagnostics) {
|
|
7162
7219
|
const engineWeight = weights[d.engine] ?? 1;
|
|
7163
7220
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
7164
|
-
|
|
7221
|
+
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
7222
|
+
deductions += severityPenalty * engineWeight * styleFactor;
|
|
7165
7223
|
}
|
|
7166
7224
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
7167
7225
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as getEngineLabel, t as ENGINE_INFO } from "./engine-info-DCvIfZ0f.js";
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
|
|
3
|
-
import { t as APP_VERSION } from "./version-
|
|
3
|
+
import { t as APP_VERSION } from "./version-DYg_ShBx.js";
|
|
4
4
|
import { r as runGenericLinter, t as fixRubyLint } from "./generic-D_T4cUaC.js";
|
|
5
5
|
import { n as runExpoDoctor } from "./expo-doctor-BcIkOte5.js";
|
|
6
6
|
import { createRequire, isBuiltin } from "node:module";
|
|
@@ -1280,14 +1280,16 @@ const THIN_WRAPPER_PATTERNS = [
|
|
|
1280
1280
|
const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
|
|
1281
1281
|
const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
|
|
1282
1282
|
const DUNDER_PATTERN = /^__\w+__$/;
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1283
|
+
const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
|
|
1284
|
+
const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
|
|
1285
|
+
const isIdentityForward = (matchText) => {
|
|
1286
|
+
const paramsMatch = matchText.match(/\(([^)]*)\)/);
|
|
1287
|
+
const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
|
|
1288
|
+
if (!paramsMatch || !innerMatch) return false;
|
|
1289
|
+
const params = paramNames(paramsMatch[1]);
|
|
1290
|
+
const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
|
|
1291
|
+
if (args.length === 0) return false;
|
|
1292
|
+
return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
|
|
1291
1293
|
};
|
|
1292
1294
|
const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
|
|
1293
1295
|
const detectThinWrappers = (content, relativePath, ext) => {
|
|
@@ -1307,7 +1309,7 @@ const detectThinWrappers = (content, relativePath, ext) => {
|
|
|
1307
1309
|
const prevLine = lines[lineNumber - 2]?.trim();
|
|
1308
1310
|
if (prevLine && prevLine.startsWith("@")) continue;
|
|
1309
1311
|
}
|
|
1310
|
-
if (
|
|
1312
|
+
if (!isIdentityForward(matchText)) continue;
|
|
1311
1313
|
if (isUseContextWrapper(matchText)) continue;
|
|
1312
1314
|
diagnostics.push({
|
|
1313
1315
|
filePath: relativePath,
|
|
@@ -1392,8 +1394,7 @@ const JUSTIFICATION_OPENERS = [
|
|
|
1392
1394
|
/^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
|
|
1393
1395
|
];
|
|
1394
1396
|
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
1395
|
-
const
|
|
1396
|
-
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design)\b/i;
|
|
1397
|
+
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design|ideally|however|although|even\s+though|despite|whereas|unfortunately|trade-?off|first\s+need)\b/i;
|
|
1397
1398
|
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
1398
1399
|
"deprecated",
|
|
1399
1400
|
"see",
|
|
@@ -1489,7 +1490,7 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
|
|
|
1489
1490
|
|
|
1490
1491
|
//#endregion
|
|
1491
1492
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
1492
|
-
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1493
|
+
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1493
1494
|
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
|
|
1494
1495
|
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
1495
1496
|
|
|
@@ -1498,7 +1499,7 @@ const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) ||
|
|
|
1498
1499
|
const TRIVIAL_VERB_STEMS = "Import|Defin|Initializ|Setting|Set\\s+up|Setup|Return|Check|Loop|Iterat|Creat|Updat|Delet|Remov|Handl|Get|Fetch|Increment|Decrement|Writ|Runn|Run|Pars|Execut|Extract|Sav|Load|Build|Start|Stopp|Stop|Clean(?:up|\\s+up)?|Configur|Validat|Process|Queue|Fire|Emit|Dispatch|Log|Print|Render";
|
|
1499
1500
|
const TRIVIAL_JS_COMMENT_PATTERNS = [/\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i, new RegExp(`\\/\\/\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
1500
1501
|
const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, new RegExp(`^#\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
1501
|
-
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
|
|
1502
|
+
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?|if|when|unless|until|only|except|otherwise|needs?|must|should|ensure|avoid|prevent|requires?)\b/i;
|
|
1502
1503
|
const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
1503
1504
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
1504
1505
|
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
@@ -1647,6 +1648,7 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
|
|
|
1647
1648
|
"PLACEHOLDER",
|
|
1648
1649
|
"STUB"
|
|
1649
1650
|
].join("|")})[:\\s]`);
|
|
1651
|
+
const TODO_TRACKING_RE = /https?:\/\/|#\d+|\bgh-\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|\b(?:issue|ticket|jira)\b/i;
|
|
1650
1652
|
const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
|
|
1651
1653
|
const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
1652
1654
|
const contextLines = [];
|
|
@@ -1666,7 +1668,10 @@ const detectTodoStubs = (content, relativePath) => {
|
|
|
1666
1668
|
for (let i = 0; i < lines.length; i++) {
|
|
1667
1669
|
const trimmed = lines[i].trim();
|
|
1668
1670
|
if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
|
|
1669
|
-
if (TODO_PATTERN.test(trimmed))
|
|
1671
|
+
if (TODO_PATTERN.test(trimmed)) {
|
|
1672
|
+
if (TODO_TRACKING_RE.test(trimmed)) continue;
|
|
1673
|
+
diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
|
|
1674
|
+
}
|
|
1670
1675
|
}
|
|
1671
1676
|
return diagnostics;
|
|
1672
1677
|
};
|
|
@@ -2180,11 +2185,42 @@ const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|li
|
|
|
2180
2185
|
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
2181
2186
|
const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
|
|
2182
2187
|
const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
|
|
2188
|
+
const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
|
|
2183
2189
|
const PLACEHOLDER_HOSTS = new Set([
|
|
2184
2190
|
"example.com",
|
|
2185
2191
|
"example.org",
|
|
2186
2192
|
"example.net"
|
|
2187
2193
|
]);
|
|
2194
|
+
const LOOPBACK_HOSTS = new Set([
|
|
2195
|
+
"localhost",
|
|
2196
|
+
"127.0.0.1",
|
|
2197
|
+
"0.0.0.0",
|
|
2198
|
+
"::1"
|
|
2199
|
+
]);
|
|
2200
|
+
const VENDOR_API_DOMAINS = [
|
|
2201
|
+
"github.com",
|
|
2202
|
+
"githubusercontent.com",
|
|
2203
|
+
"googleapis.com",
|
|
2204
|
+
"accounts.google.com",
|
|
2205
|
+
"stripe.com",
|
|
2206
|
+
"openai.com",
|
|
2207
|
+
"anthropic.com",
|
|
2208
|
+
"slack.com",
|
|
2209
|
+
"twilio.com",
|
|
2210
|
+
"sendgrid.com",
|
|
2211
|
+
"mailgun.net",
|
|
2212
|
+
"cloudflare.com",
|
|
2213
|
+
"discord.com",
|
|
2214
|
+
"telegram.org",
|
|
2215
|
+
"login.microsoftonline.com",
|
|
2216
|
+
"graph.microsoft.com",
|
|
2217
|
+
"twitter.com",
|
|
2218
|
+
"x.com",
|
|
2219
|
+
"twimg.com",
|
|
2220
|
+
"t.co",
|
|
2221
|
+
"api.telegram.org"
|
|
2222
|
+
];
|
|
2223
|
+
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
2188
2224
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2189
2225
|
const HARDCODED_URL_FINDING = {
|
|
2190
2226
|
rule: "ai-slop/hardcoded-url",
|
|
@@ -2228,14 +2264,18 @@ const shouldFlagUrlLiteral = (line, urlText) => {
|
|
|
2228
2264
|
const host = safeUrlHost(urlText);
|
|
2229
2265
|
if (!host) return false;
|
|
2230
2266
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2267
|
+
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
2268
|
+
if (isVendorApiHost(host)) return false;
|
|
2231
2269
|
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2232
2270
|
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2233
2271
|
};
|
|
2272
|
+
const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
|
|
2234
2273
|
const hasUsefulIdShape = (value) => {
|
|
2235
2274
|
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
2275
|
+
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
2236
2276
|
if (/^https?:\/\//i.test(value)) return false;
|
|
2237
2277
|
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
2238
|
-
return /[0-
|
|
2278
|
+
return /[0-9]/.test(value);
|
|
2239
2279
|
};
|
|
2240
2280
|
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
2241
2281
|
const diagnostics = [];
|
|
@@ -2262,6 +2302,7 @@ const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
|
2262
2302
|
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
2263
2303
|
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
2264
2304
|
if (isNonProductionPath(relativePath)) return [];
|
|
2305
|
+
if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
|
|
2265
2306
|
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
2266
2307
|
};
|
|
2267
2308
|
const detectHardcodedConfigLiterals = async (context) => {
|
|
@@ -2409,7 +2450,9 @@ const PYTHON_STDLIB = new Set([
|
|
|
2409
2450
|
"builtins",
|
|
2410
2451
|
"bz2",
|
|
2411
2452
|
"calendar",
|
|
2453
|
+
"code",
|
|
2412
2454
|
"codecs",
|
|
2455
|
+
"codeop",
|
|
2413
2456
|
"collections",
|
|
2414
2457
|
"concurrent",
|
|
2415
2458
|
"configparser",
|
|
@@ -2482,6 +2525,7 @@ const PYTHON_STDLIB = new Set([
|
|
|
2482
2525
|
"readline",
|
|
2483
2526
|
"reprlib",
|
|
2484
2527
|
"resource",
|
|
2528
|
+
"rlcompleter",
|
|
2485
2529
|
"secrets",
|
|
2486
2530
|
"select",
|
|
2487
2531
|
"selectors",
|
|
@@ -2580,6 +2624,7 @@ const PYTHON_IMPORT_TO_PIP = {
|
|
|
2580
2624
|
pptx: ["python-pptx"],
|
|
2581
2625
|
git: ["gitpython"],
|
|
2582
2626
|
socks: ["pysocks"],
|
|
2627
|
+
psycopg2: ["psycopg2-binary", "psycopg2"],
|
|
2583
2628
|
redis: ["redis"],
|
|
2584
2629
|
cairo: ["pycairo"],
|
|
2585
2630
|
serial: ["pyserial"],
|
|
@@ -2669,6 +2714,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
2669
2714
|
}
|
|
2670
2715
|
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
2671
2716
|
if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2717
|
+
const groups = content.match(/\[dependency-groups\]([\s\S]*?)(?=\n\[[^[]|$)/);
|
|
2718
|
+
if (groups) for (const m of groups[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2672
2719
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
2673
2720
|
let match = poetryRe.exec(content);
|
|
2674
2721
|
while (match !== null) {
|
|
@@ -2901,9 +2948,28 @@ const extractJsImports = (content) => {
|
|
|
2901
2948
|
const extractPyImports = (content) => {
|
|
2902
2949
|
const lines = content.split("\n");
|
|
2903
2950
|
const results = [];
|
|
2951
|
+
let inDoc = null;
|
|
2952
|
+
let typeCheckIndent = -1;
|
|
2904
2953
|
for (let i = 0; i < lines.length; i++) {
|
|
2905
|
-
const
|
|
2906
|
-
|
|
2954
|
+
const raw = lines[i];
|
|
2955
|
+
const line = raw.trim();
|
|
2956
|
+
if (inDoc) {
|
|
2957
|
+
if (line.includes(inDoc)) inDoc = null;
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
2961
|
+
const triples = line.match(/"""|'''/g);
|
|
2962
|
+
if (triples) {
|
|
2963
|
+
if (triples.length % 2 === 1) inDoc = triples[triples.length - 1];
|
|
2964
|
+
continue;
|
|
2965
|
+
}
|
|
2966
|
+
const indent = raw.length - raw.trimStart().length;
|
|
2967
|
+
if (typeCheckIndent >= 0 && indent <= typeCheckIndent) typeCheckIndent = -1;
|
|
2968
|
+
if (/^if\s+(?:[\w.]+\.)?TYPE_CHECKING\b/.test(line)) {
|
|
2969
|
+
typeCheckIndent = indent;
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
if (typeCheckIndent >= 0) continue;
|
|
2907
2973
|
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
2908
2974
|
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
2909
2975
|
results.push({
|
|
@@ -2913,8 +2979,8 @@ const extractPyImports = (content) => {
|
|
|
2913
2979
|
continue;
|
|
2914
2980
|
}
|
|
2915
2981
|
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
2916
|
-
if (importMatch) for (const
|
|
2917
|
-
const cleaned =
|
|
2982
|
+
if (importMatch) for (const part of importMatch[1].split(",")) {
|
|
2983
|
+
const cleaned = part.trim().split(/\s+as\s+/)[0];
|
|
2918
2984
|
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
2919
2985
|
spec: cleaned,
|
|
2920
2986
|
line: i + 1
|
|
@@ -2971,6 +3037,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2971
3037
|
continue;
|
|
2972
3038
|
}
|
|
2973
3039
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3040
|
+
if (isNonProductionPath(relPath)) continue;
|
|
2974
3041
|
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2975
3042
|
for (const { spec, line } of imports) {
|
|
2976
3043
|
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
@@ -3098,7 +3165,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
3098
3165
|
//#endregion
|
|
3099
3166
|
//#region src/engines/ai-slop/meta-comment.ts
|
|
3100
3167
|
const PLAN_REFERENCE_RES = [
|
|
3101
|
-
|
|
3168
|
+
/^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
|
|
3102
3169
|
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
3103
3170
|
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
3104
3171
|
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
@@ -3200,24 +3267,6 @@ const looksLikeLicenseHeader = (block) => {
|
|
|
3200
3267
|
const text = block.rawLines.join(" ").toLowerCase();
|
|
3201
3268
|
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
3202
3269
|
};
|
|
3203
|
-
const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
|
|
3204
|
-
const isBareSectionLabel = (prose) => {
|
|
3205
|
-
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
3206
|
-
if (prose.endsWith(".")) return false;
|
|
3207
|
-
if (prose.split(/\s+/).length > 3) return false;
|
|
3208
|
-
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
3209
|
-
return true;
|
|
3210
|
-
};
|
|
3211
|
-
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
3212
|
-
const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
3213
|
-
if (nextLine === null) return false;
|
|
3214
|
-
if (!DATA_ENTRY_START.test(nextLine)) return false;
|
|
3215
|
-
const trimmed = nextLine.trim();
|
|
3216
|
-
if (trimmed.startsWith("case ")) return true;
|
|
3217
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
|
|
3218
|
-
if (/^\w+\s*:/.test(trimmed)) return true;
|
|
3219
|
-
return false;
|
|
3220
|
-
};
|
|
3221
3270
|
const looksLikeSuppressDirective = (block) => block.rawLines.some((l) => /\b(biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(l));
|
|
3222
3271
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
3223
3272
|
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
@@ -3306,10 +3355,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3306
3355
|
matched: true,
|
|
3307
3356
|
reason: "phase/section header"
|
|
3308
3357
|
};
|
|
3309
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
3310
|
-
matched: true,
|
|
3311
|
-
reason: "bare section label"
|
|
3312
|
-
};
|
|
3313
3358
|
const joined = block.prose.join(" ");
|
|
3314
3359
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
3315
3360
|
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
@@ -3340,17 +3385,11 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3340
3385
|
};
|
|
3341
3386
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
3342
3387
|
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
3343
|
-
if (nonEmptyProseCount >= 5) {
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
return {
|
|
3349
|
-
matched: true,
|
|
3350
|
-
reason: "long narrative block"
|
|
3351
|
-
};
|
|
3352
|
-
}
|
|
3353
|
-
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
|
|
3388
|
+
if (nonEmptyProseCount >= 5 && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
3389
|
+
matched: true,
|
|
3390
|
+
reason: "long narrative block"
|
|
3391
|
+
};
|
|
3392
|
+
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
3354
3393
|
matched: true,
|
|
3355
3394
|
reason: "multi-line narrative prose"
|
|
3356
3395
|
};
|
|
@@ -3792,7 +3831,14 @@ const JS_EXTS$1 = new Set([
|
|
|
3792
3831
|
".mjs",
|
|
3793
3832
|
".cjs"
|
|
3794
3833
|
]);
|
|
3795
|
-
const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
|
|
3834
|
+
const CATCH_HEAD_RE = /\bcatch\s*(?:\(\s*([^)]*?)\s*\))?\s*\{/g;
|
|
3835
|
+
const isIdentifier = (s) => /^[A-Za-z_$][\w$]*$/.test(s);
|
|
3836
|
+
const recoveryDropsError = (binding, body) => {
|
|
3837
|
+
const name = binding?.trim() ?? "";
|
|
3838
|
+
if (name === "") return true;
|
|
3839
|
+
if (!isIdentifier(name)) return false;
|
|
3840
|
+
return !new RegExp(`\\b${name}\\b`).test(body);
|
|
3841
|
+
};
|
|
3796
3842
|
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3797
3843
|
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3798
3844
|
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -3842,14 +3888,15 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3842
3888
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3843
3889
|
if (body === null) continue;
|
|
3844
3890
|
if (!isLogOnlyBody(body)) continue;
|
|
3891
|
+
if (!recoveryDropsError(match[1], body)) continue;
|
|
3845
3892
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3846
3893
|
out.push({
|
|
3847
3894
|
filePath: relPath,
|
|
3848
3895
|
engine: "ai-slop",
|
|
3849
3896
|
rule: "ai-slop/silent-recovery",
|
|
3850
3897
|
severity: "warning",
|
|
3851
|
-
message: "Catch
|
|
3852
|
-
help: "
|
|
3898
|
+
message: "Catch logs without the caught error then continues; the failure cause is lost",
|
|
3899
|
+
help: "Include the caught error in the log, or rethrow / recover explicitly, so the failure stays diagnosable.",
|
|
3853
3900
|
line,
|
|
3854
3901
|
column: 0,
|
|
3855
3902
|
category: "AI Slop",
|
|
@@ -3859,6 +3906,7 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3859
3906
|
return out;
|
|
3860
3907
|
};
|
|
3861
3908
|
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3909
|
+
const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
|
|
3862
3910
|
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3863
3911
|
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3864
3912
|
const detectPySilentRecovery = (content, relPath) => {
|
|
@@ -3882,13 +3930,14 @@ const detectPySilentRecovery = (content, relPath) => {
|
|
|
3882
3930
|
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3883
3931
|
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3884
3932
|
if (!allLogs || !sawLog) continue;
|
|
3933
|
+
if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
|
|
3885
3934
|
out.push({
|
|
3886
3935
|
filePath: relPath,
|
|
3887
3936
|
engine: "ai-slop",
|
|
3888
3937
|
rule: "ai-slop/silent-recovery",
|
|
3889
3938
|
severity: "warning",
|
|
3890
|
-
message: "except
|
|
3891
|
-
help: "
|
|
3939
|
+
message: "except logs without the caught error then continues; the failure cause is lost",
|
|
3940
|
+
help: "Include the caught error in the log, or re-raise / recover explicitly, so the failure stays diagnosable.",
|
|
3892
3941
|
line: i + 1,
|
|
3893
3942
|
column: 0,
|
|
3894
3943
|
category: "AI Slop",
|
|
@@ -3991,10 +4040,11 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3991
4040
|
const importLines = /* @__PURE__ */ new Set();
|
|
3992
4041
|
for (let i = 0; i < lines.length; i++) {
|
|
3993
4042
|
const trimmed = lines[i].trim();
|
|
3994
|
-
const fromMatch = trimmed.match(/^from\s+[\w.]
|
|
4043
|
+
const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
|
|
3995
4044
|
if (fromMatch) {
|
|
3996
4045
|
importLines.add(i);
|
|
3997
|
-
|
|
4046
|
+
if (fromMatch[1] === "__future__") continue;
|
|
4047
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
3998
4048
|
if (importPart === "*") continue;
|
|
3999
4049
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
4000
4050
|
for (const item of cleaned.split(",")) {
|
|
@@ -7106,6 +7156,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
7106
7156
|
//#endregion
|
|
7107
7157
|
//#region src/scoring/index.ts
|
|
7108
7158
|
const PERFECT_SCORE = 100;
|
|
7159
|
+
const STYLE_RULES = new Set([
|
|
7160
|
+
"ai-slop/trivial-comment",
|
|
7161
|
+
"ai-slop/narrative-comment",
|
|
7162
|
+
"complexity/file-too-large",
|
|
7163
|
+
"complexity/function-too-long"
|
|
7164
|
+
]);
|
|
7165
|
+
const STYLE_WEIGHT = .5;
|
|
7109
7166
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
7110
7167
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
7111
7168
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
@@ -7120,7 +7177,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
7120
7177
|
for (const d of diagnostics) {
|
|
7121
7178
|
const engineWeight = weights[d.engine] ?? 1;
|
|
7122
7179
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
7123
|
-
|
|
7180
|
+
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
7181
|
+
deductions += severityPenalty * engineWeight * styleFactor;
|
|
7124
7182
|
}
|
|
7125
7183
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
7126
7184
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
@@ -8301,12 +8359,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
8301
8359
|
engineTimings
|
|
8302
8360
|
};
|
|
8303
8361
|
if (options.sarif) {
|
|
8304
|
-
const { buildSarifLog } = await import("./sarif-
|
|
8362
|
+
const { buildSarifLog } = await import("./sarif-BtSQ92c6.js");
|
|
8305
8363
|
console.log(JSON.stringify(buildSarifLog(results), null, 2));
|
|
8306
8364
|
return completion;
|
|
8307
8365
|
}
|
|
8308
8366
|
if (options.json) {
|
|
8309
|
-
const { buildJsonOutput } = await import("./json-
|
|
8367
|
+
const { buildJsonOutput } = await import("./json-DaFOYHcf.js");
|
|
8310
8368
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
8311
8369
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
8312
8370
|
return completion;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as ENGINE_INFO } from "./engine-info-DCvIfZ0f.js";
|
|
2
|
-
import { t as APP_VERSION } from "./version-
|
|
2
|
+
import { t as APP_VERSION } from "./version-DYg_ShBx.js";
|
|
3
3
|
|
|
4
4
|
//#region src/output/json.ts
|
|
5
5
|
const buildJsonOutput = (results, scoreResult, fileCount, elapsedMs) => {
|
package/dist/mcp.js
CHANGED
|
@@ -538,14 +538,16 @@ const THIN_WRAPPER_PATTERNS = [
|
|
|
538
538
|
const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
|
|
539
539
|
const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
|
|
540
540
|
const DUNDER_PATTERN = /^__\w+__$/;
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
541
|
+
const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
|
|
542
|
+
const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
|
|
543
|
+
const isIdentityForward = (matchText) => {
|
|
544
|
+
const paramsMatch = matchText.match(/\(([^)]*)\)/);
|
|
545
|
+
const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
|
|
546
|
+
if (!paramsMatch || !innerMatch) return false;
|
|
547
|
+
const params = paramNames(paramsMatch[1]);
|
|
548
|
+
const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
|
|
549
|
+
if (args.length === 0) return false;
|
|
550
|
+
return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
|
|
549
551
|
};
|
|
550
552
|
const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
|
|
551
553
|
const detectThinWrappers = (content, relativePath, ext) => {
|
|
@@ -565,7 +567,7 @@ const detectThinWrappers = (content, relativePath, ext) => {
|
|
|
565
567
|
const prevLine = lines[lineNumber - 2]?.trim();
|
|
566
568
|
if (prevLine && prevLine.startsWith("@")) continue;
|
|
567
569
|
}
|
|
568
|
-
if (
|
|
570
|
+
if (!isIdentityForward(matchText)) continue;
|
|
569
571
|
if (isUseContextWrapper(matchText)) continue;
|
|
570
572
|
diagnostics.push({
|
|
571
573
|
filePath: relativePath,
|
|
@@ -650,8 +652,7 @@ const JUSTIFICATION_OPENERS = [
|
|
|
650
652
|
/^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
|
|
651
653
|
];
|
|
652
654
|
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
653
|
-
const
|
|
654
|
-
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design)\b/i;
|
|
655
|
+
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design|ideally|however|although|even\s+though|despite|whereas|unfortunately|trade-?off|first\s+need)\b/i;
|
|
655
656
|
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
656
657
|
"deprecated",
|
|
657
658
|
"see",
|
|
@@ -747,7 +748,7 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
|
|
|
747
748
|
|
|
748
749
|
//#endregion
|
|
749
750
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
750
|
-
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
751
|
+
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
751
752
|
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
|
|
752
753
|
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
753
754
|
|
|
@@ -756,7 +757,7 @@ const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) ||
|
|
|
756
757
|
const TRIVIAL_VERB_STEMS = "Import|Defin|Initializ|Setting|Set\\s+up|Setup|Return|Check|Loop|Iterat|Creat|Updat|Delet|Remov|Handl|Get|Fetch|Increment|Decrement|Writ|Runn|Run|Pars|Execut|Extract|Sav|Load|Build|Start|Stopp|Stop|Clean(?:up|\\s+up)?|Configur|Validat|Process|Queue|Fire|Emit|Dispatch|Log|Print|Render";
|
|
757
758
|
const TRIVIAL_JS_COMMENT_PATTERNS = [/\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i, new RegExp(`\\/\\/\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
758
759
|
const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, new RegExp(`^#\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
759
|
-
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
|
|
760
|
+
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?|if|when|unless|until|only|except|otherwise|needs?|must|should|ensure|avoid|prevent|requires?)\b/i;
|
|
760
761
|
const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
761
762
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
762
763
|
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
@@ -905,6 +906,7 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
|
|
|
905
906
|
"PLACEHOLDER",
|
|
906
907
|
"STUB"
|
|
907
908
|
].join("|")})[:\\s]`);
|
|
909
|
+
const TODO_TRACKING_RE = /https?:\/\/|#\d+|\bgh-\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|\b(?:issue|ticket|jira)\b/i;
|
|
908
910
|
const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
|
|
909
911
|
const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
910
912
|
const contextLines = [];
|
|
@@ -924,7 +926,10 @@ const detectTodoStubs = (content, relativePath) => {
|
|
|
924
926
|
for (let i = 0; i < lines.length; i++) {
|
|
925
927
|
const trimmed = lines[i].trim();
|
|
926
928
|
if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
|
|
927
|
-
if (TODO_PATTERN.test(trimmed))
|
|
929
|
+
if (TODO_PATTERN.test(trimmed)) {
|
|
930
|
+
if (TODO_TRACKING_RE.test(trimmed)) continue;
|
|
931
|
+
diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
|
|
932
|
+
}
|
|
928
933
|
}
|
|
929
934
|
return diagnostics;
|
|
930
935
|
};
|
|
@@ -1438,11 +1443,42 @@ const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|li
|
|
|
1438
1443
|
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
1439
1444
|
const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
|
|
1440
1445
|
const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
|
|
1446
|
+
const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
|
|
1441
1447
|
const PLACEHOLDER_HOSTS = new Set([
|
|
1442
1448
|
"example.com",
|
|
1443
1449
|
"example.org",
|
|
1444
1450
|
"example.net"
|
|
1445
1451
|
]);
|
|
1452
|
+
const LOOPBACK_HOSTS = new Set([
|
|
1453
|
+
"localhost",
|
|
1454
|
+
"127.0.0.1",
|
|
1455
|
+
"0.0.0.0",
|
|
1456
|
+
"::1"
|
|
1457
|
+
]);
|
|
1458
|
+
const VENDOR_API_DOMAINS = [
|
|
1459
|
+
"github.com",
|
|
1460
|
+
"githubusercontent.com",
|
|
1461
|
+
"googleapis.com",
|
|
1462
|
+
"accounts.google.com",
|
|
1463
|
+
"stripe.com",
|
|
1464
|
+
"openai.com",
|
|
1465
|
+
"anthropic.com",
|
|
1466
|
+
"slack.com",
|
|
1467
|
+
"twilio.com",
|
|
1468
|
+
"sendgrid.com",
|
|
1469
|
+
"mailgun.net",
|
|
1470
|
+
"cloudflare.com",
|
|
1471
|
+
"discord.com",
|
|
1472
|
+
"telegram.org",
|
|
1473
|
+
"login.microsoftonline.com",
|
|
1474
|
+
"graph.microsoft.com",
|
|
1475
|
+
"twitter.com",
|
|
1476
|
+
"x.com",
|
|
1477
|
+
"twimg.com",
|
|
1478
|
+
"t.co",
|
|
1479
|
+
"api.telegram.org"
|
|
1480
|
+
];
|
|
1481
|
+
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
1446
1482
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
1447
1483
|
const HARDCODED_URL_FINDING = {
|
|
1448
1484
|
rule: "ai-slop/hardcoded-url",
|
|
@@ -1486,14 +1522,18 @@ const shouldFlagUrlLiteral = (line, urlText) => {
|
|
|
1486
1522
|
const host = safeUrlHost(urlText);
|
|
1487
1523
|
if (!host) return false;
|
|
1488
1524
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
1525
|
+
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
1526
|
+
if (isVendorApiHost(host)) return false;
|
|
1489
1527
|
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
1490
1528
|
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
1491
1529
|
};
|
|
1530
|
+
const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
|
|
1492
1531
|
const hasUsefulIdShape = (value) => {
|
|
1493
1532
|
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
1533
|
+
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
1494
1534
|
if (/^https?:\/\//i.test(value)) return false;
|
|
1495
1535
|
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
1496
|
-
return /[0-
|
|
1536
|
+
return /[0-9]/.test(value);
|
|
1497
1537
|
};
|
|
1498
1538
|
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
1499
1539
|
const diagnostics = [];
|
|
@@ -1520,6 +1560,7 @@ const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
|
1520
1560
|
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
1521
1561
|
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
1522
1562
|
if (isNonProductionPath(relativePath)) return [];
|
|
1563
|
+
if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
|
|
1523
1564
|
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
1524
1565
|
};
|
|
1525
1566
|
const detectHardcodedConfigLiterals = async (context) => {
|
|
@@ -1667,7 +1708,9 @@ const PYTHON_STDLIB = new Set([
|
|
|
1667
1708
|
"builtins",
|
|
1668
1709
|
"bz2",
|
|
1669
1710
|
"calendar",
|
|
1711
|
+
"code",
|
|
1670
1712
|
"codecs",
|
|
1713
|
+
"codeop",
|
|
1671
1714
|
"collections",
|
|
1672
1715
|
"concurrent",
|
|
1673
1716
|
"configparser",
|
|
@@ -1740,6 +1783,7 @@ const PYTHON_STDLIB = new Set([
|
|
|
1740
1783
|
"readline",
|
|
1741
1784
|
"reprlib",
|
|
1742
1785
|
"resource",
|
|
1786
|
+
"rlcompleter",
|
|
1743
1787
|
"secrets",
|
|
1744
1788
|
"select",
|
|
1745
1789
|
"selectors",
|
|
@@ -1838,6 +1882,7 @@ const PYTHON_IMPORT_TO_PIP = {
|
|
|
1838
1882
|
pptx: ["python-pptx"],
|
|
1839
1883
|
git: ["gitpython"],
|
|
1840
1884
|
socks: ["pysocks"],
|
|
1885
|
+
psycopg2: ["psycopg2-binary", "psycopg2"],
|
|
1841
1886
|
redis: ["redis"],
|
|
1842
1887
|
cairo: ["pycairo"],
|
|
1843
1888
|
serial: ["pyserial"],
|
|
@@ -1927,6 +1972,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
1927
1972
|
}
|
|
1928
1973
|
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
1929
1974
|
if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
1975
|
+
const groups = content.match(/\[dependency-groups\]([\s\S]*?)(?=\n\[[^[]|$)/);
|
|
1976
|
+
if (groups) for (const m of groups[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
1930
1977
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1931
1978
|
let match = poetryRe.exec(content);
|
|
1932
1979
|
while (match !== null) {
|
|
@@ -2159,9 +2206,28 @@ const extractJsImports = (content) => {
|
|
|
2159
2206
|
const extractPyImports = (content) => {
|
|
2160
2207
|
const lines = content.split("\n");
|
|
2161
2208
|
const results = [];
|
|
2209
|
+
let inDoc = null;
|
|
2210
|
+
let typeCheckIndent = -1;
|
|
2162
2211
|
for (let i = 0; i < lines.length; i++) {
|
|
2163
|
-
const
|
|
2164
|
-
|
|
2212
|
+
const raw = lines[i];
|
|
2213
|
+
const line = raw.trim();
|
|
2214
|
+
if (inDoc) {
|
|
2215
|
+
if (line.includes(inDoc)) inDoc = null;
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
2219
|
+
const triples = line.match(/"""|'''/g);
|
|
2220
|
+
if (triples) {
|
|
2221
|
+
if (triples.length % 2 === 1) inDoc = triples[triples.length - 1];
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
const indent = raw.length - raw.trimStart().length;
|
|
2225
|
+
if (typeCheckIndent >= 0 && indent <= typeCheckIndent) typeCheckIndent = -1;
|
|
2226
|
+
if (/^if\s+(?:[\w.]+\.)?TYPE_CHECKING\b/.test(line)) {
|
|
2227
|
+
typeCheckIndent = indent;
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
if (typeCheckIndent >= 0) continue;
|
|
2165
2231
|
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
2166
2232
|
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
2167
2233
|
results.push({
|
|
@@ -2171,8 +2237,8 @@ const extractPyImports = (content) => {
|
|
|
2171
2237
|
continue;
|
|
2172
2238
|
}
|
|
2173
2239
|
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
2174
|
-
if (importMatch) for (const
|
|
2175
|
-
const cleaned =
|
|
2240
|
+
if (importMatch) for (const part of importMatch[1].split(",")) {
|
|
2241
|
+
const cleaned = part.trim().split(/\s+as\s+/)[0];
|
|
2176
2242
|
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
2177
2243
|
spec: cleaned,
|
|
2178
2244
|
line: i + 1
|
|
@@ -2229,6 +2295,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2229
2295
|
continue;
|
|
2230
2296
|
}
|
|
2231
2297
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2298
|
+
if (isNonProductionPath(relPath)) continue;
|
|
2232
2299
|
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2233
2300
|
for (const { spec, line } of imports) {
|
|
2234
2301
|
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
@@ -2356,7 +2423,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
2356
2423
|
//#endregion
|
|
2357
2424
|
//#region src/engines/ai-slop/meta-comment.ts
|
|
2358
2425
|
const PLAN_REFERENCE_RES = [
|
|
2359
|
-
|
|
2426
|
+
/^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
|
|
2360
2427
|
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
2361
2428
|
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
2362
2429
|
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
@@ -2458,24 +2525,6 @@ const looksLikeLicenseHeader = (block) => {
|
|
|
2458
2525
|
const text = block.rawLines.join(" ").toLowerCase();
|
|
2459
2526
|
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
2460
2527
|
};
|
|
2461
|
-
const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
|
|
2462
|
-
const isBareSectionLabel = (prose) => {
|
|
2463
|
-
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
2464
|
-
if (prose.endsWith(".")) return false;
|
|
2465
|
-
if (prose.split(/\s+/).length > 3) return false;
|
|
2466
|
-
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
2467
|
-
return true;
|
|
2468
|
-
};
|
|
2469
|
-
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
2470
|
-
const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
2471
|
-
if (nextLine === null) return false;
|
|
2472
|
-
if (!DATA_ENTRY_START.test(nextLine)) return false;
|
|
2473
|
-
const trimmed = nextLine.trim();
|
|
2474
|
-
if (trimmed.startsWith("case ")) return true;
|
|
2475
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
|
|
2476
|
-
if (/^\w+\s*:/.test(trimmed)) return true;
|
|
2477
|
-
return false;
|
|
2478
|
-
};
|
|
2479
2528
|
const looksLikeSuppressDirective = (block) => block.rawLines.some((l) => /\b(biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(l));
|
|
2480
2529
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
2481
2530
|
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
@@ -2564,10 +2613,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2564
2613
|
matched: true,
|
|
2565
2614
|
reason: "phase/section header"
|
|
2566
2615
|
};
|
|
2567
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
2568
|
-
matched: true,
|
|
2569
|
-
reason: "bare section label"
|
|
2570
|
-
};
|
|
2571
2616
|
const joined = block.prose.join(" ");
|
|
2572
2617
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
2573
2618
|
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
@@ -2598,17 +2643,11 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2598
2643
|
};
|
|
2599
2644
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
2600
2645
|
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
2601
|
-
if (nonEmptyProseCount >= 5) {
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
return {
|
|
2607
|
-
matched: true,
|
|
2608
|
-
reason: "long narrative block"
|
|
2609
|
-
};
|
|
2610
|
-
}
|
|
2611
|
-
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
|
|
2646
|
+
if (nonEmptyProseCount >= 5 && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
2647
|
+
matched: true,
|
|
2648
|
+
reason: "long narrative block"
|
|
2649
|
+
};
|
|
2650
|
+
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
2612
2651
|
matched: true,
|
|
2613
2652
|
reason: "multi-line narrative prose"
|
|
2614
2653
|
};
|
|
@@ -3050,7 +3089,14 @@ const JS_EXTS$1 = new Set([
|
|
|
3050
3089
|
".mjs",
|
|
3051
3090
|
".cjs"
|
|
3052
3091
|
]);
|
|
3053
|
-
const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
|
|
3092
|
+
const CATCH_HEAD_RE = /\bcatch\s*(?:\(\s*([^)]*?)\s*\))?\s*\{/g;
|
|
3093
|
+
const isIdentifier = (s) => /^[A-Za-z_$][\w$]*$/.test(s);
|
|
3094
|
+
const recoveryDropsError = (binding, body) => {
|
|
3095
|
+
const name = binding?.trim() ?? "";
|
|
3096
|
+
if (name === "") return true;
|
|
3097
|
+
if (!isIdentifier(name)) return false;
|
|
3098
|
+
return !new RegExp(`\\b${name}\\b`).test(body);
|
|
3099
|
+
};
|
|
3054
3100
|
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3055
3101
|
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3056
3102
|
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -3100,14 +3146,15 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3100
3146
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3101
3147
|
if (body === null) continue;
|
|
3102
3148
|
if (!isLogOnlyBody(body)) continue;
|
|
3149
|
+
if (!recoveryDropsError(match[1], body)) continue;
|
|
3103
3150
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3104
3151
|
out.push({
|
|
3105
3152
|
filePath: relPath,
|
|
3106
3153
|
engine: "ai-slop",
|
|
3107
3154
|
rule: "ai-slop/silent-recovery",
|
|
3108
3155
|
severity: "warning",
|
|
3109
|
-
message: "Catch
|
|
3110
|
-
help: "
|
|
3156
|
+
message: "Catch logs without the caught error then continues; the failure cause is lost",
|
|
3157
|
+
help: "Include the caught error in the log, or rethrow / recover explicitly, so the failure stays diagnosable.",
|
|
3111
3158
|
line,
|
|
3112
3159
|
column: 0,
|
|
3113
3160
|
category: "AI Slop",
|
|
@@ -3117,6 +3164,7 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3117
3164
|
return out;
|
|
3118
3165
|
};
|
|
3119
3166
|
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3167
|
+
const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
|
|
3120
3168
|
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3121
3169
|
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3122
3170
|
const detectPySilentRecovery = (content, relPath) => {
|
|
@@ -3140,13 +3188,14 @@ const detectPySilentRecovery = (content, relPath) => {
|
|
|
3140
3188
|
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3141
3189
|
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3142
3190
|
if (!allLogs || !sawLog) continue;
|
|
3191
|
+
if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
|
|
3143
3192
|
out.push({
|
|
3144
3193
|
filePath: relPath,
|
|
3145
3194
|
engine: "ai-slop",
|
|
3146
3195
|
rule: "ai-slop/silent-recovery",
|
|
3147
3196
|
severity: "warning",
|
|
3148
|
-
message: "except
|
|
3149
|
-
help: "
|
|
3197
|
+
message: "except logs without the caught error then continues; the failure cause is lost",
|
|
3198
|
+
help: "Include the caught error in the log, or re-raise / recover explicitly, so the failure stays diagnosable.",
|
|
3150
3199
|
line: i + 1,
|
|
3151
3200
|
column: 0,
|
|
3152
3201
|
category: "AI Slop",
|
|
@@ -3248,10 +3297,11 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3248
3297
|
const importLines = /* @__PURE__ */ new Set();
|
|
3249
3298
|
for (let i = 0; i < lines.length; i++) {
|
|
3250
3299
|
const trimmed = lines[i].trim();
|
|
3251
|
-
const fromMatch = trimmed.match(/^from\s+[\w.]
|
|
3300
|
+
const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
|
|
3252
3301
|
if (fromMatch) {
|
|
3253
3302
|
importLines.add(i);
|
|
3254
|
-
|
|
3303
|
+
if (fromMatch[1] === "__future__") continue;
|
|
3304
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
3255
3305
|
if (importPart === "*") continue;
|
|
3256
3306
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
3257
3307
|
for (const item of cleaned.split(",")) {
|
|
@@ -6075,6 +6125,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
6075
6125
|
//#endregion
|
|
6076
6126
|
//#region src/scoring/index.ts
|
|
6077
6127
|
const PERFECT_SCORE = 100;
|
|
6128
|
+
const STYLE_RULES = new Set([
|
|
6129
|
+
"ai-slop/trivial-comment",
|
|
6130
|
+
"ai-slop/narrative-comment",
|
|
6131
|
+
"complexity/file-too-large",
|
|
6132
|
+
"complexity/function-too-long"
|
|
6133
|
+
]);
|
|
6134
|
+
const STYLE_WEIGHT = .5;
|
|
6078
6135
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
6079
6136
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
6080
6137
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
@@ -6089,7 +6146,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
6089
6146
|
for (const d of diagnostics) {
|
|
6090
6147
|
const engineWeight = weights[d.engine] ?? 1;
|
|
6091
6148
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
6092
|
-
|
|
6149
|
+
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
6150
|
+
deductions += severityPenalty * engineWeight * styleFactor;
|
|
6093
6151
|
}
|
|
6094
6152
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
6095
6153
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
@@ -6456,7 +6514,7 @@ const handleAislopBaseline = (input) => {
|
|
|
6456
6514
|
|
|
6457
6515
|
//#endregion
|
|
6458
6516
|
//#region src/version.ts
|
|
6459
|
-
const APP_VERSION = "0.
|
|
6517
|
+
const APP_VERSION = "0.10.0";
|
|
6460
6518
|
|
|
6461
6519
|
//#endregion
|
|
6462
6520
|
//#region src/telemetry/env.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aislop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Catch the slop AI coding agents leave in your code: narrative comments, swallowed exceptions, as-any casts, dead code, oversized functions. 40+ rules across 7 languages (TS/JS, Python, Go, Rust, Ruby, PHP, Java). Sub-second, deterministic, no LLM at runtime. MIT-licensed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|