aislop 0.9.6 → 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 +107 -61
- package/dist/index.js +109 -63
- package/dist/{json-CxiErSgX.js → json-DaFOYHcf.js} +1 -1
- package/dist/mcp.js +107 -61
- package/dist/{sarif-CLVijBAO.js → sarif-BtSQ92c6.js} +1 -1
- package/dist/version-DYg_ShBx.js +5 -0
- package/package.json +1 -1
- package/dist/version-CPpO6jbj.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
|
};
|
|
@@ -2023,6 +2028,30 @@ const LOOPBACK_HOSTS = new Set([
|
|
|
2023
2028
|
"0.0.0.0",
|
|
2024
2029
|
"::1"
|
|
2025
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}`));
|
|
2026
2055
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2027
2056
|
const HARDCODED_URL_FINDING = {
|
|
2028
2057
|
rule: "ai-slop/hardcoded-url",
|
|
@@ -2067,6 +2096,7 @@ const shouldFlagUrlLiteral = (line, urlText) => {
|
|
|
2067
2096
|
if (!host) return false;
|
|
2068
2097
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2069
2098
|
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
2099
|
+
if (isVendorApiHost(host)) return false;
|
|
2070
2100
|
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2071
2101
|
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2072
2102
|
};
|
|
@@ -2251,7 +2281,9 @@ const PYTHON_STDLIB = new Set([
|
|
|
2251
2281
|
"builtins",
|
|
2252
2282
|
"bz2",
|
|
2253
2283
|
"calendar",
|
|
2284
|
+
"code",
|
|
2254
2285
|
"codecs",
|
|
2286
|
+
"codeop",
|
|
2255
2287
|
"collections",
|
|
2256
2288
|
"concurrent",
|
|
2257
2289
|
"configparser",
|
|
@@ -2324,6 +2356,7 @@ const PYTHON_STDLIB = new Set([
|
|
|
2324
2356
|
"readline",
|
|
2325
2357
|
"reprlib",
|
|
2326
2358
|
"resource",
|
|
2359
|
+
"rlcompleter",
|
|
2327
2360
|
"secrets",
|
|
2328
2361
|
"select",
|
|
2329
2362
|
"selectors",
|
|
@@ -2512,6 +2545,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
2512
2545
|
}
|
|
2513
2546
|
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
2514
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]);
|
|
2515
2550
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
2516
2551
|
let match = poetryRe.exec(content);
|
|
2517
2552
|
while (match !== null) {
|
|
@@ -2744,9 +2779,28 @@ const extractJsImports = (content) => {
|
|
|
2744
2779
|
const extractPyImports = (content) => {
|
|
2745
2780
|
const lines = content.split("\n");
|
|
2746
2781
|
const results = [];
|
|
2782
|
+
let inDoc = null;
|
|
2783
|
+
let typeCheckIndent = -1;
|
|
2747
2784
|
for (let i = 0; i < lines.length; i++) {
|
|
2748
|
-
const
|
|
2749
|
-
|
|
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;
|
|
2750
2804
|
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
2751
2805
|
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
2752
2806
|
results.push({
|
|
@@ -2756,8 +2810,8 @@ const extractPyImports = (content) => {
|
|
|
2756
2810
|
continue;
|
|
2757
2811
|
}
|
|
2758
2812
|
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
2759
|
-
if (importMatch) for (const
|
|
2760
|
-
const cleaned =
|
|
2813
|
+
if (importMatch) for (const part of importMatch[1].split(",")) {
|
|
2814
|
+
const cleaned = part.trim().split(/\s+as\s+/)[0];
|
|
2761
2815
|
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
2762
2816
|
spec: cleaned,
|
|
2763
2817
|
line: i + 1
|
|
@@ -2814,6 +2868,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2814
2868
|
continue;
|
|
2815
2869
|
}
|
|
2816
2870
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2871
|
+
if (isNonProductionPath(relPath)) continue;
|
|
2817
2872
|
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2818
2873
|
for (const { spec, line } of imports) {
|
|
2819
2874
|
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
@@ -2941,7 +2996,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
2941
2996
|
//#endregion
|
|
2942
2997
|
//#region src/engines/ai-slop/meta-comment.ts
|
|
2943
2998
|
const PLAN_REFERENCE_RES = [
|
|
2944
|
-
|
|
2999
|
+
/^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
|
|
2945
3000
|
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
2946
3001
|
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
2947
3002
|
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
@@ -3043,24 +3098,6 @@ const looksLikeLicenseHeader = (block) => {
|
|
|
3043
3098
|
const text = block.rawLines.join(" ").toLowerCase();
|
|
3044
3099
|
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
3045
3100
|
};
|
|
3046
|
-
const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
|
|
3047
|
-
const isBareSectionLabel = (prose) => {
|
|
3048
|
-
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
3049
|
-
if (prose.endsWith(".")) return false;
|
|
3050
|
-
if (prose.split(/\s+/).length > 3) return false;
|
|
3051
|
-
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
3052
|
-
return true;
|
|
3053
|
-
};
|
|
3054
|
-
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
3055
|
-
const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
3056
|
-
if (nextLine === null) return false;
|
|
3057
|
-
if (!DATA_ENTRY_START.test(nextLine)) return false;
|
|
3058
|
-
const trimmed = nextLine.trim();
|
|
3059
|
-
if (trimmed.startsWith("case ")) return true;
|
|
3060
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
|
|
3061
|
-
if (/^\w+\s*:/.test(trimmed)) return true;
|
|
3062
|
-
return false;
|
|
3063
|
-
};
|
|
3064
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));
|
|
3065
3102
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
3066
3103
|
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
@@ -3149,10 +3186,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3149
3186
|
matched: true,
|
|
3150
3187
|
reason: "phase/section header"
|
|
3151
3188
|
};
|
|
3152
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
3153
|
-
matched: true,
|
|
3154
|
-
reason: "bare section label"
|
|
3155
|
-
};
|
|
3156
3189
|
const joined = block.prose.join(" ");
|
|
3157
3190
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
3158
3191
|
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
@@ -3183,17 +3216,11 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3183
3216
|
};
|
|
3184
3217
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
3185
3218
|
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
3186
|
-
if (nonEmptyProseCount >= 5) {
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
return {
|
|
3192
|
-
matched: true,
|
|
3193
|
-
reason: "long narrative block"
|
|
3194
|
-
};
|
|
3195
|
-
}
|
|
3196
|
-
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 {
|
|
3197
3224
|
matched: true,
|
|
3198
3225
|
reason: "multi-line narrative prose"
|
|
3199
3226
|
};
|
|
@@ -3635,7 +3662,14 @@ const JS_EXTS$1 = new Set([
|
|
|
3635
3662
|
".mjs",
|
|
3636
3663
|
".cjs"
|
|
3637
3664
|
]);
|
|
3638
|
-
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
|
+
};
|
|
3639
3673
|
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3640
3674
|
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3641
3675
|
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -3685,14 +3719,15 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3685
3719
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3686
3720
|
if (body === null) continue;
|
|
3687
3721
|
if (!isLogOnlyBody(body)) continue;
|
|
3722
|
+
if (!recoveryDropsError(match[1], body)) continue;
|
|
3688
3723
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3689
3724
|
out.push({
|
|
3690
3725
|
filePath: relPath,
|
|
3691
3726
|
engine: "ai-slop",
|
|
3692
3727
|
rule: "ai-slop/silent-recovery",
|
|
3693
3728
|
severity: "warning",
|
|
3694
|
-
message: "Catch
|
|
3695
|
-
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.",
|
|
3696
3731
|
line,
|
|
3697
3732
|
column: 0,
|
|
3698
3733
|
category: "AI Slop",
|
|
@@ -3702,6 +3737,7 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3702
3737
|
return out;
|
|
3703
3738
|
};
|
|
3704
3739
|
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3740
|
+
const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
|
|
3705
3741
|
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3706
3742
|
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3707
3743
|
const detectPySilentRecovery = (content, relPath) => {
|
|
@@ -3725,13 +3761,14 @@ const detectPySilentRecovery = (content, relPath) => {
|
|
|
3725
3761
|
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3726
3762
|
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3727
3763
|
if (!allLogs || !sawLog) continue;
|
|
3764
|
+
if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
|
|
3728
3765
|
out.push({
|
|
3729
3766
|
filePath: relPath,
|
|
3730
3767
|
engine: "ai-slop",
|
|
3731
3768
|
rule: "ai-slop/silent-recovery",
|
|
3732
3769
|
severity: "warning",
|
|
3733
|
-
message: "except
|
|
3734
|
-
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.",
|
|
3735
3772
|
line: i + 1,
|
|
3736
3773
|
column: 0,
|
|
3737
3774
|
category: "AI Slop",
|
|
@@ -3834,10 +3871,11 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3834
3871
|
const importLines = /* @__PURE__ */ new Set();
|
|
3835
3872
|
for (let i = 0; i < lines.length; i++) {
|
|
3836
3873
|
const trimmed = lines[i].trim();
|
|
3837
|
-
const fromMatch = trimmed.match(/^from\s+[\w.]
|
|
3874
|
+
const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
|
|
3838
3875
|
if (fromMatch) {
|
|
3839
3876
|
importLines.add(i);
|
|
3840
|
-
|
|
3877
|
+
if (fromMatch[1] === "__future__") continue;
|
|
3878
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
3841
3879
|
if (importPart === "*") continue;
|
|
3842
3880
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
3843
3881
|
for (const item of cleaned.split(",")) {
|
|
@@ -7159,6 +7197,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
7159
7197
|
//#endregion
|
|
7160
7198
|
//#region src/scoring/index.ts
|
|
7161
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;
|
|
7162
7207
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
7163
7208
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
7164
7209
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
@@ -7173,7 +7218,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
7173
7218
|
for (const d of diagnostics) {
|
|
7174
7219
|
const engineWeight = weights[d.engine] ?? 1;
|
|
7175
7220
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
7176
|
-
|
|
7221
|
+
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
7222
|
+
deductions += severityPenalty * engineWeight * styleFactor;
|
|
7177
7223
|
}
|
|
7178
7224
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
7179
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
|
};
|
|
@@ -2192,6 +2197,30 @@ const LOOPBACK_HOSTS = new Set([
|
|
|
2192
2197
|
"0.0.0.0",
|
|
2193
2198
|
"::1"
|
|
2194
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}`));
|
|
2195
2224
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2196
2225
|
const HARDCODED_URL_FINDING = {
|
|
2197
2226
|
rule: "ai-slop/hardcoded-url",
|
|
@@ -2236,6 +2265,7 @@ const shouldFlagUrlLiteral = (line, urlText) => {
|
|
|
2236
2265
|
if (!host) return false;
|
|
2237
2266
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2238
2267
|
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
2268
|
+
if (isVendorApiHost(host)) return false;
|
|
2239
2269
|
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2240
2270
|
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2241
2271
|
};
|
|
@@ -2420,7 +2450,9 @@ const PYTHON_STDLIB = new Set([
|
|
|
2420
2450
|
"builtins",
|
|
2421
2451
|
"bz2",
|
|
2422
2452
|
"calendar",
|
|
2453
|
+
"code",
|
|
2423
2454
|
"codecs",
|
|
2455
|
+
"codeop",
|
|
2424
2456
|
"collections",
|
|
2425
2457
|
"concurrent",
|
|
2426
2458
|
"configparser",
|
|
@@ -2493,6 +2525,7 @@ const PYTHON_STDLIB = new Set([
|
|
|
2493
2525
|
"readline",
|
|
2494
2526
|
"reprlib",
|
|
2495
2527
|
"resource",
|
|
2528
|
+
"rlcompleter",
|
|
2496
2529
|
"secrets",
|
|
2497
2530
|
"select",
|
|
2498
2531
|
"selectors",
|
|
@@ -2681,6 +2714,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
2681
2714
|
}
|
|
2682
2715
|
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
2683
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]);
|
|
2684
2719
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
2685
2720
|
let match = poetryRe.exec(content);
|
|
2686
2721
|
while (match !== null) {
|
|
@@ -2913,9 +2948,28 @@ const extractJsImports = (content) => {
|
|
|
2913
2948
|
const extractPyImports = (content) => {
|
|
2914
2949
|
const lines = content.split("\n");
|
|
2915
2950
|
const results = [];
|
|
2951
|
+
let inDoc = null;
|
|
2952
|
+
let typeCheckIndent = -1;
|
|
2916
2953
|
for (let i = 0; i < lines.length; i++) {
|
|
2917
|
-
const
|
|
2918
|
-
|
|
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;
|
|
2919
2973
|
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
2920
2974
|
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
2921
2975
|
results.push({
|
|
@@ -2925,8 +2979,8 @@ const extractPyImports = (content) => {
|
|
|
2925
2979
|
continue;
|
|
2926
2980
|
}
|
|
2927
2981
|
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
2928
|
-
if (importMatch) for (const
|
|
2929
|
-
const cleaned =
|
|
2982
|
+
if (importMatch) for (const part of importMatch[1].split(",")) {
|
|
2983
|
+
const cleaned = part.trim().split(/\s+as\s+/)[0];
|
|
2930
2984
|
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
2931
2985
|
spec: cleaned,
|
|
2932
2986
|
line: i + 1
|
|
@@ -2983,6 +3037,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2983
3037
|
continue;
|
|
2984
3038
|
}
|
|
2985
3039
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3040
|
+
if (isNonProductionPath(relPath)) continue;
|
|
2986
3041
|
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2987
3042
|
for (const { spec, line } of imports) {
|
|
2988
3043
|
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
@@ -3110,7 +3165,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
3110
3165
|
//#endregion
|
|
3111
3166
|
//#region src/engines/ai-slop/meta-comment.ts
|
|
3112
3167
|
const PLAN_REFERENCE_RES = [
|
|
3113
|
-
|
|
3168
|
+
/^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
|
|
3114
3169
|
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
3115
3170
|
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
3116
3171
|
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
@@ -3212,24 +3267,6 @@ const looksLikeLicenseHeader = (block) => {
|
|
|
3212
3267
|
const text = block.rawLines.join(" ").toLowerCase();
|
|
3213
3268
|
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
3214
3269
|
};
|
|
3215
|
-
const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
|
|
3216
|
-
const isBareSectionLabel = (prose) => {
|
|
3217
|
-
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
3218
|
-
if (prose.endsWith(".")) return false;
|
|
3219
|
-
if (prose.split(/\s+/).length > 3) return false;
|
|
3220
|
-
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
3221
|
-
return true;
|
|
3222
|
-
};
|
|
3223
|
-
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
3224
|
-
const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
3225
|
-
if (nextLine === null) return false;
|
|
3226
|
-
if (!DATA_ENTRY_START.test(nextLine)) return false;
|
|
3227
|
-
const trimmed = nextLine.trim();
|
|
3228
|
-
if (trimmed.startsWith("case ")) return true;
|
|
3229
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
|
|
3230
|
-
if (/^\w+\s*:/.test(trimmed)) return true;
|
|
3231
|
-
return false;
|
|
3232
|
-
};
|
|
3233
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));
|
|
3234
3271
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
3235
3272
|
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
@@ -3318,10 +3355,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3318
3355
|
matched: true,
|
|
3319
3356
|
reason: "phase/section header"
|
|
3320
3357
|
};
|
|
3321
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
3322
|
-
matched: true,
|
|
3323
|
-
reason: "bare section label"
|
|
3324
|
-
};
|
|
3325
3358
|
const joined = block.prose.join(" ");
|
|
3326
3359
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
3327
3360
|
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
@@ -3352,17 +3385,11 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3352
3385
|
};
|
|
3353
3386
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
3354
3387
|
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
3355
|
-
if (nonEmptyProseCount >= 5) {
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
return {
|
|
3361
|
-
matched: true,
|
|
3362
|
-
reason: "long narrative block"
|
|
3363
|
-
};
|
|
3364
|
-
}
|
|
3365
|
-
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 {
|
|
3366
3393
|
matched: true,
|
|
3367
3394
|
reason: "multi-line narrative prose"
|
|
3368
3395
|
};
|
|
@@ -3804,7 +3831,14 @@ const JS_EXTS$1 = new Set([
|
|
|
3804
3831
|
".mjs",
|
|
3805
3832
|
".cjs"
|
|
3806
3833
|
]);
|
|
3807
|
-
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
|
+
};
|
|
3808
3842
|
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3809
3843
|
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3810
3844
|
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -3854,14 +3888,15 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3854
3888
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3855
3889
|
if (body === null) continue;
|
|
3856
3890
|
if (!isLogOnlyBody(body)) continue;
|
|
3891
|
+
if (!recoveryDropsError(match[1], body)) continue;
|
|
3857
3892
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3858
3893
|
out.push({
|
|
3859
3894
|
filePath: relPath,
|
|
3860
3895
|
engine: "ai-slop",
|
|
3861
3896
|
rule: "ai-slop/silent-recovery",
|
|
3862
3897
|
severity: "warning",
|
|
3863
|
-
message: "Catch
|
|
3864
|
-
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.",
|
|
3865
3900
|
line,
|
|
3866
3901
|
column: 0,
|
|
3867
3902
|
category: "AI Slop",
|
|
@@ -3871,6 +3906,7 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3871
3906
|
return out;
|
|
3872
3907
|
};
|
|
3873
3908
|
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3909
|
+
const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
|
|
3874
3910
|
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3875
3911
|
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3876
3912
|
const detectPySilentRecovery = (content, relPath) => {
|
|
@@ -3894,13 +3930,14 @@ const detectPySilentRecovery = (content, relPath) => {
|
|
|
3894
3930
|
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3895
3931
|
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3896
3932
|
if (!allLogs || !sawLog) continue;
|
|
3933
|
+
if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
|
|
3897
3934
|
out.push({
|
|
3898
3935
|
filePath: relPath,
|
|
3899
3936
|
engine: "ai-slop",
|
|
3900
3937
|
rule: "ai-slop/silent-recovery",
|
|
3901
3938
|
severity: "warning",
|
|
3902
|
-
message: "except
|
|
3903
|
-
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.",
|
|
3904
3941
|
line: i + 1,
|
|
3905
3942
|
column: 0,
|
|
3906
3943
|
category: "AI Slop",
|
|
@@ -4003,10 +4040,11 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
4003
4040
|
const importLines = /* @__PURE__ */ new Set();
|
|
4004
4041
|
for (let i = 0; i < lines.length; i++) {
|
|
4005
4042
|
const trimmed = lines[i].trim();
|
|
4006
|
-
const fromMatch = trimmed.match(/^from\s+[\w.]
|
|
4043
|
+
const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
|
|
4007
4044
|
if (fromMatch) {
|
|
4008
4045
|
importLines.add(i);
|
|
4009
|
-
|
|
4046
|
+
if (fromMatch[1] === "__future__") continue;
|
|
4047
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
4010
4048
|
if (importPart === "*") continue;
|
|
4011
4049
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
4012
4050
|
for (const item of cleaned.split(",")) {
|
|
@@ -7118,6 +7156,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
7118
7156
|
//#endregion
|
|
7119
7157
|
//#region src/scoring/index.ts
|
|
7120
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;
|
|
7121
7166
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
7122
7167
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
7123
7168
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
@@ -7132,7 +7177,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
7132
7177
|
for (const d of diagnostics) {
|
|
7133
7178
|
const engineWeight = weights[d.engine] ?? 1;
|
|
7134
7179
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
7135
|
-
|
|
7180
|
+
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
7181
|
+
deductions += severityPenalty * engineWeight * styleFactor;
|
|
7136
7182
|
}
|
|
7137
7183
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
7138
7184
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
@@ -8313,12 +8359,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
8313
8359
|
engineTimings
|
|
8314
8360
|
};
|
|
8315
8361
|
if (options.sarif) {
|
|
8316
|
-
const { buildSarifLog } = await import("./sarif-
|
|
8362
|
+
const { buildSarifLog } = await import("./sarif-BtSQ92c6.js");
|
|
8317
8363
|
console.log(JSON.stringify(buildSarifLog(results), null, 2));
|
|
8318
8364
|
return completion;
|
|
8319
8365
|
}
|
|
8320
8366
|
if (options.json) {
|
|
8321
|
-
const { buildJsonOutput } = await import("./json-
|
|
8367
|
+
const { buildJsonOutput } = await import("./json-DaFOYHcf.js");
|
|
8322
8368
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
8323
8369
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
8324
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
|
};
|
|
@@ -1450,6 +1455,30 @@ const LOOPBACK_HOSTS = new Set([
|
|
|
1450
1455
|
"0.0.0.0",
|
|
1451
1456
|
"::1"
|
|
1452
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}`));
|
|
1453
1482
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
1454
1483
|
const HARDCODED_URL_FINDING = {
|
|
1455
1484
|
rule: "ai-slop/hardcoded-url",
|
|
@@ -1494,6 +1523,7 @@ const shouldFlagUrlLiteral = (line, urlText) => {
|
|
|
1494
1523
|
if (!host) return false;
|
|
1495
1524
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
1496
1525
|
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
1526
|
+
if (isVendorApiHost(host)) return false;
|
|
1497
1527
|
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
1498
1528
|
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
1499
1529
|
};
|
|
@@ -1678,7 +1708,9 @@ const PYTHON_STDLIB = new Set([
|
|
|
1678
1708
|
"builtins",
|
|
1679
1709
|
"bz2",
|
|
1680
1710
|
"calendar",
|
|
1711
|
+
"code",
|
|
1681
1712
|
"codecs",
|
|
1713
|
+
"codeop",
|
|
1682
1714
|
"collections",
|
|
1683
1715
|
"concurrent",
|
|
1684
1716
|
"configparser",
|
|
@@ -1751,6 +1783,7 @@ const PYTHON_STDLIB = new Set([
|
|
|
1751
1783
|
"readline",
|
|
1752
1784
|
"reprlib",
|
|
1753
1785
|
"resource",
|
|
1786
|
+
"rlcompleter",
|
|
1754
1787
|
"secrets",
|
|
1755
1788
|
"select",
|
|
1756
1789
|
"selectors",
|
|
@@ -1939,6 +1972,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
1939
1972
|
}
|
|
1940
1973
|
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
1941
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]);
|
|
1942
1977
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1943
1978
|
let match = poetryRe.exec(content);
|
|
1944
1979
|
while (match !== null) {
|
|
@@ -2171,9 +2206,28 @@ const extractJsImports = (content) => {
|
|
|
2171
2206
|
const extractPyImports = (content) => {
|
|
2172
2207
|
const lines = content.split("\n");
|
|
2173
2208
|
const results = [];
|
|
2209
|
+
let inDoc = null;
|
|
2210
|
+
let typeCheckIndent = -1;
|
|
2174
2211
|
for (let i = 0; i < lines.length; i++) {
|
|
2175
|
-
const
|
|
2176
|
-
|
|
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;
|
|
2177
2231
|
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
2178
2232
|
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
2179
2233
|
results.push({
|
|
@@ -2183,8 +2237,8 @@ const extractPyImports = (content) => {
|
|
|
2183
2237
|
continue;
|
|
2184
2238
|
}
|
|
2185
2239
|
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
2186
|
-
if (importMatch) for (const
|
|
2187
|
-
const cleaned =
|
|
2240
|
+
if (importMatch) for (const part of importMatch[1].split(",")) {
|
|
2241
|
+
const cleaned = part.trim().split(/\s+as\s+/)[0];
|
|
2188
2242
|
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
2189
2243
|
spec: cleaned,
|
|
2190
2244
|
line: i + 1
|
|
@@ -2241,6 +2295,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2241
2295
|
continue;
|
|
2242
2296
|
}
|
|
2243
2297
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2298
|
+
if (isNonProductionPath(relPath)) continue;
|
|
2244
2299
|
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2245
2300
|
for (const { spec, line } of imports) {
|
|
2246
2301
|
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
@@ -2368,7 +2423,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
2368
2423
|
//#endregion
|
|
2369
2424
|
//#region src/engines/ai-slop/meta-comment.ts
|
|
2370
2425
|
const PLAN_REFERENCE_RES = [
|
|
2371
|
-
|
|
2426
|
+
/^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
|
|
2372
2427
|
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
2373
2428
|
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
2374
2429
|
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
@@ -2470,24 +2525,6 @@ const looksLikeLicenseHeader = (block) => {
|
|
|
2470
2525
|
const text = block.rawLines.join(" ").toLowerCase();
|
|
2471
2526
|
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
2472
2527
|
};
|
|
2473
|
-
const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
|
|
2474
|
-
const isBareSectionLabel = (prose) => {
|
|
2475
|
-
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
2476
|
-
if (prose.endsWith(".")) return false;
|
|
2477
|
-
if (prose.split(/\s+/).length > 3) return false;
|
|
2478
|
-
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
2479
|
-
return true;
|
|
2480
|
-
};
|
|
2481
|
-
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
2482
|
-
const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
2483
|
-
if (nextLine === null) return false;
|
|
2484
|
-
if (!DATA_ENTRY_START.test(nextLine)) return false;
|
|
2485
|
-
const trimmed = nextLine.trim();
|
|
2486
|
-
if (trimmed.startsWith("case ")) return true;
|
|
2487
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
|
|
2488
|
-
if (/^\w+\s*:/.test(trimmed)) return true;
|
|
2489
|
-
return false;
|
|
2490
|
-
};
|
|
2491
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));
|
|
2492
2529
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
2493
2530
|
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
@@ -2576,10 +2613,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2576
2613
|
matched: true,
|
|
2577
2614
|
reason: "phase/section header"
|
|
2578
2615
|
};
|
|
2579
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
2580
|
-
matched: true,
|
|
2581
|
-
reason: "bare section label"
|
|
2582
|
-
};
|
|
2583
2616
|
const joined = block.prose.join(" ");
|
|
2584
2617
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
2585
2618
|
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
@@ -2610,17 +2643,11 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2610
2643
|
};
|
|
2611
2644
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
2612
2645
|
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
2613
|
-
if (nonEmptyProseCount >= 5) {
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
return {
|
|
2619
|
-
matched: true,
|
|
2620
|
-
reason: "long narrative block"
|
|
2621
|
-
};
|
|
2622
|
-
}
|
|
2623
|
-
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 {
|
|
2624
2651
|
matched: true,
|
|
2625
2652
|
reason: "multi-line narrative prose"
|
|
2626
2653
|
};
|
|
@@ -3062,7 +3089,14 @@ const JS_EXTS$1 = new Set([
|
|
|
3062
3089
|
".mjs",
|
|
3063
3090
|
".cjs"
|
|
3064
3091
|
]);
|
|
3065
|
-
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
|
+
};
|
|
3066
3100
|
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3067
3101
|
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3068
3102
|
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -3112,14 +3146,15 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3112
3146
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3113
3147
|
if (body === null) continue;
|
|
3114
3148
|
if (!isLogOnlyBody(body)) continue;
|
|
3149
|
+
if (!recoveryDropsError(match[1], body)) continue;
|
|
3115
3150
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3116
3151
|
out.push({
|
|
3117
3152
|
filePath: relPath,
|
|
3118
3153
|
engine: "ai-slop",
|
|
3119
3154
|
rule: "ai-slop/silent-recovery",
|
|
3120
3155
|
severity: "warning",
|
|
3121
|
-
message: "Catch
|
|
3122
|
-
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.",
|
|
3123
3158
|
line,
|
|
3124
3159
|
column: 0,
|
|
3125
3160
|
category: "AI Slop",
|
|
@@ -3129,6 +3164,7 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3129
3164
|
return out;
|
|
3130
3165
|
};
|
|
3131
3166
|
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3167
|
+
const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
|
|
3132
3168
|
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3133
3169
|
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3134
3170
|
const detectPySilentRecovery = (content, relPath) => {
|
|
@@ -3152,13 +3188,14 @@ const detectPySilentRecovery = (content, relPath) => {
|
|
|
3152
3188
|
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3153
3189
|
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3154
3190
|
if (!allLogs || !sawLog) continue;
|
|
3191
|
+
if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
|
|
3155
3192
|
out.push({
|
|
3156
3193
|
filePath: relPath,
|
|
3157
3194
|
engine: "ai-slop",
|
|
3158
3195
|
rule: "ai-slop/silent-recovery",
|
|
3159
3196
|
severity: "warning",
|
|
3160
|
-
message: "except
|
|
3161
|
-
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.",
|
|
3162
3199
|
line: i + 1,
|
|
3163
3200
|
column: 0,
|
|
3164
3201
|
category: "AI Slop",
|
|
@@ -3260,10 +3297,11 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3260
3297
|
const importLines = /* @__PURE__ */ new Set();
|
|
3261
3298
|
for (let i = 0; i < lines.length; i++) {
|
|
3262
3299
|
const trimmed = lines[i].trim();
|
|
3263
|
-
const fromMatch = trimmed.match(/^from\s+[\w.]
|
|
3300
|
+
const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
|
|
3264
3301
|
if (fromMatch) {
|
|
3265
3302
|
importLines.add(i);
|
|
3266
|
-
|
|
3303
|
+
if (fromMatch[1] === "__future__") continue;
|
|
3304
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
3267
3305
|
if (importPart === "*") continue;
|
|
3268
3306
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
3269
3307
|
for (const item of cleaned.split(",")) {
|
|
@@ -6087,6 +6125,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
6087
6125
|
//#endregion
|
|
6088
6126
|
//#region src/scoring/index.ts
|
|
6089
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;
|
|
6090
6135
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
6091
6136
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
6092
6137
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
@@ -6101,7 +6146,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
6101
6146
|
for (const d of diagnostics) {
|
|
6102
6147
|
const engineWeight = weights[d.engine] ?? 1;
|
|
6103
6148
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
6104
|
-
|
|
6149
|
+
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
6150
|
+
deductions += severityPenalty * engineWeight * styleFactor;
|
|
6105
6151
|
}
|
|
6106
6152
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
6107
6153
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
@@ -6468,7 +6514,7 @@ const handleAislopBaseline = (input) => {
|
|
|
6468
6514
|
|
|
6469
6515
|
//#endregion
|
|
6470
6516
|
//#region src/version.ts
|
|
6471
|
-
const APP_VERSION = "0.
|
|
6517
|
+
const APP_VERSION = "0.10.0";
|
|
6472
6518
|
|
|
6473
6519
|
//#endregion
|
|
6474
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": {
|