aislop 0.9.4 → 0.9.6

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/mcp.js CHANGED
@@ -11,9 +11,9 @@ import fs from "node:fs";
11
11
  import YAML from "yaml";
12
12
  import { z as z$1 } from "zod/v4";
13
13
  import micromatch from "micromatch";
14
+ import ts from "typescript";
14
15
  import { fileURLToPath } from "node:url";
15
16
  import os from "node:os";
16
- import "typescript";
17
17
  import { randomUUID } from "node:crypto";
18
18
 
19
19
  //#region src/config/defaults.ts
@@ -65,7 +65,8 @@ const DEFAULT_CONFIG = {
65
65
  failBelow: 70,
66
66
  format: "json"
67
67
  },
68
- telemetry: { enabled: true }
68
+ telemetry: { enabled: true },
69
+ rules: {}
69
70
  };
70
71
 
71
72
  //#endregion
@@ -156,6 +157,12 @@ const CiSchema = z$1.object({
156
157
  format: z$1.enum(["json"]).default("json")
157
158
  });
158
159
  const TelemetrySchema = z$1.object({ enabled: z$1.boolean().default(true) });
160
+ const RuleSeverityOverride = z$1.enum([
161
+ "error",
162
+ "warning",
163
+ "off"
164
+ ]);
165
+ const RulesSchema = z$1.record(z$1.string(), RuleSeverityOverride).default(() => ({}));
159
166
  const AislopConfigSchema = z$1.object({
160
167
  version: z$1.number().default(1),
161
168
  engines: EnginesSchema.default(() => ({
@@ -190,6 +197,7 @@ const AislopConfigSchema = z$1.object({
190
197
  format: "json"
191
198
  })),
192
199
  telemetry: TelemetrySchema.default(() => ({ enabled: true })),
200
+ rules: RulesSchema,
193
201
  exclude: z$1.array(z$1.string()).default(() => [
194
202
  "node_modules",
195
203
  ".git",
@@ -258,7 +266,7 @@ const loadConfig = (directory) => {
258
266
  //#endregion
259
267
  //#region src/utils/source-files.ts
260
268
  const MAX_BUFFER = 50 * 1024 * 1024;
261
- const SOURCE_EXTENSIONS = new Set([
269
+ const SOURCE_EXTENSIONS$1 = new Set([
262
270
  ".ts",
263
271
  ".tsx",
264
272
  ".js",
@@ -366,7 +374,7 @@ const toProjectPath = (rootDirectory, filePath) => {
366
374
  const isWithinProject = (relativePath) => relativePath.length > 0 && !relativePath.startsWith("..");
367
375
  const hasAllowedExtension = (filePath, extraExtensions) => {
368
376
  const extension = path.extname(filePath);
369
- return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
377
+ return SOURCE_EXTENSIONS$1.has(extension) || extraExtensions.has(extension);
370
378
  };
371
379
  const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
372
380
  const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
@@ -505,7 +513,7 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
505
513
 
506
514
  //#endregion
507
515
  //#region src/engines/ai-slop/abstractions.ts
508
- const JS_EXTS$1 = new Set([
516
+ const JS_EXTS$2 = new Set([
509
517
  ".ts",
510
518
  ".tsx",
511
519
  ".js",
@@ -516,11 +524,11 @@ const JS_EXTS$1 = new Set([
516
524
  const THIN_WRAPPER_PATTERNS = [
517
525
  {
518
526
  pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
519
- extensions: JS_EXTS$1
527
+ extensions: JS_EXTS$2
520
528
  },
521
529
  {
522
530
  pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
523
- extensions: JS_EXTS$1
531
+ extensions: JS_EXTS$2
524
532
  },
525
533
  {
526
534
  pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
@@ -753,6 +761,12 @@ const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
753
761
  const MAX_TRIVIAL_COMMENT_LENGTH = 60;
754
762
  const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
755
763
  const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
764
+ const isLineComment = (trimmed) => isJsComment(trimmed) || isPythonComment(trimmed);
765
+ const isInMultiLineCommentRun = (lines, index) => {
766
+ const prev = index > 0 ? lines[index - 1].trim() : "";
767
+ const next = index + 1 < lines.length ? lines[index + 1].trim() : "";
768
+ return isLineComment(prev) || isLineComment(next);
769
+ };
756
770
  /**
757
771
  * Extract just the comment text after the comment marker.
758
772
  */
@@ -805,6 +819,7 @@ const scanFileForTrivialComments = (content, relativePath, ext) => {
805
819
  const lines = content.split("\n");
806
820
  for (let i = 0; i < lines.length; i++) {
807
821
  if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
822
+ if (isInMultiLineCommentRun(lines, i)) continue;
808
823
  if (isDocCommentForDeclaration(lines, i, ext)) continue;
809
824
  diagnostics.push({
810
825
  filePath: relativePath,
@@ -966,6 +981,177 @@ const detectDeadPatterns = async (context) => {
966
981
  return diagnostics;
967
982
  };
968
983
 
984
+ //#endregion
985
+ //#region src/engines/ai-slop/defensive-patterns.ts
986
+ const JS_TS_EXTENSIONS = new Set([
987
+ ".ts",
988
+ ".tsx",
989
+ ".js",
990
+ ".jsx",
991
+ ".mjs",
992
+ ".cjs"
993
+ ]);
994
+ const TS_EXTENSIONS = new Set([".ts", ".tsx"]);
995
+ const COERCION_CTORS = {
996
+ string: "String",
997
+ number: "Number",
998
+ boolean: "Boolean"
999
+ };
1000
+ const scriptKindFor = (ext) => {
1001
+ switch (ext) {
1002
+ case ".tsx": return ts.ScriptKind.TSX;
1003
+ case ".jsx": return ts.ScriptKind.JSX;
1004
+ case ".js":
1005
+ case ".mjs":
1006
+ case ".cjs": return ts.ScriptKind.JS;
1007
+ default: return ts.ScriptKind.TS;
1008
+ }
1009
+ };
1010
+ const lineFor = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
1011
+ const makeDiagnostic = (filePath, line, rule, message, help) => ({
1012
+ filePath,
1013
+ engine: "ai-slop",
1014
+ rule,
1015
+ severity: "warning",
1016
+ message,
1017
+ help,
1018
+ line,
1019
+ column: 0,
1020
+ category: "AI Slop",
1021
+ fixable: false
1022
+ });
1023
+ const isFunctionNode = (node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
1024
+ const primitiveKindOf = (node) => {
1025
+ if (!node) return null;
1026
+ switch (node.kind) {
1027
+ case ts.SyntaxKind.StringKeyword: return "string";
1028
+ case ts.SyntaxKind.NumberKeyword: return "number";
1029
+ case ts.SyntaxKind.BooleanKeyword: return "boolean";
1030
+ default: return null;
1031
+ }
1032
+ };
1033
+ const hasExportModifier = (node) => node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
1034
+ const primitiveParamsOf = (node) => {
1035
+ const params = /* @__PURE__ */ new Map();
1036
+ for (const param of node.parameters) {
1037
+ if (!ts.isIdentifier(param.name)) continue;
1038
+ const kind = primitiveKindOf(param.type);
1039
+ if (!kind) continue;
1040
+ params.set(param.name.text, kind);
1041
+ }
1042
+ return params;
1043
+ };
1044
+ const isRethrowStatement = (statement, errorName) => ts.isThrowStatement(statement) && statement.expression !== void 0 && ts.isIdentifier(statement.expression) && statement.expression.text === errorName;
1045
+ const isPromiseRejectRethrow = (statement, errorName) => {
1046
+ if (!ts.isReturnStatement(statement) || !statement.expression) return false;
1047
+ const expression = statement.expression;
1048
+ if (!ts.isCallExpression(expression) || expression.arguments.length !== 1) return false;
1049
+ const [arg] = expression.arguments;
1050
+ if (!ts.isIdentifier(arg) || arg.text !== errorName) return false;
1051
+ if (!ts.isPropertyAccessExpression(expression.expression)) return false;
1052
+ const target = expression.expression;
1053
+ return ts.isIdentifier(target.expression) && target.expression.text === "Promise" && target.name.text === "reject";
1054
+ };
1055
+ const detectRedundantTryCatch = (sourceFile, relativePath) => {
1056
+ const diagnostics = [];
1057
+ const visit = (node) => {
1058
+ if (ts.isTryStatement(node) && node.catchClause && !node.finallyBlock) {
1059
+ const catchNameNode = node.catchClause.variableDeclaration?.name;
1060
+ const [onlyStatement] = node.catchClause.block.statements;
1061
+ if (catchNameNode && ts.isIdentifier(catchNameNode) && node.catchClause.block.statements.length === 1 && onlyStatement && (isRethrowStatement(onlyStatement, catchNameNode.text) || isPromiseRejectRethrow(onlyStatement, catchNameNode.text))) diagnostics.push(makeDiagnostic(relativePath, lineFor(sourceFile, node.catchClause), "ai-slop/redundant-try-catch", "Catch block only rethrows the same error", "Remove the try/catch or add useful context, cleanup, or recovery. Rethrowing unchanged errors is usually defensive agent noise."));
1062
+ }
1063
+ ts.forEachChild(node, visit);
1064
+ };
1065
+ visit(sourceFile);
1066
+ return diagnostics;
1067
+ };
1068
+ const detectPrimitiveCoercions = (sourceFile, relativePath) => {
1069
+ const diagnostics = [];
1070
+ const scanFunctionBody = (node, params) => {
1071
+ const body = node.body;
1072
+ if (!body || params.size === 0) return;
1073
+ const visitBody = (child) => {
1074
+ if (child !== body && isFunctionNode(child)) return;
1075
+ if (ts.isCallExpression(child) && ts.isIdentifier(child.expression)) {
1076
+ const [arg] = child.arguments;
1077
+ if (arg && ts.isIdentifier(arg)) {
1078
+ const primitive = params.get(arg.text);
1079
+ if (primitive && child.expression.text === COERCION_CTORS[primitive]) diagnostics.push(makeDiagnostic(relativePath, lineFor(sourceFile, child), "ai-slop/redundant-type-coercion", `Parameter '${arg.text}' is already typed as ${primitive} but is coerced again`, "Trust the typed boundary or validate unknown input before this function. Re-coercing already typed parameters is usually defensive agent noise."));
1080
+ }
1081
+ }
1082
+ ts.forEachChild(child, visitBody);
1083
+ };
1084
+ visitBody(body);
1085
+ };
1086
+ const visit = (node) => {
1087
+ if (isFunctionNode(node)) scanFunctionBody(node, primitiveParamsOf(node));
1088
+ ts.forEachChild(node, visit);
1089
+ };
1090
+ visit(sourceFile);
1091
+ return diagnostics;
1092
+ };
1093
+ const normalizedTypeDeclaration = (sourceFile, node) => sourceFile.text.slice(node.getStart(sourceFile), node.getEnd()).replace(/\bexport\b/g, "").replace(/\bdeclare\b/g, "").replace(/\s+/g, " ").trim();
1094
+ const exportedTypesOf = (parsed) => {
1095
+ const declarations = [];
1096
+ const visit = (node) => {
1097
+ if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && hasExportModifier(node)) declarations.push({
1098
+ name: node.name.text,
1099
+ signature: normalizedTypeDeclaration(parsed.sourceFile, node),
1100
+ filePath: parsed.relativePath,
1101
+ line: lineFor(parsed.sourceFile, node)
1102
+ });
1103
+ ts.forEachChild(node, visit);
1104
+ };
1105
+ visit(parsed.sourceFile);
1106
+ return declarations;
1107
+ };
1108
+ const duplicateTypeKeyOf = (declaration) => `${declaration.name}\0${declaration.signature}`;
1109
+ const detectDuplicateExportedTypes = (parsedSources) => {
1110
+ const diagnostics = [];
1111
+ const seen = /* @__PURE__ */ new Map();
1112
+ for (const parsed of parsedSources) {
1113
+ if (!TS_EXTENSIONS.has(parsed.ext)) continue;
1114
+ for (const declaration of exportedTypesOf(parsed)) {
1115
+ const key = duplicateTypeKeyOf(declaration);
1116
+ const previous = seen.get(key);
1117
+ if (!previous) {
1118
+ seen.set(key, declaration);
1119
+ continue;
1120
+ }
1121
+ if (previous.filePath === declaration.filePath) continue;
1122
+ diagnostics.push(makeDiagnostic(declaration.filePath, declaration.line, "ai-slop/duplicate-type-declaration", `Exported type '${declaration.name}' duplicates an existing declaration`, `Reuse or import the existing type from ${previous.filePath} instead of re-declaring the same shape in another file.`));
1123
+ }
1124
+ }
1125
+ return diagnostics;
1126
+ };
1127
+ const detectDefensivePatterns = async (context) => {
1128
+ const diagnostics = [];
1129
+ const parsedSources = [];
1130
+ for (const filePath of getSourceFiles(context)) {
1131
+ if (isAutoGenerated(filePath)) continue;
1132
+ let content;
1133
+ try {
1134
+ content = fs.readFileSync(filePath, "utf-8");
1135
+ } catch {
1136
+ continue;
1137
+ }
1138
+ const relativePath = path.relative(context.rootDirectory, filePath);
1139
+ if (isNonProductionPath(relativePath)) continue;
1140
+ const ext = path.extname(filePath);
1141
+ if (!JS_TS_EXTENSIONS.has(ext)) continue;
1142
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindFor(ext));
1143
+ parsedSources.push({
1144
+ sourceFile,
1145
+ relativePath,
1146
+ ext
1147
+ });
1148
+ diagnostics.push(...detectRedundantTryCatch(sourceFile, relativePath));
1149
+ if (TS_EXTENSIONS.has(ext)) diagnostics.push(...detectPrimitiveCoercions(sourceFile, relativePath));
1150
+ }
1151
+ diagnostics.push(...detectDuplicateExportedTypes(parsedSources));
1152
+ return diagnostics;
1153
+ };
1154
+
969
1155
  //#endregion
970
1156
  //#region src/engines/ai-slop/duplicate-imports.ts
971
1157
  const JS_EXTENSIONS$2 = new Set([
@@ -976,7 +1162,16 @@ const JS_EXTENSIONS$2 = new Set([
976
1162
  ".mjs",
977
1163
  ".cjs"
978
1164
  ]);
979
- const IMPORT_FROM_RE = /^\s*import\s+[^;]*?from\s+["']([^"']+)["']/;
1165
+ const IMPORT_FROM_RE = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
1166
+ const TYPE_ONLY_RE = /^\s*type\b/;
1167
+ const VALUE_BINDING_RE = /\{([^}]*)\}/;
1168
+ const isTypeOnly = (clause) => {
1169
+ if (TYPE_ONLY_RE.test(clause)) return true;
1170
+ const braces = VALUE_BINDING_RE.exec(clause);
1171
+ if (!braces) return false;
1172
+ const members = braces[1].split(",").map((member) => member.trim()).filter((member) => member.length > 0);
1173
+ return members.length > 0 && members.every((member) => /^type\b/.test(member));
1174
+ };
980
1175
  const extractImportLines = (content) => {
981
1176
  const lines = content.split("\n");
982
1177
  const results = [];
@@ -985,8 +1180,9 @@ const extractImportLines = (content) => {
985
1180
  const match = IMPORT_FROM_RE.exec(line);
986
1181
  if (!match) continue;
987
1182
  results.push({
988
- spec: match[1],
989
- line: i + 1
1183
+ spec: match[2],
1184
+ line: i + 1,
1185
+ typeOnly: isTypeOnly(match[1])
990
1186
  });
991
1187
  }
992
1188
  return results;
@@ -1005,14 +1201,16 @@ const detectDuplicateImports = async (context) => {
1005
1201
  }
1006
1202
  const imports = extractImportLines(content);
1007
1203
  if (imports.length < 2) continue;
1008
- const bySpec = /* @__PURE__ */ new Map();
1204
+ const byBucket = /* @__PURE__ */ new Map();
1009
1205
  for (const imp of imports) {
1010
- const list = bySpec.get(imp.spec) ?? [];
1206
+ const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
1207
+ const list = byBucket.get(key) ?? [];
1011
1208
  list.push(imp);
1012
- bySpec.set(imp.spec, list);
1209
+ byBucket.set(key, list);
1013
1210
  }
1014
1211
  const relPath = path.relative(context.rootDirectory, filePath);
1015
- for (const [spec, occurrences] of bySpec) {
1212
+ for (const occurrences of byBucket.values()) {
1213
+ const { spec } = occurrences[0];
1016
1214
  if (occurrences.length < 2) continue;
1017
1215
  for (const dup of occurrences.slice(1)) {
1018
1216
  const firstLine = occurrences[0].line;
@@ -1217,6 +1415,141 @@ const detectGoPatterns = async (context) => {
1217
1415
  return diagnostics;
1218
1416
  };
1219
1417
 
1418
+ //#endregion
1419
+ //#region src/engines/ai-slop/hardcoded-config.ts
1420
+ const SOURCE_EXTENSIONS = new Set([
1421
+ ".ts",
1422
+ ".tsx",
1423
+ ".js",
1424
+ ".jsx",
1425
+ ".mjs",
1426
+ ".cjs",
1427
+ ".py",
1428
+ ".go",
1429
+ ".rs",
1430
+ ".rb",
1431
+ ".java",
1432
+ ".php"
1433
+ ]);
1434
+ const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
1435
+ const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
1436
+ const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
1437
+ const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
1438
+ const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
1439
+ const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
1440
+ const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
1441
+ const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
1442
+ const PLACEHOLDER_HOSTS = new Set([
1443
+ "example.com",
1444
+ "example.org",
1445
+ "example.net"
1446
+ ]);
1447
+ const LOOPBACK_HOSTS = new Set([
1448
+ "localhost",
1449
+ "127.0.0.1",
1450
+ "0.0.0.0",
1451
+ "::1"
1452
+ ]);
1453
+ const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
1454
+ const HARDCODED_URL_FINDING = {
1455
+ rule: "ai-slop/hardcoded-url",
1456
+ message: "Hardcoded environment URL in production code",
1457
+ help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
1458
+ };
1459
+ const HARDCODED_ID_FINDING = {
1460
+ rule: "ai-slop/hardcoded-id",
1461
+ message: "Hardcoded provider/project ID in production code",
1462
+ help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
1463
+ };
1464
+ const makeFinding = (filePath, line, spec) => ({
1465
+ filePath,
1466
+ engine: "ai-slop",
1467
+ rule: spec.rule,
1468
+ severity: "warning",
1469
+ message: spec.message,
1470
+ help: spec.help,
1471
+ line,
1472
+ column: 0,
1473
+ category: "AI Slop",
1474
+ fixable: false
1475
+ });
1476
+ const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
1477
+ const commentStartsBefore = (line, index, ext) => {
1478
+ const prefix = line.slice(0, index);
1479
+ if (ext === ".py" || ext === ".rb") return prefix.includes("#");
1480
+ if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
1481
+ return prefix.includes("//") || prefix.includes("/*");
1482
+ };
1483
+ const safeUrlHost = (urlText) => {
1484
+ try {
1485
+ return new URL(urlText).hostname.toLowerCase();
1486
+ } catch {
1487
+ return null;
1488
+ }
1489
+ };
1490
+ const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
1491
+ const shouldFlagUrlLiteral = (line, urlText) => {
1492
+ if (isEnvBackedLine(line)) return false;
1493
+ const host = safeUrlHost(urlText);
1494
+ if (!host) return false;
1495
+ if (PLACEHOLDER_HOSTS.has(host)) return false;
1496
+ if (LOOPBACK_HOSTS.has(host)) return false;
1497
+ if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
1498
+ return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
1499
+ };
1500
+ const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
1501
+ const hasUsefulIdShape = (value) => {
1502
+ if (PLACEHOLDER_ID_RE.test(value)) return false;
1503
+ if (ENV_VAR_NAME_RE.test(value)) return false;
1504
+ if (/^https?:\/\//i.test(value)) return false;
1505
+ if (/^[A-Za-z]+$/.test(value)) return false;
1506
+ return /[0-9]/.test(value);
1507
+ };
1508
+ const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
1509
+ const diagnostics = [];
1510
+ if (isCommentOnlyLine(line.trim())) return diagnostics;
1511
+ URL_LITERAL_RE.lastIndex = 0;
1512
+ let urlMatch;
1513
+ while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
1514
+ const urlText = urlMatch[2];
1515
+ if (commentStartsBefore(line, urlMatch.index, ext)) continue;
1516
+ if (!shouldFlagUrlLiteral(line, urlText)) continue;
1517
+ diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
1518
+ }
1519
+ if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
1520
+ ID_LITERAL_RE.lastIndex = 0;
1521
+ let idMatch;
1522
+ while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
1523
+ const value = idMatch[2];
1524
+ if (commentStartsBefore(line, idMatch.index, ext)) continue;
1525
+ if (!hasUsefulIdShape(value)) continue;
1526
+ diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
1527
+ }
1528
+ return diagnostics;
1529
+ };
1530
+ const scanFileForConfigLiterals = (content, relativePath, ext) => {
1531
+ if (!SOURCE_EXTENSIONS.has(ext)) return [];
1532
+ if (isNonProductionPath(relativePath)) return [];
1533
+ if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
1534
+ return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
1535
+ };
1536
+ const detectHardcodedConfigLiterals = async (context) => {
1537
+ const diagnostics = [];
1538
+ for (const filePath of getSourceFiles(context)) {
1539
+ if (isAutoGenerated(filePath)) continue;
1540
+ let content;
1541
+ try {
1542
+ content = fs.readFileSync(filePath, "utf-8");
1543
+ } catch {
1544
+ continue;
1545
+ }
1546
+ const relativePath = path.relative(context.rootDirectory, filePath);
1547
+ const ext = path.extname(filePath);
1548
+ diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
1549
+ }
1550
+ return diagnostics;
1551
+ };
1552
+
1220
1553
  //#endregion
1221
1554
  //#region src/engines/ai-slop/js-import-aliases.ts
1222
1555
  const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
@@ -1484,23 +1817,88 @@ const PYTHON_STDLIB = new Set([
1484
1817
  "zoneinfo"
1485
1818
  ]);
1486
1819
  const PYTHON_IMPORT_TO_PIP = {
1487
- yaml: "pyyaml",
1488
- PIL: "pillow",
1489
- dateutil: "python-dateutil",
1490
- cv2: "opencv-python",
1491
- sklearn: "scikit-learn",
1492
- bs4: "beautifulsoup4",
1493
- typing_extensions: "typing-extensions",
1494
- google: "google-api-python-client",
1495
- jose: "python-jose",
1496
- jwt: "pyjwt",
1497
- OpenSSL: "pyopenssl",
1498
- magic: "python-magic",
1499
- docx: "python-docx",
1500
- pptx: "python-pptx",
1501
- git: "gitpython",
1502
- socks: "pysocks",
1503
- redis: "redis"
1820
+ yaml: ["pyyaml"],
1821
+ PIL: ["pillow"],
1822
+ dateutil: ["python-dateutil"],
1823
+ cv2: [
1824
+ "opencv-python",
1825
+ "opencv-python-headless",
1826
+ "opencv-contrib-python"
1827
+ ],
1828
+ sklearn: ["scikit-learn"],
1829
+ bs4: ["beautifulsoup4"],
1830
+ typing_extensions: ["typing-extensions"],
1831
+ dotenv: ["python-dotenv"],
1832
+ genai: ["google-genai"],
1833
+ google: [
1834
+ "google-genai",
1835
+ "google-generativeai",
1836
+ "google-api-python-client",
1837
+ "google-cloud-storage",
1838
+ "google-cloud-aiplatform",
1839
+ "google-auth",
1840
+ "protobuf"
1841
+ ],
1842
+ jose: ["python-jose"],
1843
+ jwt: ["pyjwt"],
1844
+ OpenSSL: ["pyopenssl"],
1845
+ Crypto: ["pycryptodome", "pycryptodomex"],
1846
+ Cryptodome: ["pycryptodomex", "pycryptodome"],
1847
+ magic: ["python-magic"],
1848
+ docx: ["python-docx"],
1849
+ pptx: ["python-pptx"],
1850
+ git: ["gitpython"],
1851
+ socks: ["pysocks"],
1852
+ psycopg2: ["psycopg2-binary", "psycopg2"],
1853
+ redis: ["redis"],
1854
+ cairo: ["pycairo"],
1855
+ serial: ["pyserial"],
1856
+ usb: ["pyusb"],
1857
+ gi: ["pygobject"],
1858
+ Xlib: ["python-xlib"],
1859
+ ldap: ["python-ldap"],
1860
+ slugify: ["python-slugify"],
1861
+ memcache: ["python-memcached"],
1862
+ dns: ["dnspython"],
1863
+ attr: ["attrs"],
1864
+ attrs: ["attrs"],
1865
+ zoneinfo_data: ["tzdata"],
1866
+ pkg_resources: ["setuptools"],
1867
+ setuptools: ["setuptools"],
1868
+ wx: ["wxpython"],
1869
+ skimage: ["scikit-image"],
1870
+ OpenGL: ["pyopengl"],
1871
+ win32api: ["pywin32"],
1872
+ win32con: ["pywin32"],
1873
+ win32com: ["pywin32"],
1874
+ pythoncom: ["pywin32"],
1875
+ pywintypes: ["pywin32"],
1876
+ rest_framework: ["djangorestframework"],
1877
+ allauth: ["django-allauth"],
1878
+ corsheaders: ["django-cors-headers"],
1879
+ debug_toolbar: ["django-debug-toolbar"],
1880
+ environ: ["django-environ"],
1881
+ flask_cors: ["flask-cors"],
1882
+ flask_sqlalchemy: ["flask-sqlalchemy"],
1883
+ flask_migrate: ["flask-migrate"],
1884
+ flask_login: ["flask-login"],
1885
+ jwt_extended: ["flask-jwt-extended"],
1886
+ dateparser: ["dateparser"],
1887
+ yaml_include: ["pyyaml-include"],
1888
+ lxml_html_clean: ["lxml-html-clean"],
1889
+ grpc: ["grpcio"],
1890
+ grpc_status: ["grpcio-status"],
1891
+ google_crc32c: ["google-crc32c"],
1892
+ pkg_about: ["pkg-about"],
1893
+ mpl_toolkits: ["matplotlib"],
1894
+ dotmap: ["dotmap"],
1895
+ pydantic_settings: ["pydantic-settings"],
1896
+ telegram: ["python-telegram-bot"],
1897
+ discord: ["discord-py"],
1898
+ nacl: ["pynacl"],
1899
+ jwcrypto: ["jwcrypto"],
1900
+ humanfriendly: ["humanfriendly"],
1901
+ multipart: ["python-multipart"]
1504
1902
  };
1505
1903
 
1506
1904
  //#endregion
@@ -1539,6 +1937,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
1539
1937
  const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
1540
1938
  if (m) addPyDep(pyDeps, m[1]);
1541
1939
  }
1940
+ const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
1941
+ if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
1542
1942
  const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
1543
1943
  let match = poetryRe.exec(content);
1544
1944
  while (match !== null) {
@@ -1737,6 +2137,11 @@ const packageNameFromImport = (spec) => {
1737
2137
  }
1738
2138
  return spec.split("/")[0];
1739
2139
  };
2140
+ const typesPackageName = (pkg) => {
2141
+ if (pkg.startsWith("@types/")) return pkg;
2142
+ if (pkg.startsWith("@")) return `@types/${pkg.slice(1).replace("/", "__")}`;
2143
+ return `@types/${pkg}`;
2144
+ };
1740
2145
  const STATIC_IMPORT_RE = /^\s*import\s+(?:[\w*{},\s]+\s+from\s+)?["']([^"']+)["']/;
1741
2146
  const DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*["']([^"']+)["']/g;
1742
2147
  const extractJsImports = (content) => {
@@ -1801,15 +2206,16 @@ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
1801
2206
  const realPkg = pkg.slice(7);
1802
2207
  if (manifest.jsDeps.has(realPkg)) return null;
1803
2208
  }
2209
+ if (manifest.jsDeps.has(typesPackageName(pkg))) return null;
1804
2210
  return pkg;
1805
2211
  };
2212
+ const normalizePyName = (name) => name.toLowerCase().replace(/_/g, "-");
1806
2213
  const checkPyImport = (spec, manifest) => {
1807
2214
  const root = spec.split(".")[0];
1808
2215
  if (PYTHON_STDLIB.has(root)) return null;
1809
- const normalized = root.toLowerCase().replace(/_/g, "-");
2216
+ const normalized = normalizePyName(root);
1810
2217
  if (manifest.pyDeps.has(normalized)) return null;
1811
- const pipName = PYTHON_IMPORT_TO_PIP[root];
1812
- if (pipName && manifest.pyDeps.has(pipName)) return null;
2218
+ if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
1813
2219
  return root;
1814
2220
  };
1815
2221
  const detectHallucinatedImports = async (context) => {
@@ -1959,6 +2365,85 @@ const collectBlocks = (sourceLines, syntax) => {
1959
2365
  return blocks;
1960
2366
  };
1961
2367
 
2368
+ //#endregion
2369
+ //#region src/engines/ai-slop/meta-comment.ts
2370
+ const PLAN_REFERENCE_RES = [
2371
+ /\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
2372
+ /\bstep\s+\d+\s+of\s+the\s+plan\b/i,
2373
+ /\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
2374
+ /\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
2375
+ /\bfrom\s+the\s+(?:task|todo|plan|spec|ticket|prompt|requirements?)\b/i,
2376
+ /\bimplement(?:ing|s|ed)?\s+use\s*case\s+\d*/i,
2377
+ /\b(?:requirements?\s+doc|requirement\s+\d+)\b/i,
2378
+ /\bas\s+(?:instructed|specified|outlined)\s+(?:above|below|in\s+the)\b/i
2379
+ ];
2380
+ const BEFORE_AFTER_RES = [
2381
+ /\bpreviously[,:]?\s+(?:this|we|it|the)\b/i,
2382
+ /\bused\s+to\s+(?:be|use|call|return|do|have|rely)\b/i,
2383
+ /\bchanged\s+(?:\w+\s+){0,3}from\s+.+\bto\b/i,
2384
+ /\bno\s+longer\s+(?:needed|used|required|necessary|calls?|returns?|does)\b/i,
2385
+ /\bthis\s+was\s+.+\bbut\s+now\b/i,
2386
+ /\bwe\s+(?:now|used\s+to)\s+(?:no\s+longer\s+)?(?:use|call|return|do|have)\b/i,
2387
+ /\breplaced\s+the\s+(?:old|previous|former)\b/i,
2388
+ /\b(?:was|were)\s+(?:renamed|moved|removed|refactored|extracted)\s+(?:from|to|out\s+of)\b/i
2389
+ ];
2390
+ const WHY_OR_TODO_RE = /\b(?:because|since|otherwise|todo|fixme|hack|note:|reason:|workaround|see\s+(?:issue|#))\b/i;
2391
+ const looksLikeLicenseHeader$1 = (block) => {
2392
+ if (block.startLine !== 1) return false;
2393
+ const text = block.rawLines.join(" ").toLowerCase();
2394
+ return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
2395
+ };
2396
+ const looksLikeSuppressDirective$1 = (block) => block.rawLines.some((line) => /\b(?:biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(line));
2397
+ const matchMetaSignal = (block) => {
2398
+ if (looksLikeLicenseHeader$1(block)) return null;
2399
+ if (looksLikeSuppressDirective$1(block)) return null;
2400
+ if (block.kind === "jsdoc" && block.hasMeaningfulJsdocTag) return null;
2401
+ if (block.isRustDoc) return null;
2402
+ const joined = block.prose.join(" ");
2403
+ if (joined.trim().length === 0) return null;
2404
+ if (WHY_OR_TODO_RE.test(joined)) return null;
2405
+ if (PLAN_REFERENCE_RES.some((re) => re.test(joined))) return "plan/process reference";
2406
+ if (BEFORE_AFTER_RES.some((re) => re.test(joined))) return "before/after state narration";
2407
+ return null;
2408
+ };
2409
+ const detectMetaComments = async (context) => {
2410
+ const files = getSourceFiles(context);
2411
+ const diagnostics = [];
2412
+ for (const filePath of files) {
2413
+ const ext = path.extname(filePath);
2414
+ if (!SUPPORTED_EXTS.has(ext)) continue;
2415
+ if (isAutoGenerated(filePath)) continue;
2416
+ const syntax = getCommentSyntax(ext);
2417
+ if (!syntax) continue;
2418
+ const relativePath = path.relative(context.rootDirectory, filePath);
2419
+ if (isNonProductionPath(relativePath)) continue;
2420
+ let content;
2421
+ try {
2422
+ content = fs.readFileSync(filePath, "utf-8");
2423
+ } catch {
2424
+ continue;
2425
+ }
2426
+ const blocks = collectBlocks(content.split("\n"), syntax);
2427
+ for (const block of blocks) {
2428
+ const reason = matchMetaSignal(block);
2429
+ if (!reason) continue;
2430
+ diagnostics.push({
2431
+ filePath: relativePath,
2432
+ engine: "ai-slop",
2433
+ rule: "ai-slop/meta-comment",
2434
+ severity: "warning",
2435
+ message: `Meta/plan comment (${reason})`,
2436
+ help: "Remove — references to the build plan or before/after code state belong in PR descriptions and commit messages, not source.",
2437
+ line: block.startLine,
2438
+ column: 0,
2439
+ category: "Comments",
2440
+ fixable: false
2441
+ });
2442
+ }
2443
+ }
2444
+ return diagnostics;
2445
+ };
2446
+
1962
2447
  //#endregion
1963
2448
  //#region src/engines/ai-slop/narrative-comments.ts
1964
2449
  const looksLikeDeclarationPreamble = (nextLine, ext) => {
@@ -2567,6 +3052,143 @@ const detectRustPatterns = async (context) => {
2567
3052
  return diagnostics;
2568
3053
  };
2569
3054
 
3055
+ //#endregion
3056
+ //#region src/engines/ai-slop/silent-recovery.ts
3057
+ const JS_EXTS$1 = new Set([
3058
+ ".ts",
3059
+ ".tsx",
3060
+ ".js",
3061
+ ".jsx",
3062
+ ".mjs",
3063
+ ".cjs"
3064
+ ]);
3065
+ const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
3066
+ const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
3067
+ const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
3068
+ const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
3069
+ const extractCatchBody = (content, openBraceIndex) => {
3070
+ let depth = 0;
3071
+ let inString = null;
3072
+ for (let i = openBraceIndex; i < content.length; i += 1) {
3073
+ const ch = content[i];
3074
+ const prev = content[i - 1];
3075
+ if (inString) {
3076
+ if (ch === inString && prev !== "\\") inString = null;
3077
+ continue;
3078
+ }
3079
+ if (ch === "\"" || ch === "'" || ch === "`") {
3080
+ inString = ch;
3081
+ continue;
3082
+ }
3083
+ if (ch === "{") depth += 1;
3084
+ else if (ch === "}") {
3085
+ depth -= 1;
3086
+ if (depth === 0) return content.slice(openBraceIndex + 1, i);
3087
+ }
3088
+ }
3089
+ return null;
3090
+ };
3091
+ const isLogOnlyBody = (body) => {
3092
+ const statements = stripBlockComments(body).split("\n").map((line) => line.replace(/\/\/.*$/, "").trim()).filter((line) => line.length > 0 && line !== ";");
3093
+ if (statements.length === 0) return false;
3094
+ if (statements.some((line) => HANDLING_TOKEN_RE.test(line))) return false;
3095
+ let sawLog = false;
3096
+ for (const statement of statements) {
3097
+ const normalized = statement.replace(/;+$/, "");
3098
+ if (LOG_STATEMENT_RE.test(normalized)) {
3099
+ sawLog = true;
3100
+ continue;
3101
+ }
3102
+ if (/^[\w$"'`{[(),.\s+:-]+$/.test(normalized) && !/[=(]\s*(?:async\s+)?\(/.test(normalized)) continue;
3103
+ return false;
3104
+ }
3105
+ return sawLog;
3106
+ };
3107
+ const detectJsSilentRecovery = (content, relPath) => {
3108
+ const out = [];
3109
+ CATCH_HEAD_RE.lastIndex = 0;
3110
+ let match;
3111
+ while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
3112
+ const body = extractCatchBody(content, match.index + match[0].length - 1);
3113
+ if (body === null) continue;
3114
+ if (!isLogOnlyBody(body)) continue;
3115
+ const line = content.slice(0, match.index).split("\n").length;
3116
+ out.push({
3117
+ filePath: relPath,
3118
+ engine: "ai-slop",
3119
+ rule: "ai-slop/silent-recovery",
3120
+ severity: "warning",
3121
+ message: "Catch only logs then continues, leaving execution in a possibly broken state",
3122
+ help: "Handle the error: rethrow, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
3123
+ line,
3124
+ column: 0,
3125
+ category: "AI Slop",
3126
+ fixable: false
3127
+ });
3128
+ }
3129
+ return out;
3130
+ };
3131
+ const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
3132
+ const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
3133
+ const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
3134
+ const detectPySilentRecovery = (content, relPath) => {
3135
+ const out = [];
3136
+ const lines = content.split("\n");
3137
+ for (let i = 0; i < lines.length; i += 1) {
3138
+ const exceptMatch = PY_EXCEPT_RE.exec(lines[i]);
3139
+ if (!exceptMatch) continue;
3140
+ const indent = exceptMatch[1].length;
3141
+ const bodyLines = [];
3142
+ let j = i + 1;
3143
+ for (; j < lines.length; j += 1) {
3144
+ const raw = lines[j];
3145
+ if (raw.trim() === "") continue;
3146
+ if (raw.length - raw.trimStart().length <= indent) break;
3147
+ bodyLines.push(raw.trim());
3148
+ }
3149
+ if (bodyLines.length === 0) continue;
3150
+ if (bodyLines.some((line) => PY_HANDLING_TOKEN_RE.test(line))) continue;
3151
+ if (bodyLines.some((line) => line === "pass")) continue;
3152
+ const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
3153
+ const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
3154
+ if (!allLogs || !sawLog) continue;
3155
+ out.push({
3156
+ filePath: relPath,
3157
+ engine: "ai-slop",
3158
+ rule: "ai-slop/silent-recovery",
3159
+ severity: "warning",
3160
+ message: "except only logs then continues, leaving execution in a possibly broken state",
3161
+ help: "Handle the error: re-raise, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
3162
+ line: i + 1,
3163
+ column: 0,
3164
+ category: "AI Slop",
3165
+ fixable: false
3166
+ });
3167
+ }
3168
+ return out;
3169
+ };
3170
+ const detectSilentRecovery = async (context) => {
3171
+ const files = getSourceFiles(context);
3172
+ const diagnostics = [];
3173
+ for (const filePath of files) {
3174
+ if (isAutoGenerated(filePath)) continue;
3175
+ const ext = path.extname(filePath);
3176
+ const isJs = JS_EXTS$1.has(ext);
3177
+ if (!isJs && !(ext === ".py")) continue;
3178
+ const relPath = path.relative(context.rootDirectory, filePath);
3179
+ if (isNonProductionPath(relPath)) continue;
3180
+ let content;
3181
+ try {
3182
+ content = fs.readFileSync(filePath, "utf-8");
3183
+ } catch {
3184
+ continue;
3185
+ }
3186
+ if (isJs) diagnostics.push(...detectJsSilentRecovery(content, relPath));
3187
+ else diagnostics.push(...detectPySilentRecovery(content, relPath));
3188
+ }
3189
+ return diagnostics;
3190
+ };
3191
+
2570
3192
  //#endregion
2571
3193
  //#region src/engines/ai-slop/unused-imports.ts
2572
3194
  const JS_EXTENSIONS = new Set([
@@ -2755,15 +3377,19 @@ const aiSlopEngine = {
2755
3377
  const results = await Promise.allSettled([
2756
3378
  detectTrivialComments(context),
2757
3379
  detectSwallowedExceptions(context),
3380
+ detectDefensivePatterns(context),
2758
3381
  detectOverAbstraction(context),
2759
3382
  detectDeadPatterns(context),
2760
3383
  detectUnusedImports(context),
2761
3384
  detectNarrativeComments(context),
2762
3385
  detectDuplicateImports(context),
3386
+ detectHardcodedConfigLiterals(context),
2763
3387
  detectPythonPatterns(context),
2764
3388
  detectGoPatterns(context),
2765
3389
  detectRustPatterns(context),
2766
- detectHallucinatedImports(context)
3390
+ detectHallucinatedImports(context),
3391
+ detectSilentRecovery(context),
3392
+ detectMetaComments(context)
2767
3393
  ]);
2768
3394
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
2769
3395
  return {
@@ -5164,7 +5790,7 @@ const RISKY_PATTERNS = [
5164
5790
  help: "Use JSON, MessagePack, or other safe serialization formats for untrusted data"
5165
5791
  },
5166
5792
  {
5167
- pattern: new RegExp(`\\bexec\\s*\\(`, "g"),
5793
+ pattern: new RegExp(`(?<![\\w.>:\\\\])\\bexec\\s*\\(`, "g"),
5168
5794
  extensions: [".py"],
5169
5795
  name: "python-exec",
5170
5796
  message: "Use of exec() can execute arbitrary code",
@@ -5842,7 +6468,7 @@ const handleAislopBaseline = (input) => {
5842
6468
 
5843
6469
  //#endregion
5844
6470
  //#region src/version.ts
5845
- const APP_VERSION = "0.9.4";
6471
+ const APP_VERSION = "0.9.6";
5846
6472
 
5847
6473
  //#endregion
5848
6474
  //#region src/telemetry/env.ts
@@ -5974,8 +6600,8 @@ const redactProperties = (props) => {
5974
6600
 
5975
6601
  //#endregion
5976
6602
  //#region src/telemetry/client.ts
5977
- const POSTHOG_HOST = "https://eu.i.posthog.com";
5978
- const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
6603
+ const POSTHOG_HOST = process.env.AISLOP_POSTHOG_HOST ?? "https://eu.i.posthog.com";
6604
+ const POSTHOG_KEY = process.env.AISLOP_POSTHOG_KEY ?? "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
5979
6605
  const SCHEMA_VERSION = "v2";
5980
6606
  const REQUEST_TIMEOUT_MS = 3e3;
5981
6607
  const isTelemetryDisabled = (config) => {