aislop 0.9.6 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -34,7 +34,7 @@ var __exportAll = (all, no_symbols) => {
34
34
 
35
35
  //#endregion
36
36
  //#region src/version.ts
37
- const APP_VERSION = "0.9.6";
37
+ const APP_VERSION = "0.10.1";
38
38
 
39
39
  //#endregion
40
40
  //#region src/telemetry/env.ts
@@ -183,7 +183,7 @@ const redactProperties = (props) => {
183
183
  const POSTHOG_HOST = process.env.AISLOP_POSTHOG_HOST ?? "https://eu.i.posthog.com";
184
184
  const POSTHOG_KEY = process.env.AISLOP_POSTHOG_KEY ?? "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
185
185
  const SCHEMA_VERSION = "v2";
186
- const REQUEST_TIMEOUT_MS = 3e3;
186
+ const REQUEST_TIMEOUT_MS$1 = 3e3;
187
187
  const isTelemetryDisabled = (config) => {
188
188
  const env = process.env;
189
189
  if (env.AISLOP_NO_TELEMETRY === "1" || env.DO_NOT_TRACK === "1") return true;
@@ -237,7 +237,7 @@ const track = (input) => {
237
237
  method: "POST",
238
238
  headers: { "Content-Type": "application/json" },
239
239
  body: JSON.stringify(payload),
240
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
240
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS$1)
241
241
  }).then(() => {}).catch(() => {}).finally(() => {
242
242
  pendingRequests.delete(request);
243
243
  });
@@ -836,6 +836,218 @@ const loadConfig = (directory) => {
836
836
  }
837
837
  };
838
838
 
839
+ //#endregion
840
+ //#region src/utils/source-masker.ts
841
+ const JS_EXTS$2 = new Set([
842
+ ".ts",
843
+ ".tsx",
844
+ ".js",
845
+ ".jsx",
846
+ ".mjs",
847
+ ".cjs"
848
+ ]);
849
+ const PY_EXTS = new Set([".py"]);
850
+ const RB_EXTS = new Set([".rb"]);
851
+ const PHP_EXTS = new Set([".php"]);
852
+ const familyForExt = (ext) => {
853
+ if (JS_EXTS$2.has(ext)) return "js";
854
+ if (PY_EXTS.has(ext)) return "py";
855
+ if (RB_EXTS.has(ext)) return "rb";
856
+ if (PHP_EXTS.has(ext)) return "php";
857
+ return "none";
858
+ };
859
+ const maskStringsAndComments = (content, ext) => {
860
+ const family = familyForExt(ext);
861
+ if (family === "none") return content;
862
+ if (family === "js") return maskJs(content, true);
863
+ return maskSimple(content, family, true);
864
+ };
865
+ const maskComments = (content, ext) => {
866
+ const family = familyForExt(ext);
867
+ if (family === "none") return content;
868
+ if (family === "js") return maskJs(content, false);
869
+ return maskSimple(content, family, false);
870
+ };
871
+ const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
872
+ const len = content.length;
873
+ const c = content[i];
874
+ const next = content[i + 1];
875
+ if (c === "\"" || c === "'") {
876
+ const strStart = i;
877
+ const end = consumeQuotedString(content, i, c);
878
+ if (maskStrings) mask(strStart + 1, end - 1);
879
+ return {
880
+ handled: true,
881
+ nextI: end
882
+ };
883
+ }
884
+ if (c === "`") {
885
+ const scan = consumeTemplateString(content, i + 1);
886
+ if (maskStrings) mask(i + 1, scan.maskEnd);
887
+ if (scan.openedInterp) tplStack.push(0);
888
+ return {
889
+ handled: true,
890
+ nextI: scan.resumeAt
891
+ };
892
+ }
893
+ if (c === "/" && next === "/") {
894
+ const strStart = i;
895
+ let k = i;
896
+ while (k < len && content[k] !== "\n") k++;
897
+ mask(strStart, k);
898
+ return {
899
+ handled: true,
900
+ nextI: k
901
+ };
902
+ }
903
+ if (c === "/" && next === "*") {
904
+ const strStart = i;
905
+ let k = i + 2;
906
+ while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
907
+ if (k < len - 1) k += 2;
908
+ mask(strStart, k);
909
+ return {
910
+ handled: true,
911
+ nextI: k
912
+ };
913
+ }
914
+ return {
915
+ handled: false,
916
+ nextI: i
917
+ };
918
+ };
919
+ const maskJs = (content, maskStrings) => {
920
+ const out = content.split("");
921
+ const len = content.length;
922
+ const tplStack = [];
923
+ let i = 0;
924
+ const mask = (start, end) => {
925
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
926
+ };
927
+ while (i < len) {
928
+ const c = content[i];
929
+ if (tplStack.length > 0) {
930
+ if (c === "{") {
931
+ tplStack[tplStack.length - 1]++;
932
+ i++;
933
+ continue;
934
+ }
935
+ if (c === "}") {
936
+ if (tplStack[tplStack.length - 1] === 0) {
937
+ tplStack.pop();
938
+ const scan = consumeTemplateString(content, i + 1);
939
+ if (maskStrings) mask(i + 1, scan.maskEnd);
940
+ if (scan.openedInterp) tplStack.push(0);
941
+ i = scan.resumeAt;
942
+ continue;
943
+ }
944
+ tplStack[tplStack.length - 1]--;
945
+ i++;
946
+ continue;
947
+ }
948
+ }
949
+ const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
950
+ if (handled.handled) {
951
+ i = handled.nextI;
952
+ continue;
953
+ }
954
+ i++;
955
+ }
956
+ return out.join("");
957
+ };
958
+ const consumeQuotedString = (content, start, quote) => {
959
+ const len = content.length;
960
+ let i = start + 1;
961
+ while (i < len) {
962
+ const c = content[i];
963
+ if (c === "\\" && i + 1 < len) {
964
+ i += 2;
965
+ continue;
966
+ }
967
+ if (c === quote) return i + 1;
968
+ if (c === "\n") return i;
969
+ i++;
970
+ }
971
+ return i;
972
+ };
973
+ const consumeTemplateString = (content, start) => {
974
+ const len = content.length;
975
+ let i = start;
976
+ while (i < len) {
977
+ const c = content[i];
978
+ if (c === "\\" && i + 1 < len) {
979
+ i += 2;
980
+ continue;
981
+ }
982
+ if (c === "`") return {
983
+ maskEnd: i,
984
+ resumeAt: i + 1,
985
+ openedInterp: false
986
+ };
987
+ if (c === "$" && content[i + 1] === "{") return {
988
+ maskEnd: i,
989
+ resumeAt: i + 2,
990
+ openedInterp: true
991
+ };
992
+ i++;
993
+ }
994
+ return {
995
+ maskEnd: i,
996
+ resumeAt: i,
997
+ openedInterp: false
998
+ };
999
+ };
1000
+ const maskSimple = (content, family, maskStrings) => {
1001
+ const out = content.split("");
1002
+ const len = content.length;
1003
+ let i = 0;
1004
+ const mask = (start, end) => {
1005
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
1006
+ };
1007
+ while (i < len) {
1008
+ const c = content[i];
1009
+ const next = content[i + 1];
1010
+ if (family === "py" && (c === "\"" || c === "'")) {
1011
+ if (content[i + 1] === c && content[i + 2] === c) {
1012
+ const triple = c + c + c;
1013
+ const end = content.indexOf(triple, i + 3);
1014
+ const stop = end === -1 ? len : end + 3;
1015
+ if (maskStrings) mask(i + 3, stop - 3);
1016
+ i = stop;
1017
+ continue;
1018
+ }
1019
+ }
1020
+ if (c === "\"" || c === "'") {
1021
+ const strStart = i;
1022
+ i = consumeQuotedString(content, i, c);
1023
+ if (maskStrings) mask(strStart + 1, i - 1);
1024
+ continue;
1025
+ }
1026
+ if ((family === "py" || family === "rb" || family === "php") && c === "#") {
1027
+ const strStart = i;
1028
+ while (i < len && content[i] !== "\n") i++;
1029
+ mask(strStart, i);
1030
+ continue;
1031
+ }
1032
+ if (family === "php" && c === "/" && next === "/") {
1033
+ const strStart = i;
1034
+ while (i < len && content[i] !== "\n") i++;
1035
+ mask(strStart, i);
1036
+ continue;
1037
+ }
1038
+ if (family === "php" && c === "/" && next === "*") {
1039
+ const strStart = i;
1040
+ i += 2;
1041
+ while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
1042
+ if (i < len - 1) i += 2;
1043
+ mask(strStart, i);
1044
+ continue;
1045
+ }
1046
+ i++;
1047
+ }
1048
+ return out.join("");
1049
+ };
1050
+
839
1051
  //#endregion
840
1052
  //#region src/utils/source-files.ts
841
1053
  const MAX_BUFFER$1 = 50 * 1024 * 1024;
@@ -1086,7 +1298,7 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
1086
1298
 
1087
1299
  //#endregion
1088
1300
  //#region src/engines/ai-slop/abstractions.ts
1089
- const JS_EXTS$2 = new Set([
1301
+ const JS_EXTS$1 = new Set([
1090
1302
  ".ts",
1091
1303
  ".tsx",
1092
1304
  ".js",
@@ -1097,11 +1309,11 @@ const JS_EXTS$2 = new Set([
1097
1309
  const THIN_WRAPPER_PATTERNS = [
1098
1310
  {
1099
1311
  pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
1100
- extensions: JS_EXTS$2
1312
+ extensions: JS_EXTS$1
1101
1313
  },
1102
1314
  {
1103
1315
  pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
1104
- extensions: JS_EXTS$2
1316
+ extensions: JS_EXTS$1
1105
1317
  },
1106
1318
  {
1107
1319
  pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
@@ -1111,14 +1323,16 @@ const THIN_WRAPPER_PATTERNS = [
1111
1323
  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
1324
  const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
1113
1325
  const DUNDER_PATTERN = /^__\w+__$/;
1114
- const hasHardcodedArgs = (matchText) => {
1115
- const innerCallMatch = matchText.match(/=>\s*\w+\(([^)]*)\)\s*;?\s*$/);
1116
- if (!innerCallMatch) {
1117
- const returnCallMatch = matchText.match(/return\s+\w+\(([^)]*)\)\s*;?\s*\}/);
1118
- if (!returnCallMatch) return false;
1119
- return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(returnCallMatch[1]);
1120
- }
1121
- return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(innerCallMatch[1]);
1326
+ const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
1327
+ const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
1328
+ const isIdentityForward = (matchText) => {
1329
+ const paramsMatch = matchText.match(/\(([^)]*)\)/);
1330
+ const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
1331
+ if (!paramsMatch || !innerMatch) return false;
1332
+ const params = paramNames(paramsMatch[1]);
1333
+ const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
1334
+ if (args.length === 0) return false;
1335
+ return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
1122
1336
  };
1123
1337
  const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
1124
1338
  const detectThinWrappers = (content, relativePath, ext) => {
@@ -1138,7 +1352,7 @@ const detectThinWrappers = (content, relativePath, ext) => {
1138
1352
  const prevLine = lines[lineNumber - 2]?.trim();
1139
1353
  if (prevLine && prevLine.startsWith("@")) continue;
1140
1354
  }
1141
- if (hasHardcodedArgs(matchText)) continue;
1355
+ if (!isIdentityForward(matchText)) continue;
1142
1356
  if (isUseContextWrapper(matchText)) continue;
1143
1357
  diagnostics.push({
1144
1358
  filePath: relativePath,
@@ -1192,8 +1406,9 @@ const detectOverAbstraction = async (context) => {
1192
1406
  }
1193
1407
  const relativePath = path.relative(context.rootDirectory, filePath);
1194
1408
  const ext = path.extname(filePath);
1195
- diagnostics.push(...detectThinWrappers(content, relativePath, ext));
1196
- diagnostics.push(...detectAiNaming(content, relativePath));
1409
+ const codeOnly = maskComments(content, ext);
1410
+ diagnostics.push(...detectThinWrappers(codeOnly, relativePath, ext));
1411
+ diagnostics.push(...detectAiNaming(codeOnly, relativePath));
1197
1412
  }
1198
1413
  return diagnostics;
1199
1414
  };
@@ -1223,8 +1438,7 @@ const JUSTIFICATION_OPENERS = [
1223
1438
  /^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
1224
1439
  ];
1225
1440
  const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
1226
- const STEP_COMMENT_VERB_RE = /^(?:Render|Enable|Disable|Initialize|Init|Setup|Set|Get|Fetch|Load|Save|Build|Create|Delete|Remove|Add|Update|Process|Execute|Run|Start|Stop|Clean|Cleanup|Configure|Validate|Check|Verify|Parse|Extract|Apply|Wait|Sleep|Skip|Allow|Deny|Lock|Unlock|Refresh|Reload|Reset|Clear|Send|Receive|Read|Write|Print|Log|Emit|Dispatch|Fire|Open|Close|Bind|Connect|Disconnect|Register|Unregister|Push|Pop|Insert|Append|Prepend|Sort|Filter|Find|Search|Replace|Encode|Decode|Convert|Transform|Map|Reduce|Iterate|Loop|Walk|Visit|Mark|Unmark|Toggle|Switch|Restart|Resume|Pause|Abort|Cancel|Compute|Calculate|Resolve|Reject|Ignore|Handle|Track|Trace|Increment|Decrement|Round|Truncate|Resize|Move|Copy|Clone|Merge|Split|Join|Wrap|Unwrap|Bump|Drain|Flush|Sync|Persist|Commit|Rollback|Yield|Return|Discard|Defer|Pin|Unpin|Mount|Unmount|Spawn|Kill|Restore)(?:\s|$)/;
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;
1441
+ 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
1442
  const MEANINGFUL_JSDOC_TAGS = new Set([
1229
1443
  "deprecated",
1230
1444
  "see",
@@ -1320,7 +1534,7 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
1320
1534
 
1321
1535
  //#endregion
1322
1536
  //#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;
1537
+ 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
1538
  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
1539
  const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
1326
1540
 
@@ -1329,7 +1543,7 @@ const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) ||
1329
1543
  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
1544
  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
1545
  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;
1546
+ 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
1547
  const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
1334
1548
  const MAX_TRIVIAL_COMMENT_LENGTH = 60;
1335
1549
  const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
@@ -1478,6 +1692,7 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
1478
1692
  "PLACEHOLDER",
1479
1693
  "STUB"
1480
1694
  ].join("|")})[:\\s]`);
1695
+ const TODO_TRACKING_RE = /https?:\/\/|#\d+|\bgh-\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|\b(?:issue|ticket|jira)\b/i;
1481
1696
  const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
1482
1697
  const isGuardedSingleLineExit = (lines, lineIndex) => {
1483
1698
  const contextLines = [];
@@ -1497,7 +1712,10 @@ const detectTodoStubs = (content, relativePath) => {
1497
1712
  for (let i = 0; i < lines.length; i++) {
1498
1713
  const trimmed = lines[i].trim();
1499
1714
  if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
1500
- if (TODO_PATTERN.test(trimmed)) 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));
1715
+ if (TODO_PATTERN.test(trimmed)) {
1716
+ if (TODO_TRACKING_RE.test(trimmed)) continue;
1717
+ 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));
1718
+ }
1501
1719
  }
1502
1720
  return diagnostics;
1503
1721
  };
@@ -1546,9 +1764,10 @@ const detectDeadPatterns = async (context) => {
1546
1764
  }
1547
1765
  const ext = path.extname(filePath);
1548
1766
  const relativePath = path.relative(context.rootDirectory, filePath);
1549
- diagnostics.push(...detectConsoleLeftovers(content, relativePath, ext));
1767
+ const codeOnly = maskComments(content, ext);
1768
+ diagnostics.push(...detectConsoleLeftovers(codeOnly, relativePath, ext));
1550
1769
  diagnostics.push(...detectTodoStubs(content, relativePath));
1551
- diagnostics.push(...detectDeadCodePatterns(content, relativePath, ext));
1770
+ diagnostics.push(...detectDeadCodePatterns(codeOnly, relativePath, ext));
1552
1771
  diagnostics.push(...detectUnsafeTypePatterns(content, relativePath, ext));
1553
1772
  }
1554
1773
  return diagnostics;
@@ -1738,6 +1957,7 @@ const JS_EXTENSIONS$3 = new Set([
1738
1957
  const IMPORT_FROM_RE$1 = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
1739
1958
  const TYPE_ONLY_RE = /^\s*type\b/;
1740
1959
  const VALUE_BINDING_RE = /\{([^}]*)\}/;
1960
+ const NAMESPACE_RE = /\*\s+as\s+/;
1741
1961
  const isTypeOnly = (clause) => {
1742
1962
  if (TYPE_ONLY_RE.test(clause)) return true;
1743
1963
  const braces = VALUE_BINDING_RE.exec(clause);
@@ -1755,7 +1975,8 @@ const extractImportLines = (content) => {
1755
1975
  results.push({
1756
1976
  spec: match[2],
1757
1977
  line: i + 1,
1758
- typeOnly: isTypeOnly(match[1])
1978
+ typeOnly: isTypeOnly(match[1]),
1979
+ namespace: NAMESPACE_RE.test(match[1])
1759
1980
  });
1760
1981
  }
1761
1982
  return results;
@@ -1772,11 +1993,11 @@ const detectDuplicateImports = async (context) => {
1772
1993
  } catch {
1773
1994
  continue;
1774
1995
  }
1775
- const imports = extractImportLines(content);
1996
+ const imports = extractImportLines(maskComments(content, path.extname(filePath)));
1776
1997
  if (imports.length < 2) continue;
1777
1998
  const byBucket = /* @__PURE__ */ new Map();
1778
1999
  for (const imp of imports) {
1779
- const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
2000
+ const key = `${imp.namespace ? "ns" : imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
1780
2001
  const list = byBucket.get(key) ?? [];
1781
2002
  list.push(imp);
1782
2003
  byBucket.set(key, list);
@@ -2023,6 +2244,30 @@ const LOOPBACK_HOSTS = new Set([
2023
2244
  "0.0.0.0",
2024
2245
  "::1"
2025
2246
  ]);
2247
+ const VENDOR_API_DOMAINS = [
2248
+ "github.com",
2249
+ "githubusercontent.com",
2250
+ "googleapis.com",
2251
+ "accounts.google.com",
2252
+ "stripe.com",
2253
+ "openai.com",
2254
+ "anthropic.com",
2255
+ "slack.com",
2256
+ "twilio.com",
2257
+ "sendgrid.com",
2258
+ "mailgun.net",
2259
+ "cloudflare.com",
2260
+ "discord.com",
2261
+ "telegram.org",
2262
+ "login.microsoftonline.com",
2263
+ "graph.microsoft.com",
2264
+ "twitter.com",
2265
+ "x.com",
2266
+ "twimg.com",
2267
+ "t.co",
2268
+ "api.telegram.org"
2269
+ ];
2270
+ const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
2026
2271
  const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
2027
2272
  const HARDCODED_URL_FINDING = {
2028
2273
  rule: "ai-slop/hardcoded-url",
@@ -2067,6 +2312,7 @@ const shouldFlagUrlLiteral = (line, urlText) => {
2067
2312
  if (!host) return false;
2068
2313
  if (PLACEHOLDER_HOSTS.has(host)) return false;
2069
2314
  if (LOOPBACK_HOSTS.has(host)) return false;
2315
+ if (isVendorApiHost(host)) return false;
2070
2316
  if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
2071
2317
  return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
2072
2318
  };
@@ -2118,7 +2364,7 @@ const detectHardcodedConfigLiterals = async (context) => {
2118
2364
  }
2119
2365
  const relativePath = path.relative(context.rootDirectory, filePath);
2120
2366
  const ext = path.extname(filePath);
2121
- diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
2367
+ diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
2122
2368
  }
2123
2369
  return diagnostics;
2124
2370
  };
@@ -2251,7 +2497,9 @@ const PYTHON_STDLIB = new Set([
2251
2497
  "builtins",
2252
2498
  "bz2",
2253
2499
  "calendar",
2500
+ "code",
2254
2501
  "codecs",
2502
+ "codeop",
2255
2503
  "collections",
2256
2504
  "concurrent",
2257
2505
  "configparser",
@@ -2324,6 +2572,7 @@ const PYTHON_STDLIB = new Set([
2324
2572
  "readline",
2325
2573
  "reprlib",
2326
2574
  "resource",
2575
+ "rlcompleter",
2327
2576
  "secrets",
2328
2577
  "select",
2329
2578
  "selectors",
@@ -2512,6 +2761,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
2512
2761
  }
2513
2762
  const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
2514
2763
  if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
2764
+ const groups = content.match(/\[dependency-groups\]([\s\S]*?)(?=\n\[[^[]|$)/);
2765
+ if (groups) for (const m of groups[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
2515
2766
  const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
2516
2767
  let match = poetryRe.exec(content);
2517
2768
  while (match !== null) {
@@ -2744,9 +2995,28 @@ const extractJsImports = (content) => {
2744
2995
  const extractPyImports = (content) => {
2745
2996
  const lines = content.split("\n");
2746
2997
  const results = [];
2998
+ let inDoc = null;
2999
+ let typeCheckIndent = -1;
2747
3000
  for (let i = 0; i < lines.length; i++) {
2748
- const line = lines[i].trim();
2749
- if (line.startsWith("#")) continue;
3001
+ const raw = lines[i];
3002
+ const line = raw.trim();
3003
+ if (inDoc) {
3004
+ if (line.includes(inDoc)) inDoc = null;
3005
+ continue;
3006
+ }
3007
+ if (line === "" || line.startsWith("#")) continue;
3008
+ const triples = line.match(/"""|'''/g);
3009
+ if (triples) {
3010
+ if (triples.length % 2 === 1) inDoc = triples[triples.length - 1];
3011
+ continue;
3012
+ }
3013
+ const indent = raw.length - raw.trimStart().length;
3014
+ if (typeCheckIndent >= 0 && indent <= typeCheckIndent) typeCheckIndent = -1;
3015
+ if (/^if\s+(?:[\w.]+\.)?TYPE_CHECKING\b/.test(line)) {
3016
+ typeCheckIndent = indent;
3017
+ continue;
3018
+ }
3019
+ if (typeCheckIndent >= 0) continue;
2750
3020
  const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
2751
3021
  if (fromMatch && !fromMatch[1].startsWith(".")) {
2752
3022
  results.push({
@@ -2756,8 +3026,8 @@ const extractPyImports = (content) => {
2756
3026
  continue;
2757
3027
  }
2758
3028
  const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
2759
- if (importMatch) for (const raw of importMatch[1].split(",")) {
2760
- const cleaned = raw.trim().split(/\s+as\s+/)[0];
3029
+ if (importMatch) for (const part of importMatch[1].split(",")) {
3030
+ const cleaned = part.trim().split(/\s+as\s+/)[0];
2761
3031
  if (cleaned && !cleaned.startsWith(".")) results.push({
2762
3032
  spec: cleaned,
2763
3033
  line: i + 1
@@ -2814,6 +3084,7 @@ const detectHallucinatedImports = async (context) => {
2814
3084
  continue;
2815
3085
  }
2816
3086
  const relPath = path.relative(context.rootDirectory, filePath);
3087
+ if (isNonProductionPath(relPath)) continue;
2817
3088
  const imports = isJs ? extractJsImports(content) : extractPyImports(content);
2818
3089
  for (const { spec, line } of imports) {
2819
3090
  const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
@@ -2941,7 +3212,7 @@ const collectBlocks = (sourceLines, syntax) => {
2941
3212
  //#endregion
2942
3213
  //#region src/engines/ai-slop/meta-comment.ts
2943
3214
  const PLAN_REFERENCE_RES = [
2944
- /\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
3215
+ /^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
2945
3216
  /\bstep\s+\d+\s+of\s+the\s+plan\b/i,
2946
3217
  /\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
2947
3218
  /\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
@@ -3043,24 +3314,6 @@ const looksLikeLicenseHeader = (block) => {
3043
3314
  const text = block.rawLines.join(" ").toLowerCase();
3044
3315
  return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
3045
3316
  };
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
3317
  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
3318
  const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
3066
3319
  const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
@@ -3149,10 +3402,6 @@ const detectNarrativeInBlock = (block, ext) => {
3149
3402
  matched: true,
3150
3403
  reason: "phase/section header"
3151
3404
  };
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
3405
  const joined = block.prose.join(" ");
3157
3406
  const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
3158
3407
  if (hasWhyMarker || hasDocIndicator(block)) return {
@@ -3183,17 +3432,11 @@ const detectNarrativeInBlock = (block, ext) => {
3183
3432
  };
3184
3433
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
3185
3434
  const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
3186
- if (nonEmptyProseCount >= 5) {
3187
- if (isAboveDeclaration) return {
3188
- matched: false,
3189
- reason: ""
3190
- };
3191
- return {
3192
- matched: true,
3193
- reason: "long narrative block"
3194
- };
3195
- }
3196
- if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
3435
+ if (nonEmptyProseCount >= 5 && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
3436
+ matched: true,
3437
+ reason: "long narrative block"
3438
+ };
3439
+ if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
3197
3440
  matched: true,
3198
3441
  reason: "multi-line narrative prose"
3199
3442
  };
@@ -3627,7 +3870,7 @@ const detectRustPatterns = async (context) => {
3627
3870
 
3628
3871
  //#endregion
3629
3872
  //#region src/engines/ai-slop/silent-recovery.ts
3630
- const JS_EXTS$1 = new Set([
3873
+ const JS_EXTS = new Set([
3631
3874
  ".ts",
3632
3875
  ".tsx",
3633
3876
  ".js",
@@ -3635,7 +3878,14 @@ const JS_EXTS$1 = new Set([
3635
3878
  ".mjs",
3636
3879
  ".cjs"
3637
3880
  ]);
3638
- const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
3881
+ const CATCH_HEAD_RE = /\bcatch\s*(?:\(\s*([^)]*?)\s*\))?\s*\{/g;
3882
+ const isIdentifier = (s) => /^[A-Za-z_$][\w$]*$/.test(s);
3883
+ const recoveryDropsError = (binding, body) => {
3884
+ const name = binding?.trim() ?? "";
3885
+ if (name === "") return true;
3886
+ if (!isIdentifier(name)) return false;
3887
+ return !new RegExp(`\\b${name}\\b`).test(body);
3888
+ };
3639
3889
  const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
3640
3890
  const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
3641
3891
  const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
@@ -3685,14 +3935,15 @@ const detectJsSilentRecovery = (content, relPath) => {
3685
3935
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3686
3936
  if (body === null) continue;
3687
3937
  if (!isLogOnlyBody(body)) continue;
3938
+ if (!recoveryDropsError(match[1], body)) continue;
3688
3939
  const line = content.slice(0, match.index).split("\n").length;
3689
3940
  out.push({
3690
3941
  filePath: relPath,
3691
3942
  engine: "ai-slop",
3692
3943
  rule: "ai-slop/silent-recovery",
3693
3944
  severity: "warning",
3694
- message: "Catch only logs then continues, leaving execution in a possibly broken state",
3695
- help: "Handle the error: rethrow, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
3945
+ message: "Catch logs without the caught error then continues; the failure cause is lost",
3946
+ help: "Include the caught error in the log, or rethrow / recover explicitly, so the failure stays diagnosable.",
3696
3947
  line,
3697
3948
  column: 0,
3698
3949
  category: "AI Slop",
@@ -3702,6 +3953,7 @@ const detectJsSilentRecovery = (content, relPath) => {
3702
3953
  return out;
3703
3954
  };
3704
3955
  const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
3956
+ const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
3705
3957
  const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
3706
3958
  const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
3707
3959
  const detectPySilentRecovery = (content, relPath) => {
@@ -3725,13 +3977,14 @@ const detectPySilentRecovery = (content, relPath) => {
3725
3977
  const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
3726
3978
  const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
3727
3979
  if (!allLogs || !sawLog) continue;
3980
+ if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
3728
3981
  out.push({
3729
3982
  filePath: relPath,
3730
3983
  engine: "ai-slop",
3731
3984
  rule: "ai-slop/silent-recovery",
3732
3985
  severity: "warning",
3733
- message: "except only logs then continues, leaving execution in a possibly broken state",
3734
- help: "Handle the error: re-raise, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
3986
+ message: "except logs without the caught error then continues; the failure cause is lost",
3987
+ help: "Include the caught error in the log, or re-raise / recover explicitly, so the failure stays diagnosable.",
3735
3988
  line: i + 1,
3736
3989
  column: 0,
3737
3990
  category: "AI Slop",
@@ -3746,7 +3999,7 @@ const detectSilentRecovery = async (context) => {
3746
3999
  for (const filePath of files) {
3747
4000
  if (isAutoGenerated(filePath)) continue;
3748
4001
  const ext = path.extname(filePath);
3749
- const isJs = JS_EXTS$1.has(ext);
4002
+ const isJs = JS_EXTS.has(ext);
3750
4003
  if (!isJs && !(ext === ".py")) continue;
3751
4004
  const relPath = path.relative(context.rootDirectory, filePath);
3752
4005
  if (isNonProductionPath(relPath)) continue;
@@ -3834,10 +4087,11 @@ const extractPyImportedSymbols = (lines) => {
3834
4087
  const importLines = /* @__PURE__ */ new Set();
3835
4088
  for (let i = 0; i < lines.length; i++) {
3836
4089
  const trimmed = lines[i].trim();
3837
- const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
4090
+ const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
3838
4091
  if (fromMatch) {
3839
4092
  importLines.add(i);
3840
- const importPart = fromMatch[1].replace(/#.*$/, "").trim();
4093
+ if (fromMatch[1] === "__future__") continue;
4094
+ const importPart = fromMatch[2].replace(/#.*$/, "").trim();
3841
4095
  if (importPart === "*") continue;
3842
4096
  const cleaned = importPart.replace(/[()]/g, "");
3843
4097
  for (const item of cleaned.split(",")) {
@@ -4225,12 +4479,92 @@ const findBraceFunctionEnd = (lines, startIndex) => {
4225
4479
  maxNesting
4226
4480
  };
4227
4481
  };
4228
- const findPythonFunctionEnd = (lines, startIndex) => {
4229
- const baseIndent = lines[startIndex].match(/^(\s*)/)?.[1].length ?? 0;
4230
- let endLine = startIndex;
4482
+ const extractPythonSignature = (lines, startIndex) => {
4483
+ let depth = 0;
4484
+ let started = false;
4485
+ let params = "";
4486
+ for (let j = startIndex; j < lines.length; j++) {
4487
+ const l = lines[j];
4488
+ for (let ci = 0; ci < l.length; ci++) {
4489
+ const ch = l[ci];
4490
+ if (ch === "(") {
4491
+ depth++;
4492
+ if (depth === 1 && !started) {
4493
+ started = true;
4494
+ continue;
4495
+ }
4496
+ } else if (ch === ")") {
4497
+ depth--;
4498
+ if (depth === 0) return {
4499
+ params,
4500
+ sigEndIndex: j
4501
+ };
4502
+ }
4503
+ if (started) params += ch;
4504
+ }
4505
+ if (started) params += " ";
4506
+ }
4507
+ return {
4508
+ params,
4509
+ sigEndIndex: startIndex
4510
+ };
4511
+ };
4512
+ const countPythonParams = (signature) => {
4513
+ let depth = 0;
4514
+ const parts = [];
4515
+ let current = "";
4516
+ for (const ch of signature) {
4517
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
4518
+ else if (ch === ")" || ch === "]" || ch === "}") depth--;
4519
+ if (ch === "," && depth === 0) {
4520
+ parts.push(current);
4521
+ current = "";
4522
+ continue;
4523
+ }
4524
+ current += ch;
4525
+ }
4526
+ parts.push(current);
4527
+ let count = 0;
4528
+ for (const raw of parts) {
4529
+ const p = raw.trim();
4530
+ if (p.length === 0 || p === "*" || p === "/") continue;
4531
+ if (p.startsWith("*")) continue;
4532
+ if (p.includes("=")) continue;
4533
+ const name = p.split(":")[0].trim();
4534
+ if (name === "self" || name === "cls") continue;
4535
+ count++;
4536
+ }
4537
+ return count;
4538
+ };
4539
+ const countPythonBodyCodeLines = (lines, sigEndIndex, endLine) => {
4540
+ let count = 0;
4541
+ let inDoc = false;
4542
+ let delim = "";
4543
+ for (let j = sigEndIndex + 1; j <= endLine && j < lines.length; j++) {
4544
+ const t = lines[j].trim();
4545
+ if (inDoc) {
4546
+ if (t.includes(delim)) inDoc = false;
4547
+ continue;
4548
+ }
4549
+ if (t === "" || t.startsWith("#")) continue;
4550
+ const opener = t.startsWith("\"\"\"") ? "\"\"\"" : t.startsWith("'''") ? "'''" : "";
4551
+ if (opener) {
4552
+ if (!t.slice(3).includes(opener)) {
4553
+ inDoc = true;
4554
+ delim = opener;
4555
+ }
4556
+ continue;
4557
+ }
4558
+ count++;
4559
+ }
4560
+ return count;
4561
+ };
4562
+ const findPythonFunctionEnd = (lines, defIndex, bodyStartIndex) => {
4563
+ const baseIndent = lines[defIndex].match(/^(\s*)/)?.[1].length ?? 0;
4564
+ let endLine = bodyStartIndex;
4231
4565
  let maxNesting = 0;
4232
4566
  const controlIndentStack = [];
4233
- for (let j = startIndex + 1; j < lines.length; j++) {
4567
+ for (let j = bodyStartIndex + 1; j < lines.length; j++) {
4234
4568
  const l = lines[j];
4235
4569
  if (l.trim() === "") {
4236
4570
  endLine = j;
@@ -4252,7 +4586,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
4252
4586
  };
4253
4587
  };
4254
4588
  const findFunctionEnd = (lines, startIndex, isPython) => {
4255
- if (isPython) return findPythonFunctionEnd(lines, startIndex);
4589
+ if (isPython) {
4590
+ const { sigEndIndex } = extractPythonSignature(lines, startIndex);
4591
+ return findPythonFunctionEnd(lines, startIndex, sigEndIndex);
4592
+ }
4256
4593
  return findBraceFunctionEnd(lines, startIndex);
4257
4594
  };
4258
4595
  const isBlockArrow = (lines, startIndex) => {
@@ -4317,7 +4654,7 @@ const FUNCTION_PATTERNS = [
4317
4654
  ]
4318
4655
  },
4319
4656
  {
4320
- regex: /^\s*def\s+(\w+)\s*\(([^)]*)\)/,
4657
+ regex: /^\s*(?:async\s+)?def\s+(\w+)\s*\(/,
4321
4658
  langFilter: [".py"]
4322
4659
  },
4323
4660
  {
@@ -4374,14 +4711,23 @@ const analyzeFunctions = (content, ext) => {
4374
4711
  const isPython = fnMatch.patternIndex === 2;
4375
4712
  if (fnMatch.patternIndex === 1 && !isBlockArrow(lines, i)) continue;
4376
4713
  const { endLine, maxNesting } = findFunctionEnd(lines, i, isPython);
4377
- const bodyLines = lines.slice(i + 1, endLine);
4378
- const templateLines = isPython ? 0 : countTemplateLines(bodyLines);
4714
+ let templateLines;
4715
+ let paramCount;
4716
+ if (isPython) {
4717
+ const sig = extractPythonSignature(lines, i);
4718
+ const codeLines = countPythonBodyCodeLines(lines, sig.sigEndIndex, endLine);
4719
+ templateLines = endLine - i + 1 - codeLines;
4720
+ paramCount = countPythonParams(sig.params);
4721
+ } else {
4722
+ templateLines = countTemplateLines(lines.slice(i + 1, endLine));
4723
+ paramCount = countParams(fnMatch.params);
4724
+ }
4379
4725
  functions.push({
4380
4726
  name: fnMatch.name,
4381
4727
  startLine: i + 1,
4382
4728
  lineCount: endLine - i + 1,
4383
4729
  maxNesting,
4384
- paramCount: countParams(fnMatch.params),
4730
+ paramCount,
4385
4731
  templateLines
4386
4732
  });
4387
4733
  }
@@ -6591,212 +6937,6 @@ const runCargoAudit = async (rootDir, timeout) => {
6591
6937
  }
6592
6938
  };
6593
6939
 
6594
- //#endregion
6595
- //#region src/utils/source-masker.ts
6596
- const JS_EXTS = new Set([
6597
- ".ts",
6598
- ".tsx",
6599
- ".js",
6600
- ".jsx",
6601
- ".mjs",
6602
- ".cjs"
6603
- ]);
6604
- const PY_EXTS = new Set([".py"]);
6605
- const RB_EXTS = new Set([".rb"]);
6606
- const PHP_EXTS = new Set([".php"]);
6607
- const familyForExt = (ext) => {
6608
- if (JS_EXTS.has(ext)) return "js";
6609
- if (PY_EXTS.has(ext)) return "py";
6610
- if (RB_EXTS.has(ext)) return "rb";
6611
- if (PHP_EXTS.has(ext)) return "php";
6612
- return "none";
6613
- };
6614
- const maskStringsAndComments = (content, ext) => {
6615
- const family = familyForExt(ext);
6616
- if (family === "none") return content;
6617
- if (family === "js") return maskJs(content);
6618
- return maskSimple(content, family);
6619
- };
6620
- const handleQuotesAndComments = (content, i, tplStack, mask) => {
6621
- const len = content.length;
6622
- const c = content[i];
6623
- const next = content[i + 1];
6624
- if (c === "\"" || c === "'") {
6625
- const strStart = i;
6626
- const end = consumeQuotedString(content, i, c);
6627
- mask(strStart + 1, end - 1);
6628
- return {
6629
- handled: true,
6630
- nextI: end
6631
- };
6632
- }
6633
- if (c === "`") {
6634
- const scan = consumeTemplateString(content, i + 1);
6635
- mask(i + 1, scan.maskEnd);
6636
- if (scan.openedInterp) tplStack.push(0);
6637
- return {
6638
- handled: true,
6639
- nextI: scan.resumeAt
6640
- };
6641
- }
6642
- if (c === "/" && next === "/") {
6643
- const strStart = i;
6644
- let k = i;
6645
- while (k < len && content[k] !== "\n") k++;
6646
- mask(strStart, k);
6647
- return {
6648
- handled: true,
6649
- nextI: k
6650
- };
6651
- }
6652
- if (c === "/" && next === "*") {
6653
- const strStart = i;
6654
- let k = i + 2;
6655
- while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
6656
- if (k < len - 1) k += 2;
6657
- mask(strStart, k);
6658
- return {
6659
- handled: true,
6660
- nextI: k
6661
- };
6662
- }
6663
- return {
6664
- handled: false,
6665
- nextI: i
6666
- };
6667
- };
6668
- const maskJs = (content) => {
6669
- const out = content.split("");
6670
- const len = content.length;
6671
- const tplStack = [];
6672
- let i = 0;
6673
- const mask = (start, end) => {
6674
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
6675
- };
6676
- while (i < len) {
6677
- const c = content[i];
6678
- if (tplStack.length > 0) {
6679
- if (c === "{") {
6680
- tplStack[tplStack.length - 1]++;
6681
- i++;
6682
- continue;
6683
- }
6684
- if (c === "}") {
6685
- if (tplStack[tplStack.length - 1] === 0) {
6686
- tplStack.pop();
6687
- const scan = consumeTemplateString(content, i + 1);
6688
- mask(i + 1, scan.maskEnd);
6689
- if (scan.openedInterp) tplStack.push(0);
6690
- i = scan.resumeAt;
6691
- continue;
6692
- }
6693
- tplStack[tplStack.length - 1]--;
6694
- i++;
6695
- continue;
6696
- }
6697
- }
6698
- const handled = handleQuotesAndComments(content, i, tplStack, mask);
6699
- if (handled.handled) {
6700
- i = handled.nextI;
6701
- continue;
6702
- }
6703
- i++;
6704
- }
6705
- return out.join("");
6706
- };
6707
- const consumeQuotedString = (content, start, quote) => {
6708
- const len = content.length;
6709
- let i = start + 1;
6710
- while (i < len) {
6711
- const c = content[i];
6712
- if (c === "\\" && i + 1 < len) {
6713
- i += 2;
6714
- continue;
6715
- }
6716
- if (c === quote) return i + 1;
6717
- if (c === "\n") return i;
6718
- i++;
6719
- }
6720
- return i;
6721
- };
6722
- const consumeTemplateString = (content, start) => {
6723
- const len = content.length;
6724
- let i = start;
6725
- while (i < len) {
6726
- const c = content[i];
6727
- if (c === "\\" && i + 1 < len) {
6728
- i += 2;
6729
- continue;
6730
- }
6731
- if (c === "`") return {
6732
- maskEnd: i,
6733
- resumeAt: i + 1,
6734
- openedInterp: false
6735
- };
6736
- if (c === "$" && content[i + 1] === "{") return {
6737
- maskEnd: i,
6738
- resumeAt: i + 2,
6739
- openedInterp: true
6740
- };
6741
- i++;
6742
- }
6743
- return {
6744
- maskEnd: i,
6745
- resumeAt: i,
6746
- openedInterp: false
6747
- };
6748
- };
6749
- const maskSimple = (content, family) => {
6750
- const out = content.split("");
6751
- const len = content.length;
6752
- let i = 0;
6753
- const mask = (start, end) => {
6754
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
6755
- };
6756
- while (i < len) {
6757
- const c = content[i];
6758
- const next = content[i + 1];
6759
- if (family === "py" && (c === "\"" || c === "'")) {
6760
- if (content[i + 1] === c && content[i + 2] === c) {
6761
- const triple = c + c + c;
6762
- const end = content.indexOf(triple, i + 3);
6763
- const stop = end === -1 ? len : end + 3;
6764
- mask(i + 3, stop - 3);
6765
- i = stop;
6766
- continue;
6767
- }
6768
- }
6769
- if (c === "\"" || c === "'") {
6770
- const strStart = i;
6771
- i = consumeQuotedString(content, i, c);
6772
- mask(strStart + 1, i - 1);
6773
- continue;
6774
- }
6775
- if ((family === "py" || family === "rb" || family === "php") && c === "#") {
6776
- const strStart = i;
6777
- while (i < len && content[i] !== "\n") i++;
6778
- mask(strStart, i);
6779
- continue;
6780
- }
6781
- if (family === "php" && c === "/" && next === "/") {
6782
- const strStart = i;
6783
- while (i < len && content[i] !== "\n") i++;
6784
- mask(strStart, i);
6785
- continue;
6786
- }
6787
- if (family === "php" && c === "/" && next === "*") {
6788
- const strStart = i;
6789
- i += 2;
6790
- while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
6791
- if (i < len - 1) i += 2;
6792
- mask(strStart, i);
6793
- continue;
6794
- }
6795
- i++;
6796
- }
6797
- return out.join("");
6798
- };
6799
-
6800
6940
  //#endregion
6801
6941
  //#region src/engines/security/risky.ts
6802
6942
  const ev = "eval";
@@ -7071,6 +7211,7 @@ const scanSecrets = async (context) => {
7071
7211
  } catch {
7072
7212
  continue;
7073
7213
  }
7214
+ content = maskComments(content, path.extname(filePath));
7074
7215
  const relativePath = path.relative(context.rootDirectory, filePath);
7075
7216
  for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
7076
7217
  const regex = new RegExp(pattern.source, pattern.flags);
@@ -7159,6 +7300,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
7159
7300
  //#endregion
7160
7301
  //#region src/scoring/index.ts
7161
7302
  const PERFECT_SCORE = 100;
7303
+ const STYLE_RULES = new Set([
7304
+ "ai-slop/trivial-comment",
7305
+ "ai-slop/narrative-comment",
7306
+ "complexity/file-too-large",
7307
+ "complexity/function-too-long"
7308
+ ]);
7309
+ const STYLE_WEIGHT = .5;
7162
7310
  const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
7163
7311
  if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
7164
7312
  const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
@@ -7173,7 +7321,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
7173
7321
  for (const d of diagnostics) {
7174
7322
  const engineWeight = weights[d.engine] ?? 1;
7175
7323
  const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
7176
- deductions += severityPenalty * engineWeight;
7324
+ const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
7325
+ deductions += severityPenalty * engineWeight * styleFactor;
7177
7326
  }
7178
7327
  const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
7179
7328
  const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
@@ -11684,7 +11833,7 @@ const runNpmAuditFix = async (rootDir, onProgress) => {
11684
11833
  });
11685
11834
  if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout || "npm install failed after audit fix");
11686
11835
  };
11687
- const fetchLatestVersion = async (rootDir, pkgName, pm) => {
11836
+ const fetchLatestVersion$1 = async (rootDir, pkgName, pm) => {
11688
11837
  try {
11689
11838
  const result = await runSubprocess(pm, [
11690
11839
  "view",
@@ -11704,7 +11853,7 @@ const collectOverrides = async (rootDir, vulnerabilities, pm) => {
11704
11853
  const overrides = {};
11705
11854
  for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
11706
11855
  if (vuln.fixAvailable !== false || !vuln.range) continue;
11707
- const latest = await fetchLatestVersion(rootDir, pkgName, pm);
11856
+ const latest = await fetchLatestVersion$1(rootDir, pkgName, pm);
11708
11857
  if (latest) overrides[pkgName] = latest;
11709
11858
  }
11710
11859
  return overrides;
@@ -12667,6 +12816,86 @@ const trendCommand = (directory, limit) => {
12667
12816
  }));
12668
12817
  };
12669
12818
 
12819
+ //#endregion
12820
+ //#region src/update-notifier.ts
12821
+ const REGISTRY_URL = "https://registry.npmjs.org/aislop/latest";
12822
+ const CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
12823
+ const REQUEST_TIMEOUT_MS = 2e3;
12824
+ const CACHE_BASENAME = "update_check.json";
12825
+ const isUpdateNotifierDisabled = (env = process.env) => {
12826
+ if (env.AISLOP_NO_UPDATE_NOTIFIER === "1") return true;
12827
+ if (env.NO_UPDATE_NOTIFIER === "1") return true;
12828
+ if (env.DO_NOT_TRACK === "1") return true;
12829
+ return isCiEnv(env);
12830
+ };
12831
+ const resolveUpdateCachePath = (homedir = os.homedir(), env = process.env) => {
12832
+ if (process.platform === "linux" && env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "aislop", CACHE_BASENAME);
12833
+ return path.join(homedir, ".aislop", CACHE_BASENAME);
12834
+ };
12835
+ const parseVersion = (raw) => {
12836
+ const m = raw.trim().replace(/^v/, "").split(/[-+]/, 1)[0].match(/^(\d+)\.(\d+)\.(\d+)$/);
12837
+ if (!m) return null;
12838
+ return {
12839
+ major: Number(m[1]),
12840
+ minor: Number(m[2]),
12841
+ patch: Number(m[3])
12842
+ };
12843
+ };
12844
+ const isOutdated = (current, latest) => {
12845
+ const c = parseVersion(current);
12846
+ const l = parseVersion(latest);
12847
+ if (!c || !l) return false;
12848
+ if (l.major !== c.major) return l.major > c.major;
12849
+ if (l.minor !== c.minor) return l.minor > c.minor;
12850
+ return l.patch > c.patch;
12851
+ };
12852
+ const formatUpdateNotice = (current, latest) => `\nUpdate available: ${current} -> ${latest}. Run npx aislop@latest to upgrade.\n`;
12853
+ const readCache = (cachePath) => {
12854
+ try {
12855
+ const parsed = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
12856
+ if (typeof parsed?.latest === "string" && typeof parsed?.checkedAt === "number") return {
12857
+ latest: parsed.latest,
12858
+ checkedAt: parsed.checkedAt
12859
+ };
12860
+ return null;
12861
+ } catch {
12862
+ return null;
12863
+ }
12864
+ };
12865
+ const writeCache = (cachePath, cache) => {
12866
+ try {
12867
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
12868
+ fs.writeFileSync(cachePath, JSON.stringify(cache));
12869
+ return true;
12870
+ } catch {
12871
+ return false;
12872
+ }
12873
+ };
12874
+ const fetchLatestVersion = async () => {
12875
+ try {
12876
+ const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
12877
+ if (!res.ok) return null;
12878
+ const data = await res.json();
12879
+ return typeof data.version === "string" ? data.version : null;
12880
+ } catch {
12881
+ return null;
12882
+ }
12883
+ };
12884
+ const maybeNotifyUpdate = async (now = Date.now()) => {
12885
+ if (isUpdateNotifierDisabled()) return;
12886
+ if (!process.stderr.isTTY) return;
12887
+ const cachePath = resolveUpdateCachePath();
12888
+ const cache = readCache(cachePath);
12889
+ if (cache && isOutdated(APP_VERSION, cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
12890
+ if (!cache || now - cache.checkedAt > CHECK_INTERVAL_MS) {
12891
+ const latest = await fetchLatestVersion();
12892
+ if (latest) writeCache(cachePath, {
12893
+ latest,
12894
+ checkedAt: now
12895
+ });
12896
+ }
12897
+ };
12898
+
12670
12899
  //#endregion
12671
12900
  //#region src/cli.ts
12672
12901
  process.on("SIGINT", () => process.exit(0));
@@ -12920,6 +13149,7 @@ const main = async () => {
12920
13149
  fireInstalledOnce();
12921
13150
  await program.parseAsync();
12922
13151
  await flushTelemetry();
13152
+ await maybeNotifyUpdate();
12923
13153
  };
12924
13154
  main();
12925
13155