aislop 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
2
+ import { createRequire, isBuiltin } from "node:module";
3
3
  import { Command } from "commander";
4
4
  import os from "node:os";
5
5
  import fs from "node:fs";
@@ -34,7 +34,11 @@ var __exportAll = (all, no_symbols) => {
34
34
 
35
35
  //#endregion
36
36
  //#region src/hooks/feedback.ts
37
+ const fingerprintFinding = (f) => `${f.file}:${f.line}:${f.ruleId}`;
37
38
  const MAX_FINDINGS = 20;
39
+ const MAX_NEW_SINCE_BASELINE = 10;
40
+ const REVIEW_TOP_N = 3;
41
+ const REGRESSION_FLAG_THRESHOLD = 5;
38
42
  const toFinding = (d, rootDirectory) => {
39
43
  if (d.severity !== "error" && d.severity !== "warning") return null;
40
44
  const file = path.isAbsolute(d.filePath) ? path.relative(rootDirectory, d.filePath) : d.filePath;
@@ -64,6 +68,42 @@ const buildNextSteps = (findings) => {
64
68
  }
65
69
  return steps;
66
70
  };
71
+ const buildSuggestedActions = (diagnostics, findings, regressed, delta) => {
72
+ const actions = [];
73
+ const fixableDiags = diagnostics.filter((d) => d.fixable);
74
+ if (fixableDiags.length > 0) {
75
+ const ruleIds = Array.from(new Set(fixableDiags.map((d) => d.rule)));
76
+ actions.push({
77
+ id: "run_aislop_fix",
78
+ label: `Run aislop fix to clear ${fixableDiags.length} mechanical finding${fixableDiags.length === 1 ? "" : "s"}.`,
79
+ command: "npx aislop fix",
80
+ rationale: "These findings have deterministic fixes (formatting, unused imports, trivial comments). Running this before any manual work avoids burning agent tokens on what the CLI handles for free.",
81
+ ruleIds
82
+ });
83
+ }
84
+ const archErrors = findings.filter((f) => f.ruleId.startsWith("arch/") && f.severity === "error");
85
+ if (archErrors.length > 0) actions.push({
86
+ id: "review_finding",
87
+ label: `Review ${archErrors.length} architecture rule violation${archErrors.length === 1 ? "" : "s"} — these can't be auto-fixed.`,
88
+ rationale: "Architecture rules encode intentional project structure decisions. The fix usually means moving code, not editing it.",
89
+ ruleIds: Array.from(new Set(archErrors.map((f) => f.ruleId)))
90
+ });
91
+ if (regressed && typeof delta === "number" && delta <= -REGRESSION_FLAG_THRESHOLD && fixableDiags.length === 0) {
92
+ const top = findings.filter((f) => f.severity === "error" || f.severity === "warning").slice(0, REVIEW_TOP_N);
93
+ if (top.length > 0) actions.push({
94
+ id: "review_finding",
95
+ label: `Score dropped ${Math.abs(delta)} points — review the top ${top.length} finding${top.length === 1 ? "" : "s"} from this edit.`,
96
+ rationale: "None of these are auto-fixable. Read each one against the source and decide whether the fix is to change the code or to add a justified suppression with a reason.",
97
+ ruleIds: top.map((f) => f.ruleId)
98
+ });
99
+ }
100
+ if (actions.length === 0) actions.push({
101
+ id: "no_action",
102
+ label: typeof delta === "number" ? delta > 0 ? `Score improved by ${delta}. No action needed.` : "Score unchanged. No action needed." : "No findings. No action needed.",
103
+ rationale: "The current scan didn't reveal anything that requires the agent's attention."
104
+ });
105
+ return actions;
106
+ };
67
107
  const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
68
108
  const all = diagnostics.map((d) => toFinding(d, rootDirectory)).filter((x) => x !== null);
69
109
  const capped = all.slice(0, MAX_FINDINGS);
@@ -74,15 +114,30 @@ const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
74
114
  fixable: diagnostics.filter((d) => d.fixable).length,
75
115
  total: all.length
76
116
  };
117
+ const baselineSnapshot = typeof baseline === "number" ? {
118
+ score: baseline,
119
+ findingFingerprints: []
120
+ } : baseline;
121
+ const baselineScore = baselineSnapshot?.score;
122
+ const delta = typeof baselineScore === "number" ? score - baselineScore : void 0;
123
+ const regressed = typeof delta === "number" ? delta < 0 : false;
124
+ let newSinceBaseline;
125
+ if (baselineSnapshot && baselineSnapshot.findingFingerprints.length > 0) {
126
+ const known = new Set(baselineSnapshot.findingFingerprints);
127
+ newSinceBaseline = all.filter((f) => !known.has(fingerprintFinding(f))).slice(0, MAX_NEW_SINCE_BASELINE);
128
+ }
77
129
  return {
78
- schema: "aislop.hook.v1",
130
+ schema: "aislop.hook.v2",
79
131
  score,
80
- baseline,
81
- regressed: typeof baseline === "number" ? score < baseline : false,
132
+ baseline: baselineScore,
133
+ delta,
134
+ regressed,
82
135
  counts,
83
136
  findings: capped,
84
137
  elided,
85
- nextSteps: buildNextSteps(capped)
138
+ newSinceBaseline,
139
+ nextSteps: buildNextSteps(capped),
140
+ suggestedActions: buildSuggestedActions(diagnostics, capped, regressed, delta)
86
141
  };
87
142
  };
88
143
 
@@ -147,6 +202,7 @@ const DEFAULT_CONFIG = {
147
202
  maxNesting: 5,
148
203
  maxParams: 6
149
204
  },
205
+ lint: { typecheck: false },
150
206
  security: {
151
207
  audit: true,
152
208
  auditTimeout: 25e3
@@ -275,6 +331,7 @@ const QualitySchema = z.object({
275
331
  maxNesting: z.number().positive().default(5),
276
332
  maxParams: z.number().positive().default(6)
277
333
  });
334
+ const LintConfigSchema = z.object({ typecheck: z.boolean().default(false) });
278
335
  const SecurityConfigSchema = z.object({
279
336
  audit: z.boolean().default(true),
280
337
  auditTimeout: z.number().positive().default(25e3)
@@ -312,6 +369,7 @@ const AislopConfigSchema = z.object({
312
369
  maxNesting: 5,
313
370
  maxParams: 6
314
371
  })),
372
+ lint: LintConfigSchema.default(() => ({ typecheck: false })),
315
373
  security: SecurityConfigSchema.default(() => ({
316
374
  audit: true,
317
375
  auditTimeout: 25e3
@@ -464,7 +522,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
464
522
  return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
465
523
  };
466
524
  const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
467
- const isTestFile = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
525
+ const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
468
526
  const getIgnoredPaths = (rootDirectory, files) => {
469
527
  if (files.length === 0) return /* @__PURE__ */ new Set();
470
528
  const result = spawnSync("git", [
@@ -538,7 +596,7 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
538
596
  return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
539
597
  };
540
598
  return normalizedFiles.filter(({ absolutePath, relativePath }) => {
541
- return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
599
+ return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile$2(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
542
600
  }).map(({ absolutePath }) => absolutePath);
543
601
  };
544
602
  const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
@@ -674,13 +732,14 @@ const detectOverAbstraction = async (context) => {
674
732
 
675
733
  //#endregion
676
734
  //#region src/engines/ai-slop/comments.ts
735
+ const NON_PRODUCTION_DIR_PATTERN$2 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
677
736
  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";
678
737
  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")];
679
738
  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")];
680
739
  const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
681
740
  const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
682
741
  const MAX_TRIVIAL_COMMENT_LENGTH = 60;
683
- const isJsComment = (trimmed) => trimmed.startsWith("//");
742
+ const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
684
743
  const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
685
744
  /**
686
745
  * Extract just the comment text after the comment marker.
@@ -729,13 +788,14 @@ const detectTrivialComments = async (context) => {
729
788
  const diagnostics = [];
730
789
  for (const filePath of files) {
731
790
  if (isAutoGenerated(filePath)) continue;
791
+ const relativePath = path.relative(context.rootDirectory, filePath);
792
+ if (NON_PRODUCTION_DIR_PATTERN$2.test(relativePath)) continue;
732
793
  let content;
733
794
  try {
734
795
  content = fs.readFileSync(filePath, "utf-8");
735
796
  } catch {
736
797
  continue;
737
798
  }
738
- const relativePath = path.relative(context.rootDirectory, filePath);
739
799
  diagnostics.push(...scanFileForTrivialComments(content, relativePath));
740
800
  }
741
801
  return diagnostics;
@@ -743,7 +803,7 @@ const detectTrivialComments = async (context) => {
743
803
 
744
804
  //#endregion
745
805
  //#region src/engines/ai-slop/dead-patterns.ts
746
- const JS_EXTENSIONS$1 = new Set([
806
+ const JS_EXTENSIONS$4 = new Set([
747
807
  ".ts",
748
808
  ".tsx",
749
809
  ".js",
@@ -765,11 +825,11 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
765
825
  fixable
766
826
  });
767
827
  const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
768
- const SCRIPT_DIR_PATTERN = /(?:^|\/)(scripts|bin)\//;
828
+ const NON_PRODUCTION_DIR_PATTERN$1 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|cli|cli-[\w-]+|[\w-]+-cli)\//;
769
829
  const detectConsoleLeftovers = (content, relativePath, ext) => {
770
- if (!JS_EXTENSIONS$1.has(ext)) return [];
830
+ if (!JS_EXTENSIONS$4.has(ext)) return [];
771
831
  if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
772
- if (SCRIPT_DIR_PATTERN.test(relativePath)) return [];
832
+ if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
773
833
  const diagnostics = [];
774
834
  const lines = content.split("\n");
775
835
  for (let i = 0; i < lines.length; i++) {
@@ -809,9 +869,9 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
809
869
  for (let i = 0; i < lines.length; i++) {
810
870
  const trimmed = lines[i].trim();
811
871
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
812
- if (JS_EXTENSIONS$1.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !nextLine.startsWith("}") && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push(slop(relativePath, i + 2, "ai-slop/unreachable-code", "warning", "Code after return/throw statement is unreachable", "Remove the unreachable code or restructure the control flow", false));
872
+ if (JS_EXTENSIONS$4.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !nextLine.startsWith("}") && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push(slop(relativePath, i + 2, "ai-slop/unreachable-code", "warning", "Code after return/throw statement is unreachable", "Remove the unreachable code or restructure the control flow", false));
813
873
  if (/\bif\s*\(\s*(?:false|true|0|1)\s*\)/.test(trimmed) && !trimmed.startsWith("//") && !trimmed.startsWith("*") && !/["'`].*\bif\s*\(/.test(trimmed) && !/\/.*\bif\s*\(/.test(trimmed.replace(/\/\/.*$/, ""))) diagnostics.push(slop(relativePath, i + 1, "ai-slop/constant-condition", "warning", "Conditional with a constant value — likely debugging leftover", "Remove the constant condition or replace with proper logic", false));
814
- if (JS_EXTENSIONS$1.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
874
+ if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
815
875
  }
816
876
  return diagnostics;
817
877
  };
@@ -819,6 +879,7 @@ const asAnyPattern = new RegExp(`\\bas\\s+any\\b`);
819
879
  const doubleAssertPattern = new RegExp(`\\bas\\s+unknown\\s+as\\s+`);
820
880
  const detectUnsafeTypePatterns = (content, relativePath, ext) => {
821
881
  if (ext !== ".ts" && ext !== ".tsx") return [];
882
+ if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
822
883
  const diagnostics = [];
823
884
  const lines = content.split("\n");
824
885
  for (let i = 0; i < lines.length; i++) {
@@ -855,6 +916,74 @@ const detectDeadPatterns = async (context) => {
855
916
  return diagnostics;
856
917
  };
857
918
 
919
+ //#endregion
920
+ //#region src/engines/ai-slop/duplicate-imports.ts
921
+ const JS_EXTENSIONS$3 = new Set([
922
+ ".ts",
923
+ ".tsx",
924
+ ".js",
925
+ ".jsx",
926
+ ".mjs",
927
+ ".cjs"
928
+ ]);
929
+ const IMPORT_FROM_RE$1 = /^\s*import\s+[^;]*?from\s+["']([^"']+)["']/;
930
+ const extractImportLines = (content) => {
931
+ const lines = content.split("\n");
932
+ const results = [];
933
+ for (let i = 0; i < lines.length; i++) {
934
+ const line = lines[i];
935
+ const match = IMPORT_FROM_RE$1.exec(line);
936
+ if (!match) continue;
937
+ results.push({
938
+ spec: match[1],
939
+ line: i + 1
940
+ });
941
+ }
942
+ return results;
943
+ };
944
+ const detectDuplicateImports = async (context) => {
945
+ const diagnostics = [];
946
+ const files = getSourceFiles(context);
947
+ for (const filePath of files) {
948
+ if (!JS_EXTENSIONS$3.has(path.extname(filePath))) continue;
949
+ if (isAutoGenerated(filePath)) continue;
950
+ let content;
951
+ try {
952
+ content = fs.readFileSync(filePath, "utf-8");
953
+ } catch {
954
+ continue;
955
+ }
956
+ const imports = extractImportLines(content);
957
+ if (imports.length < 2) continue;
958
+ const bySpec = /* @__PURE__ */ new Map();
959
+ for (const imp of imports) {
960
+ const list = bySpec.get(imp.spec) ?? [];
961
+ list.push(imp);
962
+ bySpec.set(imp.spec, list);
963
+ }
964
+ const relPath = path.relative(context.rootDirectory, filePath);
965
+ for (const [spec, occurrences] of bySpec) {
966
+ if (occurrences.length < 2) continue;
967
+ for (const dup of occurrences.slice(1)) {
968
+ const firstLine = occurrences[0].line;
969
+ diagnostics.push({
970
+ filePath: relPath,
971
+ engine: "ai-slop",
972
+ rule: "ai-slop/duplicate-import",
973
+ severity: "warning",
974
+ message: `"${spec}" is also imported on line ${firstLine}. Merge into a single import statement.`,
975
+ help: "Two imports from the same module split readers' attention and grow the import block. Run aislop fix to merge them automatically.",
976
+ line: dup.line,
977
+ column: 1,
978
+ category: "AI Slop",
979
+ fixable: true
980
+ });
981
+ }
982
+ }
983
+ }
984
+ return diagnostics;
985
+ };
986
+
858
987
  //#endregion
859
988
  //#region src/engines/ai-slop/exceptions.ts
860
989
  const SWALLOWED_EXCEPTION_PATTERNS = [
@@ -945,6 +1074,600 @@ const detectSwallowedExceptions = async (context) => {
945
1074
  return diagnostics;
946
1075
  };
947
1076
 
1077
+ //#endregion
1078
+ //#region src/engines/ai-slop/go-patterns.ts
1079
+ const GO_EXTENSIONS = new Set([".go"]);
1080
+ const PACKAGE_DECL_RE = /^\s*package\s+(\w+)/;
1081
+ const PANIC_CALL_RE = /\bpanic\s*\(/;
1082
+ const COMMENT_LINE_RE$1 = /^\s*\/\//;
1083
+ const NIL_GUARD_RE = /^\s*if\s+[\w.]+(?:\(\))?\s*==\s*nil\s*\{?\s*$/;
1084
+ const SHORT_STRING_PANIC_RE = /\bpanic\s*\(\s*"[^"]{1,40}"\s*\)/;
1085
+ const detectPackageName = (lines) => {
1086
+ for (const line of lines) {
1087
+ const m = PACKAGE_DECL_RE.exec(line);
1088
+ if (m) return m[1];
1089
+ }
1090
+ return null;
1091
+ };
1092
+ const PANIC_INTENT_LOOKBACK = 3;
1093
+ const hasIntentComment$1 = (lines, panicLineIdx) => {
1094
+ for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - PANIC_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE$1.test(lines[j])) return true;
1095
+ return false;
1096
+ };
1097
+ const isNilGuardPanic = (lines, panicLineIdx, line) => {
1098
+ if (!SHORT_STRING_PANIC_RE.test(line)) return false;
1099
+ for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - 2); j--) {
1100
+ const prev = lines[j];
1101
+ if (prev.trim() === "") continue;
1102
+ return NIL_GUARD_RE.test(prev);
1103
+ }
1104
+ return false;
1105
+ };
1106
+ const flagLibraryPanic = (lines, relPath, pkg, out) => {
1107
+ if (pkg === "main") return;
1108
+ for (let i = 0; i < lines.length; i++) {
1109
+ const line = lines[i];
1110
+ if (COMMENT_LINE_RE$1.test(line)) continue;
1111
+ PANIC_CALL_RE.lastIndex = 0;
1112
+ if (!PANIC_CALL_RE.test(line)) continue;
1113
+ if (hasIntentComment$1(lines, i)) continue;
1114
+ if (isNilGuardPanic(lines, i, line)) continue;
1115
+ out.push({
1116
+ filePath: relPath,
1117
+ engine: "ai-slop",
1118
+ rule: "ai-slop/go-library-panic",
1119
+ severity: "warning",
1120
+ message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
1121
+ help: "Convert to `return fmt.Errorf(...)` (or a wrapped error) and let the caller decide. Reserve `panic` for genuinely-impossible states (corrupt internal invariants), and mark those with a comment so future readers know it's intentional.",
1122
+ line: i + 1,
1123
+ column: 1,
1124
+ category: "AI Slop",
1125
+ fixable: false
1126
+ });
1127
+ }
1128
+ };
1129
+ const detectGoPatterns = async (context) => {
1130
+ const diagnostics = [];
1131
+ const files = getSourceFiles(context);
1132
+ for (const filePath of files) {
1133
+ if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
1134
+ if (isAutoGenerated(filePath)) continue;
1135
+ if (filePath.endsWith("_test.go")) continue;
1136
+ let content;
1137
+ try {
1138
+ content = fs.readFileSync(filePath, "utf-8");
1139
+ } catch {
1140
+ continue;
1141
+ }
1142
+ const lines = content.split("\n");
1143
+ const pkg = detectPackageName(lines);
1144
+ if (!pkg) continue;
1145
+ flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
1146
+ }
1147
+ return diagnostics;
1148
+ };
1149
+
1150
+ //#endregion
1151
+ //#region src/engines/ai-slop/python-data.ts
1152
+ const PYTHON_STDLIB = new Set([
1153
+ "__future__",
1154
+ "_thread",
1155
+ "abc",
1156
+ "argparse",
1157
+ "array",
1158
+ "ast",
1159
+ "asyncio",
1160
+ "atexit",
1161
+ "base64",
1162
+ "binascii",
1163
+ "bisect",
1164
+ "builtins",
1165
+ "bz2",
1166
+ "calendar",
1167
+ "codecs",
1168
+ "collections",
1169
+ "concurrent",
1170
+ "configparser",
1171
+ "contextlib",
1172
+ "contextvars",
1173
+ "copy",
1174
+ "csv",
1175
+ "ctypes",
1176
+ "dataclasses",
1177
+ "datetime",
1178
+ "decimal",
1179
+ "difflib",
1180
+ "dis",
1181
+ "doctest",
1182
+ "email",
1183
+ "encodings",
1184
+ "enum",
1185
+ "errno",
1186
+ "faulthandler",
1187
+ "filecmp",
1188
+ "fileinput",
1189
+ "fnmatch",
1190
+ "fractions",
1191
+ "functools",
1192
+ "gc",
1193
+ "getopt",
1194
+ "getpass",
1195
+ "gettext",
1196
+ "glob",
1197
+ "graphlib",
1198
+ "gzip",
1199
+ "hashlib",
1200
+ "heapq",
1201
+ "hmac",
1202
+ "html",
1203
+ "http",
1204
+ "imaplib",
1205
+ "importlib",
1206
+ "inspect",
1207
+ "io",
1208
+ "ipaddress",
1209
+ "itertools",
1210
+ "json",
1211
+ "keyword",
1212
+ "linecache",
1213
+ "locale",
1214
+ "logging",
1215
+ "lzma",
1216
+ "mailbox",
1217
+ "math",
1218
+ "mimetypes",
1219
+ "mmap",
1220
+ "multiprocessing",
1221
+ "numbers",
1222
+ "operator",
1223
+ "os",
1224
+ "pathlib",
1225
+ "pdb",
1226
+ "pickle",
1227
+ "platform",
1228
+ "plistlib",
1229
+ "pprint",
1230
+ "profile",
1231
+ "pstats",
1232
+ "pty",
1233
+ "queue",
1234
+ "quopri",
1235
+ "random",
1236
+ "re",
1237
+ "readline",
1238
+ "reprlib",
1239
+ "resource",
1240
+ "secrets",
1241
+ "select",
1242
+ "selectors",
1243
+ "shelve",
1244
+ "shlex",
1245
+ "shutil",
1246
+ "signal",
1247
+ "site",
1248
+ "smtplib",
1249
+ "socket",
1250
+ "socketserver",
1251
+ "sqlite3",
1252
+ "ssl",
1253
+ "stat",
1254
+ "statistics",
1255
+ "string",
1256
+ "stringprep",
1257
+ "struct",
1258
+ "subprocess",
1259
+ "sunau",
1260
+ "symtable",
1261
+ "sys",
1262
+ "sysconfig",
1263
+ "syslog",
1264
+ "tarfile",
1265
+ "telnetlib",
1266
+ "tempfile",
1267
+ "termios",
1268
+ "test",
1269
+ "textwrap",
1270
+ "threading",
1271
+ "time",
1272
+ "timeit",
1273
+ "tkinter",
1274
+ "token",
1275
+ "tokenize",
1276
+ "tomllib",
1277
+ "trace",
1278
+ "traceback",
1279
+ "tracemalloc",
1280
+ "tty",
1281
+ "turtle",
1282
+ "types",
1283
+ "typing",
1284
+ "unicodedata",
1285
+ "unittest",
1286
+ "urllib",
1287
+ "uu",
1288
+ "uuid",
1289
+ "venv",
1290
+ "warnings",
1291
+ "wave",
1292
+ "weakref",
1293
+ "webbrowser",
1294
+ "winreg",
1295
+ "winsound",
1296
+ "wsgiref",
1297
+ "xml",
1298
+ "xmlrpc",
1299
+ "zipapp",
1300
+ "zipfile",
1301
+ "zipimport",
1302
+ "zlib",
1303
+ "zoneinfo"
1304
+ ]);
1305
+ const PYTHON_IMPORT_TO_PIP = {
1306
+ yaml: "pyyaml",
1307
+ PIL: "pillow",
1308
+ dateutil: "python-dateutil",
1309
+ cv2: "opencv-python",
1310
+ sklearn: "scikit-learn",
1311
+ bs4: "beautifulsoup4",
1312
+ typing_extensions: "typing-extensions",
1313
+ google: "google-api-python-client",
1314
+ jose: "python-jose",
1315
+ jwt: "pyjwt",
1316
+ OpenSSL: "pyopenssl",
1317
+ magic: "python-magic",
1318
+ docx: "python-docx",
1319
+ pptx: "python-pptx",
1320
+ git: "gitpython",
1321
+ socks: "pysocks",
1322
+ redis: "redis"
1323
+ };
1324
+
1325
+ //#endregion
1326
+ //#region src/engines/ai-slop/hallucinated-imports.ts
1327
+ const JS_EXTENSIONS$2 = new Set([
1328
+ ".ts",
1329
+ ".tsx",
1330
+ ".js",
1331
+ ".jsx",
1332
+ ".mjs",
1333
+ ".cjs"
1334
+ ]);
1335
+ const PY_EXTENSIONS$2 = new Set([".py"]);
1336
+ const readJson = (filePath) => {
1337
+ try {
1338
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1339
+ } catch {
1340
+ return null;
1341
+ }
1342
+ };
1343
+ const PKG_DEP_SECTIONS = [
1344
+ "dependencies",
1345
+ "devDependencies",
1346
+ "peerDependencies",
1347
+ "optionalDependencies"
1348
+ ];
1349
+ const addDepsFromPkg = (pkg, jsDeps) => {
1350
+ for (const section of PKG_DEP_SECTIONS) {
1351
+ const deps = pkg[section];
1352
+ if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
1353
+ }
1354
+ };
1355
+ const readWorkspaceGlobs = (rootDir, rootPkg) => {
1356
+ const globs = [];
1357
+ if (rootPkg && typeof rootPkg === "object") {
1358
+ const ws = rootPkg.workspaces;
1359
+ if (Array.isArray(ws)) {
1360
+ for (const g of ws) if (typeof g === "string") globs.push(g);
1361
+ } else if (ws && typeof ws === "object") {
1362
+ const pkgs = ws.packages;
1363
+ if (Array.isArray(pkgs)) {
1364
+ for (const g of pkgs) if (typeof g === "string") globs.push(g);
1365
+ }
1366
+ }
1367
+ }
1368
+ const lerna = readJson(path.join(rootDir, "lerna.json"));
1369
+ if (lerna && Array.isArray(lerna.packages)) {
1370
+ for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
1371
+ }
1372
+ try {
1373
+ const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
1374
+ let inPackages = false;
1375
+ for (const rawLine of pnpmWs.split("\n")) {
1376
+ if (/^packages\s*:\s*$/.test(rawLine)) {
1377
+ inPackages = true;
1378
+ continue;
1379
+ }
1380
+ if (!inPackages) continue;
1381
+ if (/^\S/.test(rawLine)) break;
1382
+ const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
1383
+ if (m) globs.push(m[1].trim());
1384
+ }
1385
+ } catch {}
1386
+ return globs;
1387
+ };
1388
+ const expandWorkspaceDirs = (rootDir, globs) => {
1389
+ const dirs = [];
1390
+ for (const glob of globs) if (glob.endsWith("/*")) {
1391
+ const parent = path.join(rootDir, glob.slice(0, -2));
1392
+ try {
1393
+ for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1394
+ } catch {
1395
+ continue;
1396
+ }
1397
+ } else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
1398
+ return dirs;
1399
+ };
1400
+ const SKIP_DIRS = new Set([
1401
+ "node_modules",
1402
+ ".git",
1403
+ "dist",
1404
+ "build",
1405
+ "out",
1406
+ "target",
1407
+ "coverage"
1408
+ ]);
1409
+ const NESTED_PKG_JSON_DEPTH = 4;
1410
+ const collectNestedManifests = (rootDir, jsDeps) => {
1411
+ const walk = (dir, depth) => {
1412
+ if (depth > NESTED_PKG_JSON_DEPTH) return;
1413
+ let entries;
1414
+ try {
1415
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1416
+ } catch {
1417
+ return;
1418
+ }
1419
+ for (const entry of entries) {
1420
+ if (entry.name.startsWith(".") && entry.name !== ".github") continue;
1421
+ if (SKIP_DIRS.has(entry.name)) continue;
1422
+ const full = path.join(dir, entry.name);
1423
+ if (entry.isDirectory()) walk(full, depth + 1);
1424
+ else if (entry.name === "package.json" && depth > 0) {
1425
+ const wsPkg = readJson(full);
1426
+ if (!wsPkg) continue;
1427
+ if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
1428
+ addDepsFromPkg(wsPkg, jsDeps);
1429
+ }
1430
+ }
1431
+ };
1432
+ walk(rootDir, 0);
1433
+ };
1434
+ const collectJsDeps = (rootDir, jsDeps) => {
1435
+ const pkgPath = path.join(rootDir, "package.json");
1436
+ if (!fs.existsSync(pkgPath)) return false;
1437
+ const pkg = readJson(pkgPath);
1438
+ if (!pkg || typeof pkg !== "object") return false;
1439
+ addDepsFromPkg(pkg, jsDeps);
1440
+ if (typeof pkg.name === "string") jsDeps.add(pkg.name);
1441
+ const workspaceDirs = expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, pkg));
1442
+ for (const wsDir of workspaceDirs) {
1443
+ const wsPkg = readJson(path.join(wsDir, "package.json"));
1444
+ if (!wsPkg) continue;
1445
+ if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
1446
+ addDepsFromPkg(wsPkg, jsDeps);
1447
+ }
1448
+ collectNestedManifests(rootDir, jsDeps);
1449
+ return true;
1450
+ };
1451
+ const addPyDep = (pyDeps, name) => {
1452
+ const normalized = name.toLowerCase().replace(/_/g, "-");
1453
+ pyDeps.add(normalized);
1454
+ };
1455
+ const collectFromRequirementsTxt = (rootDir, pyDeps) => {
1456
+ const reqPath = path.join(rootDir, "requirements.txt");
1457
+ if (!fs.existsSync(reqPath)) return false;
1458
+ try {
1459
+ const content = fs.readFileSync(reqPath, "utf-8");
1460
+ for (const line of content.split("\n")) {
1461
+ const trimmed = line.trim();
1462
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
1463
+ const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
1464
+ if (match) addPyDep(pyDeps, match[1]);
1465
+ }
1466
+ return true;
1467
+ } catch {
1468
+ return false;
1469
+ }
1470
+ };
1471
+ const collectFromPyproject = (rootDir, pyDeps) => {
1472
+ const pyprojPath = path.join(rootDir, "pyproject.toml");
1473
+ if (!fs.existsSync(pyprojPath)) return false;
1474
+ try {
1475
+ const content = fs.readFileSync(pyprojPath, "utf-8");
1476
+ const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1477
+ if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
1478
+ const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
1479
+ if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
1480
+ const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
1481
+ if (pep621) for (const line of pep621[1].split("\n")) {
1482
+ const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
1483
+ if (m) addPyDep(pyDeps, m[1]);
1484
+ }
1485
+ const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
1486
+ let match = poetryRe.exec(content);
1487
+ while (match !== null) {
1488
+ for (const line of match[1].split("\n")) {
1489
+ const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1490
+ if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
1491
+ }
1492
+ match = poetryRe.exec(content);
1493
+ }
1494
+ return true;
1495
+ } catch {
1496
+ return false;
1497
+ }
1498
+ };
1499
+ const collectFromPipfile = (rootDir, pyDeps) => {
1500
+ const pipfilePath = path.join(rootDir, "Pipfile");
1501
+ if (!fs.existsSync(pipfilePath)) return false;
1502
+ try {
1503
+ const content = fs.readFileSync(pipfilePath, "utf-8");
1504
+ const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
1505
+ let match = sectionRe.exec(content);
1506
+ while (match !== null) {
1507
+ for (const line of match[2].split("\n")) {
1508
+ const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
1509
+ if (m) addPyDep(pyDeps, m[1]);
1510
+ }
1511
+ match = sectionRe.exec(content);
1512
+ }
1513
+ return true;
1514
+ } catch {
1515
+ return false;
1516
+ }
1517
+ };
1518
+ const loadManifest = (rootDir) => {
1519
+ const jsDeps = /* @__PURE__ */ new Set();
1520
+ const pyDeps = /* @__PURE__ */ new Set();
1521
+ const hasJsManifest = collectJsDeps(rootDir, jsDeps);
1522
+ const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
1523
+ const hasPyproject = collectFromPyproject(rootDir, pyDeps);
1524
+ const hasPipfile = collectFromPipfile(rootDir, pyDeps);
1525
+ return {
1526
+ jsDeps,
1527
+ pyDeps,
1528
+ hasJsManifest,
1529
+ hasPyManifest: hasReq || hasPyproject || hasPipfile
1530
+ };
1531
+ };
1532
+ const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
1533
+ const isJsBuiltin = (spec) => {
1534
+ return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
1535
+ };
1536
+ const VIRTUAL_MODULE_PREFIXES = [
1537
+ "astro:",
1538
+ "virtual:",
1539
+ "bun:"
1540
+ ];
1541
+ const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
1542
+ const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
1543
+ const isLikelyRealImportSpec = (spec) => {
1544
+ if (spec.length === 0) return false;
1545
+ if (TEMPLATE_PLACEHOLDER_RE.test(spec)) return false;
1546
+ if (spec.includes("\\")) return false;
1547
+ if (/\s/.test(spec)) return false;
1548
+ return true;
1549
+ };
1550
+ const packageNameFromImport = (spec) => {
1551
+ if (spec.startsWith("@")) {
1552
+ const parts = spec.split("/");
1553
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec;
1554
+ }
1555
+ return spec.split("/")[0];
1556
+ };
1557
+ const STATIC_IMPORT_RE = /^\s*import\s+(?:[\w*{},\s]+\s+from\s+)?["']([^"']+)["']/;
1558
+ const DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*["']([^"']+)["']/g;
1559
+ const extractJsImports = (content) => {
1560
+ const lines = content.split("\n");
1561
+ const results = [];
1562
+ for (let i = 0; i < lines.length; i++) {
1563
+ const line = lines[i];
1564
+ const trimmed = line.trim();
1565
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1566
+ const staticMatch = STATIC_IMPORT_RE.exec(line);
1567
+ if (staticMatch && isLikelyRealImportSpec(staticMatch[1])) results.push({
1568
+ spec: staticMatch[1],
1569
+ line: i + 1
1570
+ });
1571
+ DYNAMIC_IMPORT_RE.lastIndex = 0;
1572
+ let dyn = DYNAMIC_IMPORT_RE.exec(line);
1573
+ while (dyn !== null) {
1574
+ if (isLikelyRealImportSpec(dyn[1])) results.push({
1575
+ spec: dyn[1],
1576
+ line: i + 1
1577
+ });
1578
+ dyn = DYNAMIC_IMPORT_RE.exec(line);
1579
+ }
1580
+ }
1581
+ return results;
1582
+ };
1583
+ const extractPyImports = (content) => {
1584
+ const lines = content.split("\n");
1585
+ const results = [];
1586
+ for (let i = 0; i < lines.length; i++) {
1587
+ const line = lines[i].trim();
1588
+ if (line.startsWith("#")) continue;
1589
+ const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
1590
+ if (fromMatch && !fromMatch[1].startsWith(".")) {
1591
+ results.push({
1592
+ spec: fromMatch[1],
1593
+ line: i + 1
1594
+ });
1595
+ continue;
1596
+ }
1597
+ const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
1598
+ if (importMatch) for (const raw of importMatch[1].split(",")) {
1599
+ const cleaned = raw.trim().split(/\s+as\s+/)[0];
1600
+ if (cleaned && !cleaned.startsWith(".")) results.push({
1601
+ spec: cleaned,
1602
+ line: i + 1
1603
+ });
1604
+ }
1605
+ }
1606
+ return results;
1607
+ };
1608
+ const checkJsImport = (spec, manifest) => {
1609
+ if (isJsRelativeOrAbsolute(spec)) return null;
1610
+ if (isJsBuiltin(spec)) return null;
1611
+ if (isJsVirtualModule(spec)) return null;
1612
+ const pkg = packageNameFromImport(spec);
1613
+ if (manifest.jsDeps.has(pkg)) return null;
1614
+ if (pkg.startsWith("@types/")) {
1615
+ const realPkg = pkg.slice(7);
1616
+ if (manifest.jsDeps.has(realPkg)) return null;
1617
+ }
1618
+ return pkg;
1619
+ };
1620
+ const checkPyImport = (spec, manifest) => {
1621
+ const root = spec.split(".")[0];
1622
+ if (PYTHON_STDLIB.has(root)) return null;
1623
+ const normalized = root.toLowerCase().replace(/_/g, "-");
1624
+ if (manifest.pyDeps.has(normalized)) return null;
1625
+ const pipName = PYTHON_IMPORT_TO_PIP[root];
1626
+ if (pipName && manifest.pyDeps.has(pipName)) return null;
1627
+ return root;
1628
+ };
1629
+ const detectHallucinatedImports = async (context) => {
1630
+ const manifest = loadManifest(context.rootDirectory);
1631
+ if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
1632
+ const diagnostics = [];
1633
+ const files = getSourceFiles(context);
1634
+ for (const filePath of files) {
1635
+ const ext = path.extname(filePath);
1636
+ const isJs = JS_EXTENSIONS$2.has(ext);
1637
+ const isPy = PY_EXTENSIONS$2.has(ext);
1638
+ if (!isJs && !isPy) continue;
1639
+ if (isJs && !manifest.hasJsManifest) continue;
1640
+ if (isPy && !manifest.hasPyManifest) continue;
1641
+ if (isAutoGenerated(filePath)) continue;
1642
+ let content;
1643
+ try {
1644
+ content = fs.readFileSync(filePath, "utf-8");
1645
+ } catch {
1646
+ continue;
1647
+ }
1648
+ const relPath = path.relative(context.rootDirectory, filePath);
1649
+ const imports = isJs ? extractJsImports(content) : extractPyImports(content);
1650
+ for (const { spec, line } of imports) {
1651
+ const hallucinated = isJs ? checkJsImport(spec, manifest) : checkPyImport(spec, manifest);
1652
+ if (!hallucinated) continue;
1653
+ const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
1654
+ diagnostics.push({
1655
+ filePath: relPath,
1656
+ engine: "ai-slop",
1657
+ rule: "ai-slop/hallucinated-import",
1658
+ severity: "error",
1659
+ message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
1660
+ help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
1661
+ line,
1662
+ column: 1,
1663
+ category: "AI Slop",
1664
+ fixable: false
1665
+ });
1666
+ }
1667
+ }
1668
+ return diagnostics;
1669
+ };
1670
+
948
1671
  //#endregion
949
1672
  //#region src/engines/ai-slop/narrative-comments-patterns.ts
950
1673
  const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
@@ -1061,6 +1784,7 @@ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|re
1061
1784
 
1062
1785
  //#endregion
1063
1786
  //#region src/engines/ai-slop/narrative-comments.ts
1787
+ const NON_PRODUCTION_DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
1064
1788
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
1065
1789
  const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
1066
1790
  const getCommentSyntax = (ext) => {
@@ -1089,6 +1813,10 @@ const getMatchedLinePrefix = (line, syntax) => {
1089
1813
  }
1090
1814
  return null;
1091
1815
  };
1816
+ const isRustDocCommentLine = (line) => {
1817
+ const trimmed = line.trimStart();
1818
+ return trimmed.startsWith("///") || trimmed.startsWith("//!");
1819
+ };
1092
1820
  const collectBlocks = (sourceLines, syntax) => {
1093
1821
  const blocks = [];
1094
1822
  let i = 0;
@@ -1104,6 +1832,8 @@ const collectBlocks = (sourceLines, syntax) => {
1104
1832
  }
1105
1833
  let next = i;
1106
1834
  while (next < sourceLines.length && sourceLines[next].trim() === "") next += 1;
1835
+ const docCandidates = raw.filter((l) => l.trim().length > 0);
1836
+ const isRustDoc = docCandidates.length > 0 && docCandidates.every((l) => isRustDocCommentLine(l));
1107
1837
  blocks.push({
1108
1838
  kind: "line",
1109
1839
  startLine: start + 1,
@@ -1111,6 +1841,7 @@ const collectBlocks = (sourceLines, syntax) => {
1111
1841
  rawLines: raw,
1112
1842
  prose: raw.map(stripLineComment),
1113
1843
  hasMeaningfulJsdocTag: false,
1844
+ isRustDoc,
1114
1845
  nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
1115
1846
  });
1116
1847
  continue;
@@ -1144,6 +1875,7 @@ const collectBlocks = (sourceLines, syntax) => {
1144
1875
  rawLines: raw,
1145
1876
  prose,
1146
1877
  hasMeaningfulJsdocTag: hasMeaningful,
1878
+ isRustDoc: false,
1147
1879
  nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
1148
1880
  });
1149
1881
  continue;
@@ -1194,6 +1926,22 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
1194
1926
  return false;
1195
1927
  };
1196
1928
  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));
1929
+ const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
1930
+ const looksLikeGoDocComment = (block, ext) => {
1931
+ if (ext !== ".go" || block.kind !== "line") return false;
1932
+ const next = block.nextNonBlankLine;
1933
+ if (!next) return false;
1934
+ const declMatch = GO_DECL_NAME_RE.exec(next.trim());
1935
+ if (!declMatch) return false;
1936
+ return ((block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "") === declMatch[1];
1937
+ };
1938
+ const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):/i;
1939
+ const hasDocIndicator = (block) => {
1940
+ const joined = block.prose.join(" ");
1941
+ if (DOC_INDICATOR_RE.test(joined)) return true;
1942
+ for (const l of block.prose) if (/^[-]\s/.test(l)) return true;
1943
+ return false;
1944
+ };
1197
1945
  const detectNarrativeInBlock = (block, ext) => {
1198
1946
  if (looksLikeLicenseHeader(block)) return {
1199
1947
  matched: false,
@@ -1207,6 +1955,14 @@ const detectNarrativeInBlock = (block, ext) => {
1207
1955
  matched: false,
1208
1956
  reason: ""
1209
1957
  };
1958
+ if (block.isRustDoc) return {
1959
+ matched: false,
1960
+ reason: ""
1961
+ };
1962
+ if (looksLikeGoDocComment(block, ext)) return {
1963
+ matched: false,
1964
+ reason: ""
1965
+ };
1210
1966
  if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
1211
1967
  matched: true,
1212
1968
  reason: "decorative separator"
@@ -1219,11 +1975,16 @@ const detectNarrativeInBlock = (block, ext) => {
1219
1975
  matched: true,
1220
1976
  reason: "bare section label"
1221
1977
  };
1978
+ const joined = block.prose.join(" ");
1979
+ const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
1980
+ if ((hasWhyMarker || hasDocIndicator(block)) && block.kind === "jsdoc") return {
1981
+ matched: false,
1982
+ reason: ""
1983
+ };
1222
1984
  if (block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
1223
1985
  matched: true,
1224
1986
  reason: block.kind === "jsdoc" ? "JSDoc preamble before declaration" : "multi-line preamble before declaration"
1225
1987
  };
1226
- const joined = block.prose.join(" ");
1227
1988
  if (CROSS_REFERENCE_PHRASES.some((re) => re.test(joined))) return {
1228
1989
  matched: true,
1229
1990
  reason: "cross-reference commentary"
@@ -1239,8 +2000,6 @@ const detectNarrativeInBlock = (block, ext) => {
1239
2000
  reason: "explanatory preamble"
1240
2001
  };
1241
2002
  const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
1242
- const joinedProse = block.prose.join(" ");
1243
- const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joinedProse);
1244
2003
  if (nonEmptyProseCount >= 5) return {
1245
2004
  matched: true,
1246
2005
  reason: "long narrative block"
@@ -1263,6 +2022,8 @@ const detectNarrativeComments = async (context) => {
1263
2022
  if (isAutoGenerated(filePath)) continue;
1264
2023
  const syntax = getCommentSyntax(ext);
1265
2024
  if (!syntax) continue;
2025
+ const relativePath = path.relative(context.rootDirectory, filePath);
2026
+ if (NON_PRODUCTION_DIR_PATTERN.test(relativePath)) continue;
1266
2027
  let content;
1267
2028
  try {
1268
2029
  content = fs.readFileSync(filePath, "utf-8");
@@ -1270,7 +2031,6 @@ const detectNarrativeComments = async (context) => {
1270
2031
  continue;
1271
2032
  }
1272
2033
  const blocks = collectBlocks(content.split("\n"), syntax);
1273
- const relativePath = filePath.replace(`${context.rootDirectory}/`, "");
1274
2034
  for (const block of blocks) {
1275
2035
  const { matched, reason } = detectNarrativeInBlock(block, ext);
1276
2036
  if (!matched) continue;
@@ -1290,48 +2050,293 @@ const detectNarrativeComments = async (context) => {
1290
2050
  }
1291
2051
  return diagnostics;
1292
2052
  };
1293
- const fixNarrativeComments = async (context) => {
1294
- const diagnostics = await detectNarrativeComments(context);
1295
- if (diagnostics.length === 0) return;
1296
- const byFile = /* @__PURE__ */ new Map();
1297
- for (const d of diagnostics) {
1298
- const abs = d.filePath.startsWith("/") ? d.filePath : `${context.rootDirectory}/${d.filePath}`;
1299
- const list = byFile.get(abs) ?? [];
1300
- list.push(d);
1301
- byFile.set(abs, list);
2053
+
2054
+ //#endregion
2055
+ //#region src/engines/ai-slop/python-patterns.ts
2056
+ const PY_EXTENSIONS$1 = new Set([".py"]);
2057
+ const BARE_EXCEPT_RE = /^\s*except\s*:\s*(?:#.*)?$/;
2058
+ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\s*:\s*(?:#.*)?$/;
2059
+ const PRINT_RE = /^\s*print\s*\(/;
2060
+ const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
2061
+ const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
2062
+ const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
2063
+ const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
2064
+ const SCRIPT_DIR_NAMES = new Set([
2065
+ "scripts",
2066
+ "bin",
2067
+ ".github",
2068
+ "action",
2069
+ "docs",
2070
+ "docs_src",
2071
+ "examples",
2072
+ "example"
2073
+ ]);
2074
+ const isInScriptDir = (relPath) => relPath.split(path.sep).some((seg) => SCRIPT_DIR_NAMES.has(seg));
2075
+ const isTutorialFile = (basename) => basename.startsWith("tutorial") && basename.endsWith(".py");
2076
+ const MAIN_GUARD_RE = /^\s*if\s+__name__\s*==\s*["']__main__["']\s*:/;
2077
+ const hasMainGuard = (lines) => lines.some((l) => MAIN_GUARD_RE.test(l));
2078
+ const buildDocstringRanges = (lines) => {
2079
+ const inside = /* @__PURE__ */ new Set();
2080
+ let openDelim = null;
2081
+ for (let i = 0; i < lines.length; i++) {
2082
+ const line = lines[i];
2083
+ if (openDelim) {
2084
+ inside.add(i);
2085
+ if (line.includes(openDelim)) openDelim = null;
2086
+ continue;
2087
+ }
2088
+ for (const delim of ["\"\"\"", "'''"]) {
2089
+ const first = line.indexOf(delim);
2090
+ if (first === -1) continue;
2091
+ if (line.indexOf(delim, first + 3) === -1) {
2092
+ openDelim = delim;
2093
+ inside.add(i);
2094
+ break;
2095
+ }
2096
+ }
1302
2097
  }
1303
- for (const [filePath, diags] of byFile) {
1304
- const syntax = getCommentSyntax(path.extname(filePath));
1305
- if (!syntax) continue;
2098
+ return inside;
2099
+ };
2100
+ const pushFinding = (out, a) => {
2101
+ out.push({
2102
+ filePath: a.relPath,
2103
+ engine: "ai-slop",
2104
+ rule: a.rule,
2105
+ severity: a.severity,
2106
+ message: a.message,
2107
+ help: a.help,
2108
+ line: a.line,
2109
+ column: 1,
2110
+ category: "AI Slop",
2111
+ fixable: false
2112
+ });
2113
+ };
2114
+ const flagBareExcept = (lines, relPath, out) => {
2115
+ for (let i = 0; i < lines.length; i++) {
2116
+ if (!BARE_EXCEPT_RE.test(lines[i])) continue;
2117
+ pushFinding(out, {
2118
+ relPath,
2119
+ rule: "ai-slop/python-bare-except",
2120
+ severity: "warning",
2121
+ message: "Bare `except:` swallows every exception including KeyboardInterrupt and SystemExit.",
2122
+ help: "Catch the specific exception type you actually expect (`except ValueError:`, `except (KeyError, IndexError):`). If you genuinely want everything, `except BaseException:` plus a re-raise or log makes the intent explicit.",
2123
+ line: i + 1
2124
+ });
2125
+ }
2126
+ };
2127
+ const flagBroadExceptWithSilentBody = (lines, relPath, out) => {
2128
+ for (let i = 0; i < lines.length; i++) {
2129
+ const match = BROAD_EXCEPT_RE.exec(lines[i]);
2130
+ if (!match) continue;
2131
+ const trimmedNext = (lines[i + 1] ?? "").trim();
2132
+ if (!(trimmedNext === "pass" || trimmedNext.startsWith("#") && (lines[i + 2] ?? "").trim() === "pass")) continue;
2133
+ pushFinding(out, {
2134
+ relPath,
2135
+ rule: "ai-slop/python-broad-except",
2136
+ severity: "warning",
2137
+ message: `\`except ${match[1]}: pass\` silently drops every exception. Failures vanish without a trace.`,
2138
+ help: "Either narrow the exception class (`except ValueError:`), log the error, or re-raise. If you genuinely intend to swallow, add a comment naming the specific failure mode you're handling — auditors will thank you.",
2139
+ line: i + 1
2140
+ });
2141
+ }
2142
+ };
2143
+ const flagMutableDefaults = (lines, relPath, out) => {
2144
+ let i = 0;
2145
+ while (i < lines.length) {
2146
+ if (!DEF_RE.test(lines[i])) {
2147
+ i++;
2148
+ continue;
2149
+ }
2150
+ const startLine = i;
2151
+ let signature = lines[i];
2152
+ let parenDepth = 0;
2153
+ for (const ch of signature) if (ch === "(") parenDepth++;
2154
+ else if (ch === ")") parenDepth--;
2155
+ while (parenDepth > 0 && i + 1 < lines.length) {
2156
+ i++;
2157
+ signature += `\n${lines[i]}`;
2158
+ for (const ch of lines[i]) if (ch === "(") parenDepth++;
2159
+ else if (ch === ")") parenDepth--;
2160
+ }
2161
+ MUTABLE_DEFAULT_RE.lastIndex = 0;
2162
+ const found = MUTABLE_DEFAULT_RE.exec(signature);
2163
+ if (found) pushFinding(out, {
2164
+ relPath,
2165
+ rule: "ai-slop/python-mutable-default",
2166
+ severity: "warning",
2167
+ message: `Mutable default argument \`${found[1]}=${found[2]}\`. The default is shared across all calls — bugs that look like state-leakage.`,
2168
+ help: "Use `None` as the default and create the mutable value inside the body: `def f(items=None): items = items if items is not None else []`. Standard Python idiom; anything else is the AI agent shortcutting.",
2169
+ line: startLine + 1
2170
+ });
2171
+ i++;
2172
+ }
2173
+ };
2174
+ const flagPrintInProduction = (lines, relPath, basename, out) => {
2175
+ if (isTestFile$1(relPath, basename) || isScriptOrEntrypoint(basename)) return;
2176
+ if (isInScriptDir(relPath)) return;
2177
+ if (isTutorialFile(basename)) return;
2178
+ if (hasMainGuard(lines)) return;
2179
+ const docstringLines = buildDocstringRanges(lines);
2180
+ for (let i = 0; i < lines.length; i++) {
2181
+ const line = lines[i];
2182
+ if (!PRINT_RE.test(line)) continue;
2183
+ if (line.trim().startsWith("#")) continue;
2184
+ if (docstringLines.has(i)) continue;
2185
+ pushFinding(out, {
2186
+ relPath,
2187
+ rule: "ai-slop/python-print-debug",
2188
+ severity: "warning",
2189
+ message: "`print()` in production code — usually a leftover debug statement.",
2190
+ help: "Use the project's logger (`logging.getLogger(__name__).info(...)`). If this file is genuinely a CLI entry point (typer/click/argparse), it's safe to ignore — but rename to `__main__.py` or move under `scripts/` so the rule skips it next time.",
2191
+ line: i + 1
2192
+ });
2193
+ }
2194
+ };
2195
+ const detectPythonPatterns = async (context) => {
2196
+ const diagnostics = [];
2197
+ const files = getSourceFiles(context);
2198
+ for (const filePath of files) {
2199
+ if (!PY_EXTENSIONS$1.has(path.extname(filePath))) continue;
2200
+ if (isAutoGenerated(filePath)) continue;
1306
2201
  let content;
1307
2202
  try {
1308
2203
  content = fs.readFileSync(filePath, "utf-8");
1309
2204
  } catch {
1310
2205
  continue;
1311
2206
  }
2207
+ const relPath = path.relative(context.rootDirectory, filePath);
2208
+ const basename = path.basename(filePath);
1312
2209
  const lines = content.split("\n");
1313
- const blocks = collectBlocks(lines, syntax);
1314
- const toRemove = /* @__PURE__ */ new Set();
1315
- for (const d of diags) {
1316
- const block = blocks.find((b) => b.startLine === d.line);
1317
- if (!block) continue;
1318
- for (let ln = block.startLine; ln <= block.endLine; ln += 1) toRemove.add(ln);
1319
- const prev = block.startLine - 1;
1320
- const next = block.endLine + 1;
1321
- const prevIsBlank = prev >= 1 && lines[prev - 1]?.trim() === "";
1322
- const nextIsBlank = next <= lines.length && lines[next - 1]?.trim() === "";
1323
- if (prevIsBlank && nextIsBlank) toRemove.add(prev);
2210
+ flagBareExcept(lines, relPath, diagnostics);
2211
+ flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
2212
+ flagMutableDefaults(lines, relPath, diagnostics);
2213
+ flagPrintInProduction(lines, relPath, basename, diagnostics);
2214
+ }
2215
+ return diagnostics;
2216
+ };
2217
+
2218
+ //#endregion
2219
+ //#region src/engines/ai-slop/rust-patterns.ts
2220
+ const RUST_EXTENSIONS = new Set([".rs"]);
2221
+ const UNWRAP_CALL_RE = /\.unwrap\s*\(\s*\)/;
2222
+ const TODO_MACRO_RE = /\b(todo|unimplemented)\s*!\s*\(/;
2223
+ const COMMENT_LINE_RE = /^\s*\/\//;
2224
+ const TEST_ATTR_RE = /^\s*#\s*\[\s*(?:cfg\s*\(\s*test\s*\)|test|tokio::test)/;
2225
+ const WRITELN_UNWRAP_RE = /\b(?:writeln|write)\s*!\s*\([^)]*\)\s*\.unwrap\s*\(\s*\)/;
2226
+ const TEST_BASENAMES = new Set([
2227
+ "tests.rs",
2228
+ "testutil.rs",
2229
+ "test_util.rs",
2230
+ "test_utils.rs",
2231
+ "build.rs"
2232
+ ]);
2233
+ const TEST_CRATE_SEGMENT_RE = /(?:^|[-_])tests?(?:$|[-_])/;
2234
+ const isTestFile = (relPath) => {
2235
+ const segments = relPath.split(path.sep);
2236
+ if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
2237
+ const basename = segments[segments.length - 1] ?? "";
2238
+ if (TEST_BASENAMES.has(basename)) return true;
2239
+ return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
2240
+ };
2241
+ const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
2242
+ const UNWRAP_INTENT_LOOKBACK = 2;
2243
+ const hasIntentComment = (lines, lineIdx) => {
2244
+ for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - UNWRAP_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE.test(lines[j])) return true;
2245
+ return false;
2246
+ };
2247
+ const buildTestRanges = (lines) => {
2248
+ const ranges = [];
2249
+ for (let i = 0; i < lines.length; i++) {
2250
+ if (!TEST_ATTR_RE.test(lines[i])) continue;
2251
+ const openLine = i;
2252
+ let depth = 0;
2253
+ let started = false;
2254
+ for (let j = i; j < lines.length; j++) {
2255
+ const line = lines[j];
2256
+ for (const ch of line) if (ch === "{") {
2257
+ depth++;
2258
+ started = true;
2259
+ } else if (ch === "}") depth--;
2260
+ if (started && depth === 0) {
2261
+ ranges.push([openLine, j]);
2262
+ i = j;
2263
+ break;
2264
+ }
1324
2265
  }
1325
- const kept = [];
1326
- for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
1327
- const newContent = kept.join("\n");
1328
- if (newContent !== content) fs.writeFileSync(filePath, newContent);
1329
2266
  }
2267
+ return ranges;
2268
+ };
2269
+ const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
2270
+ const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
2271
+ for (let i = 0; i < lines.length; i++) {
2272
+ const line = lines[i];
2273
+ if (COMMENT_LINE_RE.test(line)) continue;
2274
+ if (isInRange(testRanges, i)) continue;
2275
+ if (!UNWRAP_CALL_RE.test(line)) continue;
2276
+ if (WRITELN_UNWRAP_RE.test(line)) continue;
2277
+ if (hasIntentComment(lines, i)) continue;
2278
+ out.push({
2279
+ filePath: relPath,
2280
+ engine: "ai-slop",
2281
+ rule: "ai-slop/rust-non-test-unwrap",
2282
+ severity: "warning",
2283
+ message: "`.unwrap()` in non-test code panics on None/Err. Surfaces as a hard crash for the caller.",
2284
+ help: "Use `?` to propagate, `.expect(\"context\")` if you really mean it (and the message names the invariant), or pattern-match the variant you care about. Reserve raw `.unwrap()` for tests and prototypes.",
2285
+ line: i + 1,
2286
+ column: 1,
2287
+ category: "AI Slop",
2288
+ fixable: false
2289
+ });
2290
+ }
2291
+ };
2292
+ const flagTodoMacro = (lines, relPath, out) => {
2293
+ for (let i = 0; i < lines.length; i++) {
2294
+ const line = lines[i];
2295
+ if (COMMENT_LINE_RE.test(line)) continue;
2296
+ const match = TODO_MACRO_RE.exec(line);
2297
+ if (!match) continue;
2298
+ out.push({
2299
+ filePath: relPath,
2300
+ engine: "ai-slop",
2301
+ rule: "ai-slop/rust-todo-stub",
2302
+ severity: "warning",
2303
+ message: `\`${match[1]}!()\` panics at runtime — almost certainly a stub the agent forgot to fill in.`,
2304
+ help: "Implement the missing path or remove it. If the work is genuinely deferred, file a ticket and put the number in a comment next to the macro so it doesn't ship invisibly.",
2305
+ line: i + 1,
2306
+ column: 1,
2307
+ category: "AI Slop",
2308
+ fixable: false
2309
+ });
2310
+ }
2311
+ };
2312
+ const detectRustPatterns = async (context) => {
2313
+ const diagnostics = [];
2314
+ const files = getSourceFiles(context);
2315
+ for (const filePath of files) {
2316
+ if (!RUST_EXTENSIONS.has(path.extname(filePath))) continue;
2317
+ if (isAutoGenerated(filePath)) continue;
2318
+ let content;
2319
+ try {
2320
+ content = fs.readFileSync(filePath, "utf-8");
2321
+ } catch {
2322
+ continue;
2323
+ }
2324
+ const relPath = path.relative(context.rootDirectory, filePath);
2325
+ const lines = content.split("\n");
2326
+ if (isExampleFile(relPath)) continue;
2327
+ if (isTestFile(relPath)) {
2328
+ flagTodoMacro(lines, relPath, diagnostics);
2329
+ continue;
2330
+ }
2331
+ flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
2332
+ flagTodoMacro(lines, relPath, diagnostics);
2333
+ }
2334
+ return diagnostics;
1330
2335
  };
1331
2336
 
1332
2337
  //#endregion
1333
2338
  //#region src/engines/ai-slop/unused-imports.ts
1334
- const JS_EXTENSIONS = new Set([
2339
+ const JS_EXTENSIONS$1 = new Set([
1335
2340
  ".ts",
1336
2341
  ".tsx",
1337
2342
  ".js",
@@ -1461,7 +2466,7 @@ const analyzeFile = (filePath) => {
1461
2466
  const lines = content.split("\n");
1462
2467
  let symbols;
1463
2468
  let importLines;
1464
- if (JS_EXTENSIONS.has(ext)) {
2469
+ if (JS_EXTENSIONS$1.has(ext)) {
1465
2470
  const result = extractJsImportedSymbols(lines);
1466
2471
  symbols = result.symbols;
1467
2472
  importLines = result.importLines;
@@ -1517,7 +2522,12 @@ const aiSlopEngine = {
1517
2522
  detectOverAbstraction(context),
1518
2523
  detectDeadPatterns(context),
1519
2524
  detectUnusedImports(context),
1520
- detectNarrativeComments(context)
2525
+ detectNarrativeComments(context),
2526
+ detectDuplicateImports(context),
2527
+ detectPythonPatterns(context),
2528
+ detectGoPatterns(context),
2529
+ detectRustPatterns(context),
2530
+ detectHallucinatedImports(context)
1521
2531
  ]);
1522
2532
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
1523
2533
  return {
@@ -1913,6 +2923,12 @@ const isDataFile = (content) => {
1913
2923
  const dataLinePattern = /^\s*[{}[\]"']/;
1914
2924
  return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
1915
2925
  };
2926
+ const TEST_PATH_RE = /(?:^|\/)(?:tests?|spec|specs|__tests__|__spec__|src\/test)\//i;
2927
+ const TEST_BASENAME_RE = /(?:^|[/.])(?:test_[\w-]+\.(?:py|rb)|[\w-]+_(?:test|spec)\.(?:py|rb|go|rs)|[\w-]+\.(?:test|spec)\.(?:[jt]sx?|mjs|cjs)|conftest\.py|[A-Z]\w*Tests?\.(?:java|cs|php))$/;
2928
+ const MIGRATION_PATH_RE = /(?:^|\/)(?:migrations?|migrate|prisma\/migrations|db\/migrate)\//i;
2929
+ const FIXTURE_PATH_RE = /(?:^|\/)(?:__fixtures__|__snapshots__|__mocks__|fixtures?|snapshots?|seeds?|stubs?)\//i;
2930
+ const GENERATED_PATH_RE = /(?:^|\/)(?:generated|gen|build|dist|out|target|coverage|node_modules|vendor|\.next|\.nuxt|\.svelte-kit)\//i;
2931
+ const isExemptFromComplexity = (relativePath) => TEST_PATH_RE.test(relativePath) || TEST_BASENAME_RE.test(relativePath) || MIGRATION_PATH_RE.test(relativePath) || FIXTURE_PATH_RE.test(relativePath) || GENERATED_PATH_RE.test(relativePath);
1916
2932
  const analyzeFunctions = (content, ext) => {
1917
2933
  const lines = content.split("\n");
1918
2934
  const functions = [];
@@ -1941,13 +2957,13 @@ const checkFileDiagnostics = (relativePath, content, limits) => {
1941
2957
  const lineCount = content.split("\n").length;
1942
2958
  const ext = path.extname(relativePath).toLowerCase();
1943
2959
  if (isDataFile(content)) return results;
1944
- const effectiveMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
1945
- if (lineCount > effectiveMax) results.push({
2960
+ const configuredMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
2961
+ if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
1946
2962
  filePath: relativePath,
1947
2963
  engine: "code-quality",
1948
2964
  rule: "complexity/file-too-large",
1949
2965
  severity: "warning",
1950
- message: `File has ${lineCount} lines (max: ${effectiveMax})`,
2966
+ message: `File has ${lineCount} lines (max: ${configuredMax})`,
1951
2967
  help: "Consider splitting this file into smaller modules",
1952
2968
  line: 0,
1953
2969
  column: 0,
@@ -1997,13 +3013,14 @@ const checkFunctionDiagnostics = (relativePath, fn, limits) => {
1997
3013
  return results;
1998
3014
  };
1999
3015
  const checkFileComplexity = (filePath, rootDirectory, limits) => {
3016
+ const relativePath = path.relative(rootDirectory, filePath);
3017
+ if (isExemptFromComplexity(relativePath)) return [];
2000
3018
  let content;
2001
3019
  try {
2002
3020
  content = fs.readFileSync(filePath, "utf-8");
2003
3021
  } catch {
2004
3022
  return [];
2005
3023
  }
2006
- const relativePath = path.relative(rootDirectory, filePath);
2007
3024
  const ext = path.extname(filePath).toLowerCase();
2008
3025
  const diagnostics = checkFileDiagnostics(relativePath, content, limits);
2009
3026
  for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
@@ -3579,7 +4596,10 @@ const lintEngine = {
3579
4596
  const diagnostics = [];
3580
4597
  const { languages, installedTools } = context;
3581
4598
  const promises = [];
3582
- if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runOxlint(context));
4599
+ if (languages.includes("typescript") || languages.includes("javascript")) {
4600
+ promises.push(runOxlint(context));
4601
+ if (context.config.lint.typecheck) promises.push(import("./typecheck-B1MXNAy-.js").then((mod) => mod.runTypecheck(context)));
4602
+ }
3583
4603
  if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
3584
4604
  if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
3585
4605
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
@@ -4597,6 +5617,7 @@ const runScopedScan = async (cwd, filePaths) => {
4597
5617
  audit: false,
4598
5618
  auditTimeout: 0
4599
5619
  },
5620
+ lint: { typecheck: false },
4600
5621
  architectureRulesPath: config.engines.architecture ? rulesPath : void 0
4601
5622
  }
4602
5623
  }, {
@@ -4635,6 +5656,9 @@ const readIfExists = (targetPath) => {
4635
5656
 
4636
5657
  //#endregion
4637
5658
  //#region src/hooks/quality-gate/baseline.ts
5659
+ const fingerprintDiagnostic = (d, rootDirectory) => {
5660
+ return `${path.isAbsolute(d.filePath) ? path.relative(rootDirectory, d.filePath) : d.filePath}:${d.line}:${d.rule}`;
5661
+ };
4638
5662
  const BASELINE_REL = path.join(".aislop", "baseline.json");
4639
5663
  const baselinePath = (cwd) => path.join(cwd, BASELINE_REL);
4640
5664
  const readBaseline = (cwd) => {
@@ -4642,8 +5666,16 @@ const readBaseline = (cwd) => {
4642
5666
  if (!raw) return null;
4643
5667
  try {
4644
5668
  const parsed = JSON.parse(raw);
4645
- if (parsed.schema !== "aislop.baseline.v1") return null;
4646
- return parsed;
5669
+ if (parsed.schema !== "aislop.baseline.v2" && parsed.schema !== "aislop.baseline.v1") return null;
5670
+ return {
5671
+ schema: "aislop.baseline.v2",
5672
+ updatedAt: parsed.updatedAt ?? "",
5673
+ score: parsed.score ?? 0,
5674
+ byEngine: parsed.byEngine ?? {},
5675
+ fileCount: parsed.fileCount ?? 0,
5676
+ commit: parsed.commit,
5677
+ findingFingerprints: parsed.findingFingerprints ?? []
5678
+ };
4647
5679
  } catch {
4648
5680
  return null;
4649
5681
  }
@@ -4667,7 +5699,8 @@ const captureBaseline = async (cwd) => {
4667
5699
  security: {
4668
5700
  audit: false,
4669
5701
  auditTimeout: 0
4670
- }
5702
+ },
5703
+ lint: { typecheck: false }
4671
5704
  }
4672
5705
  }, {
4673
5706
  format: config.engines.format,
@@ -4684,12 +5717,14 @@ const captureBaseline = async (cwd) => {
4684
5717
  const { score: engineScore } = calculateScore(diagnostics.filter((d) => r.diagnostics.includes(d)), config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
4685
5718
  byEngine[r.engine] = engineScore;
4686
5719
  }
5720
+ const findingFingerprints = diagnostics.filter((d) => d.severity === "error" || d.severity === "warning").map((d) => fingerprintDiagnostic(d, project.rootDirectory));
4687
5721
  const target = writeBaseline(cwd, {
4688
- schema: "aislop.baseline.v1",
5722
+ schema: "aislop.baseline.v2",
4689
5723
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4690
5724
  score,
4691
5725
  byEngine,
4692
- fileCount: project.sourceFileCount
5726
+ fileCount: project.sourceFileCount,
5727
+ findingFingerprints
4693
5728
  });
4694
5729
  return {
4695
5730
  score,
@@ -4778,7 +5813,10 @@ const runClaudeHook = async (deps = {}) => {
4778
5813
  const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
4779
5814
  const baseline = readBaseline(cwd);
4780
5815
  appendSessionFiles(cwd, files);
4781
- const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline?.score);
5816
+ const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline ? {
5817
+ score: baseline.score,
5818
+ findingFingerprints: baseline.findingFingerprints
5819
+ } : void 0);
4782
5820
  const envelope = renderClaudeOutput(JSON.stringify(feedback));
4783
5821
  write(JSON.stringify(envelope));
4784
5822
  return 0;
@@ -4788,6 +5826,42 @@ const runClaudeHook = async (deps = {}) => {
4788
5826
  release();
4789
5827
  }
4790
5828
  };
5829
+ const parseClaudeFileChangedStdin = (raw) => {
5830
+ if (!raw.trim()) return {};
5831
+ try {
5832
+ return JSON.parse(raw);
5833
+ } catch {
5834
+ return {};
5835
+ }
5836
+ };
5837
+ const runClaudeFileChangedHook = async (deps = {}) => {
5838
+ const getStdin = deps.stdin ?? readStdin$2;
5839
+ const write = deps.write ?? ((s) => process.stdout.write(s));
5840
+ const input = parseClaudeFileChangedStdin(await getStdin());
5841
+ const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
5842
+ const release = acquireHookLock(cwd);
5843
+ if (!release) return 0;
5844
+ try {
5845
+ const result = await captureBaseline(cwd);
5846
+ const changed = input.file_path ? path.relative(cwd, input.file_path) || input.file_path : "<unknown>";
5847
+ const envelope = renderClaudeOutput(JSON.stringify({
5848
+ schema: "aislop.hook.v2",
5849
+ event: "file_changed",
5850
+ file: changed,
5851
+ message: `Watched file changed (${changed}). aislop refreshed the baseline — score: ${result.score}.`,
5852
+ baseline: {
5853
+ score: result.score,
5854
+ fileCount: result.fileCount
5855
+ }
5856
+ }));
5857
+ write(JSON.stringify(envelope));
5858
+ return 0;
5859
+ } catch {
5860
+ return 0;
5861
+ } finally {
5862
+ release();
5863
+ }
5864
+ };
4791
5865
  const parseClaudeStopStdin = (raw) => {
4792
5866
  if (!raw.trim()) return {};
4793
5867
  try {
@@ -4810,7 +5884,10 @@ const runClaudeStopHook = async (deps = {}) => {
4810
5884
  if (!release) return 0;
4811
5885
  try {
4812
5886
  const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, sessionFiles);
4813
- const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline.score);
5887
+ const feedback = buildFeedback(diagnostics, score, rootDirectory, {
5888
+ score: baseline.score,
5889
+ findingFingerprints: baseline.findingFingerprints
5890
+ });
4814
5891
  if (!feedback.regressed) {
4815
5892
  clearSessionFiles(cwd);
4816
5893
  return 0;
@@ -4938,7 +6015,7 @@ const AISLOP_MD_BODY = `# aislop — agent instructions
4938
6015
 
4939
6016
  ## On every edit
4940
6017
 
4941
- A PostToolUse hook runs \`aislop hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`AislopFeedback\` (schema \`aislop.hook.v1\`). Act on them the same turn.
6018
+ A PostToolUse hook runs \`aislop hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`AislopFeedback\` (schema \`aislop.hook.v2\` — score, baseline, delta, regressed, counts, findings, newSinceBaseline, suggestedActions). Act on them the same turn; the \`suggestedActions\` field tells you exactly what to run next.
4942
6019
 
4943
6020
  ## Severity ladder
4944
6021
 
@@ -5175,6 +6252,25 @@ const buildStopHookGroup = () => {
5175
6252
  }]
5176
6253
  };
5177
6254
  };
6255
+ const FILE_CHANGED_MATCHER = ".aislop/config.yml|.aislop/rules.yml|package.json";
6256
+ const buildFileChangedHookGroup = () => {
6257
+ const hashBody = JSON.stringify({
6258
+ command: "aislop hook claude --on-file-changed",
6259
+ matcher: FILE_CHANGED_MATCHER
6260
+ });
6261
+ return {
6262
+ matcher: FILE_CHANGED_MATCHER,
6263
+ hooks: [{
6264
+ type: "command",
6265
+ command: "aislop hook claude --on-file-changed",
6266
+ [AISLOP_SENTINEL_KEY]: {
6267
+ v: 1,
6268
+ managed: true,
6269
+ hash: sentinelHash(hashBody)
6270
+ }
6271
+ }]
6272
+ };
6273
+ };
5178
6274
  const renderSettings$1 = (existingRaw, qualityGate) => {
5179
6275
  let obj = {};
5180
6276
  if (existingRaw) try {
@@ -5183,6 +6279,7 @@ const renderSettings$1 = (existingRaw, qualityGate) => {
5183
6279
  obj = {};
5184
6280
  }
5185
6281
  let next = upsertHookGroup(obj, "PostToolUse", buildHookGroup$1());
6282
+ next = upsertHookGroup(next, "FileChanged", buildFileChangedHookGroup());
5186
6283
  if (qualityGate) next = upsertHookGroup(next, "Stop", buildStopHookGroup());
5187
6284
  else next = removeAislopEntries(next, "Stop").next;
5188
6285
  return `${JSON.stringify(next, null, 2)}\n`;
@@ -5191,7 +6288,7 @@ const installClaude = (opts) => {
5191
6288
  const paths = resolveClaudePaths(opts);
5192
6289
  const result = emptyResult();
5193
6290
  const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
5194
- applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse hook");
6291
+ applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse + FileChanged hooks");
5195
6292
  const mdHash = sentinelHash(AISLOP_MD_BODY);
5196
6293
  const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, mdHash);
5197
6294
  applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
@@ -5221,7 +6318,9 @@ const uninstallClaude = (opts) => {
5221
6318
  } catch {
5222
6319
  obj = {};
5223
6320
  }
5224
- const stripped = removeAislopEntries(removeAislopEntries(obj, "PostToolUse").next, "Stop").next;
6321
+ const afterPostToolUse = removeAislopEntries(obj, "PostToolUse").next;
6322
+ const afterFileChanged = removeAislopEntries(afterPostToolUse, "FileChanged").next;
6323
+ const stripped = removeAislopEntries(afterFileChanged, "Stop").next;
5225
6324
  const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
5226
6325
  const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
5227
6326
  if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
@@ -5230,7 +6329,7 @@ const uninstallClaude = (opts) => {
5230
6329
  if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
5231
6330
  else result.skipped.push(paths.aislopMd);
5232
6331
  const claudeMd = readIfExists(paths.claudeMd);
5233
- if (claudeMd != null && claudeMd.includes("@AISLOP.md")) {
6332
+ if (claudeMd?.includes("@AISLOP.md")) {
5234
6333
  const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
5235
6334
  applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
5236
6335
  } else result.skipped.push(paths.claudeMd);
@@ -5791,7 +6890,9 @@ const hookRun = async (agent, flags) => {
5791
6890
  process.exit(0);
5792
6891
  }
5793
6892
  let exitCode = 0;
5794
- if (agent === "claude") exitCode = flags?.stop ? await runClaudeStopHook() : await runClaudeHook();
6893
+ if (agent === "claude") if (flags?.onFileChanged) exitCode = await runClaudeFileChangedHook();
6894
+ else if (flags?.stop) exitCode = await runClaudeStopHook();
6895
+ else exitCode = await runClaudeHook();
5795
6896
  else if (agent === "cursor") exitCode = await runCursorHook();
5796
6897
  else if (agent === "gemini") exitCode = await runGeminiHook();
5797
6898
  else {
@@ -5942,8 +7043,11 @@ const registerCallbacks = (hook) => {
5942
7043
  hook.command("baseline").description("Capture the current project score as the quality-gate baseline").action(async () => {
5943
7044
  await hookBaseline();
5944
7045
  });
5945
- hook.command("claude").description("Internal: Claude Code PostToolUse / Stop callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").action(async (opts) => {
5946
- await hookRun("claude", { stop: Boolean(opts.stop) });
7046
+ hook.command("claude").description("Internal: Claude Code PostToolUse / Stop / FileChanged callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").option("--on-file-changed", "run in FileChanged mode (refresh baseline on watched file change)").action(async (opts) => {
7047
+ await hookRun("claude", {
7048
+ stop: Boolean(opts.stop),
7049
+ onFileChanged: Boolean(opts.onFileChanged)
7050
+ });
5947
7051
  });
5948
7052
  hook.command("cursor").description("Internal: Cursor afterFileEdit callback (reads stdin)").action(async () => {
5949
7053
  await hookRun("cursor");
@@ -6452,7 +7556,7 @@ const renderCleanRun = (input, deps = {}) => {
6452
7556
 
6453
7557
  //#endregion
6454
7558
  //#region src/version.ts
6455
- const APP_VERSION = "0.7.0";
7559
+ const APP_VERSION = "0.8.0";
6456
7560
 
6457
7561
  //#endregion
6458
7562
  //#region src/utils/telemetry.ts
@@ -6607,6 +7711,7 @@ const scanCommand = async (directory, config, options) => {
6607
7711
  const engineConfig = {
6608
7712
  quality: config.quality,
6609
7713
  security: config.security,
7714
+ lint: config.lint,
6610
7715
  architectureRulesPath: config.engines.architecture ? rulesPath : void 0
6611
7716
  };
6612
7717
  const gridRows = ALL_ENGINE_NAMES.filter((engine) => config.engines[engine] !== false).map((engine) => ({
@@ -6674,7 +7779,7 @@ const scanCommand = async (directory, config, options) => {
6674
7779
  });
6675
7780
  }
6676
7781
  if (options.json) {
6677
- const { buildJsonOutput } = await import("./json-B51etWTw.js");
7782
+ const { buildJsonOutput } = await import("./json-BbMwrgyd.js");
6678
7783
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
6679
7784
  console.log(JSON.stringify(jsonOut, null, 2));
6680
7785
  return { exitCode };
@@ -6885,16 +7990,29 @@ const planFormat = (ctx) => {
6885
7990
  skipReason: "no supported language"
6886
7991
  };
6887
7992
  };
6888
- const planLint = (ctx) => {
6889
- const { languages, frameworks, installedTools } = ctx.projectInfo;
6890
- if (frameworks.includes("expo")) return {
6891
- tool: "expo-doctor + oxlint (bundled)",
7993
+ const findLocalTsc = (root) => {
7994
+ const candidate = path.join(root, "node_modules", ".bin", "tsc");
7995
+ return fs.existsSync(candidate) ? candidate : null;
7996
+ };
7997
+ const withTypecheckSuffix = (baseTool, ctx) => {
7998
+ if (!ctx.config.lint?.typecheck) return {
7999
+ tool: baseTool,
6892
8000
  status: "ok"
6893
8001
  };
6894
- if (hasJsLike(languages)) return {
6895
- tool: "oxlint (bundled)",
8002
+ if (findLocalTsc(ctx.rootDirectory)) return {
8003
+ tool: `${baseTool} + tsc`,
6896
8004
  status: "ok"
6897
8005
  };
8006
+ return {
8007
+ tool: `${baseTool} + tsc not found`,
8008
+ status: "missing",
8009
+ remediation: "Install TypeScript locally (pnpm add -D typescript), or set lint.typecheck: false in .aislop/config.yml."
8010
+ };
8011
+ };
8012
+ const planLint = (ctx) => {
8013
+ const { languages, frameworks, installedTools } = ctx.projectInfo;
8014
+ if (frameworks.includes("expo")) return withTypecheckSuffix("expo-doctor + oxlint (bundled)", ctx);
8015
+ if (hasJsLike(languages)) return withTypecheckSuffix("oxlint (bundled)", ctx);
6898
8016
  return firstMatching(languages, installedTools, LINT_SPECS) ?? {
6899
8017
  tool: "no linter",
6900
8018
  status: "skipped",
@@ -7489,6 +8607,212 @@ const applyEditsAndCollapse = (lines, linesToRemove, lineReplacements) => {
7489
8607
  return collapsed.join("\n");
7490
8608
  };
7491
8609
 
8610
+ //#endregion
8611
+ //#region src/engines/ai-slop/duplicate-imports-fix.ts
8612
+ const JS_EXTENSIONS = new Set([
8613
+ ".ts",
8614
+ ".tsx",
8615
+ ".js",
8616
+ ".jsx",
8617
+ ".mjs",
8618
+ ".cjs"
8619
+ ]);
8620
+ const IMPORT_FROM_RE = /^\s*import\s+(.*?)\s+from\s+["']([^"']+)["']\s*;?\s*$/;
8621
+ const SIDE_EFFECT_RE = /^\s*import\s+["']([^"']+)["']\s*;?\s*$/;
8622
+ const parseNamedClause = (clause) => {
8623
+ const inner = clause.trim().slice(1, -1).trim();
8624
+ if (inner.length === 0) return [];
8625
+ const items = [];
8626
+ for (const part of inner.split(",")) {
8627
+ const trimmed = part.trim();
8628
+ if (!trimmed) continue;
8629
+ let isType = false;
8630
+ let working = trimmed;
8631
+ if (/^type\s+/.test(working)) {
8632
+ isType = true;
8633
+ working = working.replace(/^type\s+/, "");
8634
+ }
8635
+ const aliasMatch = working.match(/^(\w+)\s+as\s+(\w+)$/);
8636
+ if (aliasMatch) {
8637
+ items.push({
8638
+ name: aliasMatch[1],
8639
+ alias: aliasMatch[2],
8640
+ isType
8641
+ });
8642
+ continue;
8643
+ }
8644
+ if (/^\w+$/.test(working)) items.push({
8645
+ name: working,
8646
+ isType
8647
+ });
8648
+ }
8649
+ return items;
8650
+ };
8651
+ const parseImportClause = (clause) => {
8652
+ let rest = clause.trim();
8653
+ let isTypeOnly = false;
8654
+ if (/^type\s+/.test(rest)) {
8655
+ isTypeOnly = true;
8656
+ rest = rest.replace(/^type\s+/, "");
8657
+ }
8658
+ const out = {
8659
+ named: [],
8660
+ isTypeOnly
8661
+ };
8662
+ const defMatch = rest.match(/^([A-Za-z_$][\w$]*)\s*(?:,\s*(.+))?$/);
8663
+ if (defMatch && !rest.startsWith("{") && !rest.startsWith("*")) {
8664
+ out.default = defMatch[1];
8665
+ rest = defMatch[2]?.trim() ?? "";
8666
+ }
8667
+ if (rest.startsWith("*")) {
8668
+ const nsMatch = rest.match(/^\*\s+as\s+(\w+)/);
8669
+ if (nsMatch) out.namespace = nsMatch[1];
8670
+ return out;
8671
+ }
8672
+ if (rest.startsWith("{")) out.named = parseNamedClause(rest);
8673
+ return out;
8674
+ };
8675
+ const parseImportLine = (line, lineIndex) => {
8676
+ const sideEffect = line.match(SIDE_EFFECT_RE);
8677
+ if (sideEffect) return {
8678
+ lineIndex,
8679
+ module: sideEffect[1],
8680
+ named: [],
8681
+ isTypeOnly: false,
8682
+ isSideEffect: true
8683
+ };
8684
+ const m = line.match(IMPORT_FROM_RE);
8685
+ if (!m) return null;
8686
+ return {
8687
+ lineIndex,
8688
+ module: m[2],
8689
+ isSideEffect: false,
8690
+ ...parseImportClause(m[1])
8691
+ };
8692
+ };
8693
+ const formatNamed = (n, stripType) => {
8694
+ const prefix = n.isType && !stripType ? "type " : "";
8695
+ const suffix = n.alias ? ` as ${n.alias}` : "";
8696
+ return `${prefix}${n.name}${suffix}`;
8697
+ };
8698
+ const mergeImports = (group) => {
8699
+ if (group.some((s) => s.isSideEffect)) return null;
8700
+ if (group.some((s) => s.namespace !== void 0)) return null;
8701
+ if (group.some((s) => s.isTypeOnly && s.default !== void 0)) return null;
8702
+ const defaults = group.map((s) => s.default).filter((d) => d !== void 0);
8703
+ const uniqueDefaults = Array.from(new Set(defaults));
8704
+ if (uniqueDefaults.length > 1) return null;
8705
+ const defaultName = uniqueDefaults[0];
8706
+ const merged = /* @__PURE__ */ new Map();
8707
+ for (const stmt of group) for (const nm of stmt.named) {
8708
+ const key = nm.alias ?? nm.name;
8709
+ const isType = nm.isType || stmt.isTypeOnly;
8710
+ const existing = merged.get(key);
8711
+ if (!existing) merged.set(key, {
8712
+ ...nm,
8713
+ isType
8714
+ });
8715
+ else existing.isType = existing.isType && isType;
8716
+ }
8717
+ const insertionOrder = Array.from(merged.values());
8718
+ const namedList = [...insertionOrder.filter((n) => !n.isType), ...insertionOrder.filter((n) => n.isType)];
8719
+ const allTypeOnly = namedList.length > 0 && namedList.every((n) => n.isType);
8720
+ const module = group[0].module;
8721
+ if (!defaultName && namedList.length === 0) return null;
8722
+ if (!defaultName && allTypeOnly) return `import type { ${namedList.map((n) => formatNamed(n, true)).join(", ")} } from "${module}";`;
8723
+ const parts = [];
8724
+ if (defaultName) parts.push(defaultName);
8725
+ if (namedList.length > 0) {
8726
+ const items = namedList.map((n) => formatNamed(n, false)).join(", ");
8727
+ parts.push(`{ ${items} }`);
8728
+ }
8729
+ return `import ${parts.join(", ")} from "${module}";`;
8730
+ };
8731
+ const fixDuplicateImports = async (context) => {
8732
+ const files = getSourceFiles(context);
8733
+ for (const filePath of files) {
8734
+ if (!JS_EXTENSIONS.has(path.extname(filePath))) continue;
8735
+ if (isAutoGenerated(filePath)) continue;
8736
+ let content;
8737
+ try {
8738
+ content = fs.readFileSync(filePath, "utf-8");
8739
+ } catch {
8740
+ continue;
8741
+ }
8742
+ const lines = content.split("\n");
8743
+ const imports = [];
8744
+ for (let i = 0; i < lines.length; i++) {
8745
+ const stmt = parseImportLine(lines[i], i);
8746
+ if (stmt) imports.push(stmt);
8747
+ }
8748
+ if (imports.length < 2) continue;
8749
+ const groups = /* @__PURE__ */ new Map();
8750
+ for (const stmt of imports) {
8751
+ const list = groups.get(stmt.module) ?? [];
8752
+ list.push(stmt);
8753
+ groups.set(stmt.module, list);
8754
+ }
8755
+ const linesToRemove = /* @__PURE__ */ new Set();
8756
+ const replacements = /* @__PURE__ */ new Map();
8757
+ let modified = false;
8758
+ for (const group of groups.values()) {
8759
+ if (group.length < 2) continue;
8760
+ const merged = mergeImports(group);
8761
+ if (!merged) continue;
8762
+ replacements.set(group[0].lineIndex, merged);
8763
+ for (const stmt of group.slice(1)) linesToRemove.add(stmt.lineIndex);
8764
+ modified = true;
8765
+ }
8766
+ if (!modified) continue;
8767
+ const next = [...lines];
8768
+ for (const [idx, replacement] of replacements) next[idx] = replacement;
8769
+ const sortedRemove = Array.from(linesToRemove).sort((a, b) => b - a);
8770
+ for (const idx of sortedRemove) next.splice(idx, 1);
8771
+ fs.writeFileSync(filePath, next.join("\n"));
8772
+ }
8773
+ };
8774
+
8775
+ //#endregion
8776
+ //#region src/engines/ai-slop/narrative-comments-fix.ts
8777
+ const fixNarrativeComments = async (context) => {
8778
+ const diagnostics = await detectNarrativeComments(context);
8779
+ if (diagnostics.length === 0) return;
8780
+ const byFile = /* @__PURE__ */ new Map();
8781
+ for (const d of diagnostics) {
8782
+ const abs = d.filePath.startsWith("/") ? d.filePath : `${context.rootDirectory}/${d.filePath}`;
8783
+ const list = byFile.get(abs) ?? [];
8784
+ list.push(d);
8785
+ byFile.set(abs, list);
8786
+ }
8787
+ for (const [filePath, diags] of byFile) {
8788
+ const syntax = getCommentSyntax(path.extname(filePath));
8789
+ if (!syntax) continue;
8790
+ let content;
8791
+ try {
8792
+ content = fs.readFileSync(filePath, "utf-8");
8793
+ } catch {
8794
+ continue;
8795
+ }
8796
+ const lines = content.split("\n");
8797
+ const blocks = collectBlocks(lines, syntax);
8798
+ const toRemove = /* @__PURE__ */ new Set();
8799
+ for (const d of diags) {
8800
+ const block = blocks.find((b) => b.startLine === d.line);
8801
+ if (!block) continue;
8802
+ for (let ln = block.startLine; ln <= block.endLine; ln += 1) toRemove.add(ln);
8803
+ const prev = block.startLine - 1;
8804
+ const next = block.endLine + 1;
8805
+ const prevIsBlank = prev >= 1 && lines[prev - 1]?.trim() === "";
8806
+ const nextIsBlank = next <= lines.length && lines[next - 1]?.trim() === "";
8807
+ if (prevIsBlank && nextIsBlank) toRemove.add(prev);
8808
+ }
8809
+ const kept = [];
8810
+ for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
8811
+ const newContent = kept.join("\n");
8812
+ if (newContent !== content) fs.writeFileSync(filePath, newContent);
8813
+ }
8814
+ };
8815
+
7492
8816
  //#endregion
7493
8817
  //#region src/engines/ai-slop/unused-imports-fix.ts
7494
8818
  const fixUnusedImports = async (context) => {
@@ -7510,9 +8834,9 @@ const fixUnusedImports = async (context) => {
7510
8834
  for (const [lineNo, syms] of symbolsByLine) {
7511
8835
  const lineIdx = lineNo - 1;
7512
8836
  const allUnused = syms.every((s) => unusedNames.has(s.name));
7513
- const importSpan = JS_EXTENSIONS.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
8837
+ const importSpan = JS_EXTENSIONS$1.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
7514
8838
  if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
7515
- else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
8839
+ else if (JS_EXTENSIONS$1.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
7516
8840
  else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
7517
8841
  }
7518
8842
  if (linesToRemove.size === 0 && unused.length === 0) continue;
@@ -8245,6 +9569,7 @@ const hasJsOrTs = (projectInfo) => projectInfo.languages.includes("typescript")
8245
9569
  const runAiSlopSteps = async (deps) => {
8246
9570
  if (!deps.config.engines["ai-slop"]) return;
8247
9571
  await deps.runStep("Unused imports", () => detectUnusedImports(deps.context), () => fixUnusedImports(deps.context));
9572
+ await deps.runStep("Duplicate imports", () => detectDuplicateImports(deps.context), () => fixDuplicateImports(deps.context));
8248
9573
  const detectFixableSlop = async () => {
8249
9574
  const [comments, dead, narrative] = await Promise.all([
8250
9575
  detectTrivialComments(deps.context),
@@ -8350,7 +9675,8 @@ const createEngineContext = (rootDirectory, projectInfo, config) => ({
8350
9675
  installedTools: projectInfo.installedTools,
8351
9676
  config: {
8352
9677
  quality: config.quality,
8353
- security: config.security
9678
+ security: config.security,
9679
+ lint: config.lint
8354
9680
  }
8355
9681
  });
8356
9682
  const fixCommand = async (directory, config, options = {
@@ -8413,6 +9739,7 @@ const fixCommand = async (directory, config, options = {
8413
9739
  const engineConfig = {
8414
9740
  quality: config.quality,
8415
9741
  security: config.security,
9742
+ lint: config.lint,
8416
9743
  architectureRulesPath: config.engines.architecture ? rulesPath : void 0
8417
9744
  };
8418
9745
  rail.start("Verifying results");
@@ -8717,8 +10044,10 @@ const buildRulesRender = (input) => {
8717
10044
  const AI_SLOP_FIXABLE = new Set([
8718
10045
  "ai-slop/trivial-comment",
8719
10046
  "ai-slop/unused-import",
8720
- "ai-slop/narrative-comment"
10047
+ "ai-slop/narrative-comment",
10048
+ "ai-slop/duplicate-import"
8721
10049
  ]);
10050
+ const AI_SLOP_ERRORS = new Set(["ai-slop/hallucinated-import"]);
8722
10051
  const BUILTIN_RULES = [
8723
10052
  {
8724
10053
  engine: "format",
@@ -8739,7 +10068,8 @@ const BUILTIN_RULES = [
8739
10068
  "ruff/*",
8740
10069
  "go/*",
8741
10070
  "clippy/*",
8742
- "rubocop/*"
10071
+ "rubocop/*",
10072
+ "typescript/*"
8743
10073
  ]
8744
10074
  },
8745
10075
  {
@@ -8775,7 +10105,16 @@ const BUILTIN_RULES = [
8775
10105
  "ai-slop/unsafe-type-assertion",
8776
10106
  "ai-slop/double-type-assertion",
8777
10107
  "ai-slop/ts-directive",
8778
- "ai-slop/narrative-comment"
10108
+ "ai-slop/narrative-comment",
10109
+ "ai-slop/duplicate-import",
10110
+ "ai-slop/python-bare-except",
10111
+ "ai-slop/python-broad-except",
10112
+ "ai-slop/python-mutable-default",
10113
+ "ai-slop/python-print-debug",
10114
+ "ai-slop/go-library-panic",
10115
+ "ai-slop/rust-non-test-unwrap",
10116
+ "ai-slop/rust-todo-stub",
10117
+ "ai-slop/hallucinated-import"
8779
10118
  ]
8780
10119
  },
8781
10120
  {
@@ -8806,7 +10145,7 @@ const toRuleEntry = (engine, ruleId) => {
8806
10145
  if (engine === "ai-slop") return {
8807
10146
  id: ruleId,
8808
10147
  engine,
8809
- severity: "warning",
10148
+ severity: AI_SLOP_ERRORS.has(ruleId) ? "error" : "warning",
8810
10149
  fixable: AI_SLOP_FIXABLE.has(ruleId)
8811
10150
  };
8812
10151
  return {
@@ -9120,4 +10459,4 @@ const main = async () => {
9120
10459
  main();
9121
10460
 
9122
10461
  //#endregion
9123
- export { ENGINE_INFO as n, APP_VERSION as t };
10462
+ export { ENGINE_INFO as n, runSubprocess as r, APP_VERSION as t };