aislop 0.6.0 → 0.6.2

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-C2lM_2fE.js";
1
+ import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-AmNwcw_U.js";
2
2
  import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
3
3
  import { r as runGenericLinter, t as fixRubyLint } from "./generic-BrcWMW7E.js";
4
4
  import { n as runExpoDoctor } from "./expo-doctor-Bz0LZhQ6.js";
@@ -10,6 +10,7 @@ import { z } from "zod/v4";
10
10
  import pc from "picocolors";
11
11
  import wcwidth from "wcwidth";
12
12
  import { spawnSync } from "node:child_process";
13
+ import micromatch from "micromatch";
13
14
  import { fileURLToPath } from "node:url";
14
15
  import { performance } from "node:perf_hooks";
15
16
  import os from "node:os";
@@ -19,6 +20,13 @@ import { isCancel, multiselect, select, text } from "@clack/prompts";
19
20
  //#region src/config/defaults.ts
20
21
  const DEFAULT_CONFIG = {
21
22
  version: 1,
23
+ exclude: [
24
+ "node_modules",
25
+ ".git",
26
+ "dist",
27
+ "build",
28
+ "coverage"
29
+ ],
22
30
  engines: {
23
31
  format: true,
24
32
  lint: true,
@@ -172,7 +180,14 @@ const AislopConfigSchema = z.object({
172
180
  failBelow: 0,
173
181
  format: "json"
174
182
  })),
175
- telemetry: TelemetrySchema.default(() => ({ enabled: true }))
183
+ telemetry: TelemetrySchema.default(() => ({ enabled: true })),
184
+ exclude: z.array(z.string()).default(() => [
185
+ "node_modules",
186
+ ".git",
187
+ "dist",
188
+ "build",
189
+ "coverage"
190
+ ])
176
191
  });
177
192
  const defaults = AislopConfigSchema.parse({});
178
193
  /**
@@ -600,7 +615,15 @@ const listProjectFiles = (rootDirectory) => {
600
615
  if (findResult.error || findResult.status !== 0) return [];
601
616
  return findResult.stdout.split("\n").filter((file) => file.length > 0).map((file) => file.replace(/^\.\//, ""));
602
617
  };
603
- const filterProjectFiles = (rootDirectory, files, extraExtensions = []) => {
618
+ const normalizeExcludePatterns = (patterns) => {
619
+ return patterns.flatMap((pattern) => {
620
+ const p = pattern.trim();
621
+ if (p.startsWith(".")) return [`**/*${p}`];
622
+ if (!p.includes("*") && !p.includes(".")) return [`${p}/**`];
623
+ return [p];
624
+ });
625
+ };
626
+ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude = []) => {
604
627
  const extraSet = new Set(extraExtensions);
605
628
  const normalizedFiles = files.map((file) => {
606
629
  const absolutePath = path.isAbsolute(file) ? file : path.resolve(rootDirectory, file);
@@ -610,7 +633,14 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = []) => {
610
633
  };
611
634
  }).filter(({ relativePath }) => isWithinProject(relativePath));
612
635
  const ignoredPaths = getIgnoredPaths(rootDirectory, normalizedFiles.map(({ relativePath }) => relativePath));
613
- return normalizedFiles.filter(({ absolutePath, relativePath }) => hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile(relativePath) && !ignoredPaths.has(relativePath) && fs.existsSync(absolutePath)).map(({ absolutePath }) => absolutePath);
636
+ const normalizedExcludePatterns = exclude.length ? normalizeExcludePatterns(exclude) : [];
637
+ const isUserExcluded = (relativePath) => {
638
+ if (!normalizedExcludePatterns.length) return false;
639
+ return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
640
+ };
641
+ return normalizedFiles.filter(({ absolutePath, relativePath }) => {
642
+ return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
643
+ }).map(({ absolutePath }) => absolutePath);
614
644
  };
615
645
  const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
616
646
  const extraSet = new Set(extraExtensions);
@@ -875,53 +905,44 @@ const primaryLanguage = (langs) => {
875
905
  ]) if (langs.includes(lang)) return lang;
876
906
  return null;
877
907
  };
908
+ const systemToolDecision = (installed, spec) => installed[spec.binary] ? {
909
+ tool: `${spec.toolLabel} (system)`,
910
+ status: "ok"
911
+ } : {
912
+ tool: `${spec.toolLabel} not found`,
913
+ status: "missing",
914
+ remediation: spec.remediation
915
+ };
916
+ const firstMatching = (langs, installed, specs) => {
917
+ for (const spec of specs) if (langs.includes(spec.language)) return systemToolDecision(installed, spec);
918
+ return null;
919
+ };
920
+ const spec = (language, binary, toolLabel, remediation) => ({
921
+ language,
922
+ binary,
923
+ toolLabel,
924
+ remediation
925
+ });
926
+ const FORMAT_SPECS = [
927
+ spec("python", "ruff", "ruff", "Install: pipx install ruff"),
928
+ spec("go", "gofmt", "gofmt", "Install: via go toolchain — https://go.dev/dl/"),
929
+ spec("rust", "cargo", "cargo fmt", "Install: rustup component add rustfmt"),
930
+ spec("ruby", "rubocop", "rubocop", "Install: gem install rubocop"),
931
+ spec("php", "php-cs-fixer", "php-cs-fixer", "Install: composer global require friendsofphp/php-cs-fixer")
932
+ ];
933
+ const LINT_SPECS = [
934
+ spec("python", "ruff", "ruff", "Install: pipx install ruff"),
935
+ spec("go", "golangci-lint", "golangci-lint", "Install: brew install golangci-lint"),
936
+ spec("rust", "clippy-driver", "clippy", "Install: rustup component add clippy"),
937
+ spec("ruby", "rubocop", "rubocop", "Install: gem install rubocop")
938
+ ];
878
939
  const planFormat = (ctx) => {
879
940
  const { languages, installedTools } = ctx.projectInfo;
880
941
  if (hasJsLike(languages)) return {
881
942
  tool: "biome (bundled)",
882
943
  status: "ok"
883
944
  };
884
- if (languages.includes("python")) return installedTools["ruff"] ? {
885
- tool: "ruff (system)",
886
- status: "ok"
887
- } : {
888
- tool: "ruff not found",
889
- status: "missing",
890
- remediation: "Install: pipx install ruff"
891
- };
892
- if (languages.includes("go")) return installedTools["gofmt"] ? {
893
- tool: "gofmt (system)",
894
- status: "ok"
895
- } : {
896
- tool: "gofmt not found",
897
- status: "missing",
898
- remediation: "Install: via go toolchain — https://go.dev/dl/"
899
- };
900
- if (languages.includes("rust")) return installedTools["cargo"] ? {
901
- tool: "cargo fmt (system)",
902
- status: "ok"
903
- } : {
904
- tool: "cargo fmt not found",
905
- status: "missing",
906
- remediation: "Install: rustup component add rustfmt"
907
- };
908
- if (languages.includes("ruby")) return installedTools["rubocop"] ? {
909
- tool: "rubocop (system)",
910
- status: "ok"
911
- } : {
912
- tool: "rubocop not found",
913
- status: "missing",
914
- remediation: "Install: gem install rubocop"
915
- };
916
- if (languages.includes("php")) return installedTools["php-cs-fixer"] ? {
917
- tool: "php-cs-fixer (system)",
918
- status: "ok"
919
- } : {
920
- tool: "php-cs-fixer not found",
921
- status: "missing",
922
- remediation: "Install: composer global require friendsofphp/php-cs-fixer"
923
- };
924
- return {
945
+ return firstMatching(languages, installedTools, FORMAT_SPECS) ?? {
925
946
  tool: "no formatter",
926
947
  status: "skipped",
927
948
  skipReason: "no supported language"
@@ -937,39 +958,7 @@ const planLint = (ctx) => {
937
958
  tool: "oxlint (bundled)",
938
959
  status: "ok"
939
960
  };
940
- if (languages.includes("python")) return installedTools["ruff"] ? {
941
- tool: "ruff (system)",
942
- status: "ok"
943
- } : {
944
- tool: "ruff not found",
945
- status: "missing",
946
- remediation: "Install: pipx install ruff"
947
- };
948
- if (languages.includes("go")) return installedTools["golangci-lint"] ? {
949
- tool: "golangci-lint (system)",
950
- status: "ok"
951
- } : {
952
- tool: "golangci-lint not found",
953
- status: "missing",
954
- remediation: "Install: brew install golangci-lint"
955
- };
956
- if (languages.includes("rust")) return installedTools["clippy-driver"] ? {
957
- tool: "clippy (system)",
958
- status: "ok"
959
- } : {
960
- tool: "clippy not found",
961
- status: "missing",
962
- remediation: "Install: rustup component add clippy"
963
- };
964
- if (languages.includes("ruby")) return installedTools["rubocop"] ? {
965
- tool: "rubocop (system)",
966
- status: "ok"
967
- } : {
968
- tool: "rubocop not found",
969
- status: "missing",
970
- remediation: "Install: gem install rubocop"
971
- };
972
- return {
961
+ return firstMatching(languages, installedTools, LINT_SPECS) ?? {
973
962
  tool: "no linter",
974
963
  status: "skipped",
975
964
  skipReason: "no supported language"
@@ -989,42 +978,64 @@ const planAiSlop = (_ctx) => ({
989
978
  tool: "built-in",
990
979
  status: "ok"
991
980
  });
981
+ const AUDIT_SPECS = [
982
+ {
983
+ files: ["pnpm-lock.yaml"],
984
+ bundled: "pnpm audit"
985
+ },
986
+ {
987
+ files: ["package-lock.json"],
988
+ bundled: "npm audit"
989
+ },
990
+ {
991
+ files: [
992
+ "requirements.txt",
993
+ "poetry.lock",
994
+ "Pipfile.lock"
995
+ ],
996
+ systemTool: {
997
+ binary: "pip-audit",
998
+ toolLabel: "pip-audit",
999
+ remediation: "Install: pipx install pip-audit"
1000
+ }
1001
+ },
1002
+ {
1003
+ files: ["Cargo.toml"],
1004
+ systemTool: {
1005
+ binary: "cargo-audit",
1006
+ toolLabel: "cargo audit",
1007
+ remediation: "Install: cargo install cargo-audit",
1008
+ requiresBinaries: ["cargo", "cargo-audit"]
1009
+ }
1010
+ },
1011
+ {
1012
+ files: ["go.mod"],
1013
+ systemTool: {
1014
+ binary: "govulncheck",
1015
+ toolLabel: "govulncheck",
1016
+ remediation: "Install: go install golang.org/x/vuln/cmd/govulncheck@latest"
1017
+ }
1018
+ }
1019
+ ];
992
1020
  const planSecurity = (ctx) => {
993
1021
  const { rootDirectory, projectInfo } = ctx;
994
1022
  const { installedTools } = projectInfo;
995
1023
  const hasFile = (rel) => fs.existsSync(path.join(rootDirectory, rel));
996
- if (hasFile("pnpm-lock.yaml")) return {
997
- tool: "pnpm audit",
998
- status: "ok"
999
- };
1000
- if (hasFile("package-lock.json")) return {
1001
- tool: "npm audit",
1002
- status: "ok"
1003
- };
1004
- if (hasFile("requirements.txt") || hasFile("poetry.lock") || hasFile("Pipfile.lock")) return installedTools["pip-audit"] ? {
1005
- tool: "pip-audit (system)",
1006
- status: "ok"
1007
- } : {
1008
- tool: "pip-audit not found",
1009
- status: "missing",
1010
- remediation: "Install: pipx install pip-audit"
1011
- };
1012
- if (hasFile("Cargo.toml")) return installedTools["cargo"] && installedTools["cargo-audit"] ? {
1013
- tool: "cargo audit (system)",
1014
- status: "ok"
1015
- } : {
1016
- tool: "cargo audit not found",
1017
- status: "missing",
1018
- remediation: "Install: cargo install cargo-audit"
1019
- };
1020
- if (hasFile("go.mod")) return installedTools["govulncheck"] ? {
1021
- tool: "govulncheck (system)",
1022
- status: "ok"
1023
- } : {
1024
- tool: "govulncheck not found",
1025
- status: "missing",
1026
- remediation: "Install: go install golang.org/x/vuln/cmd/govulncheck@latest"
1027
- };
1024
+ for (const spec of AUDIT_SPECS) {
1025
+ if (!spec.files.some(hasFile)) continue;
1026
+ if (spec.bundled) return {
1027
+ tool: spec.bundled,
1028
+ status: "ok"
1029
+ };
1030
+ if (spec.systemTool) return (spec.systemTool.requiresBinaries ?? [spec.systemTool.binary]).every((b) => installedTools[b]) ? {
1031
+ tool: `${spec.systemTool.toolLabel} (system)`,
1032
+ status: "ok"
1033
+ } : {
1034
+ tool: `${spec.systemTool.toolLabel} not found`,
1035
+ status: "missing",
1036
+ remediation: spec.systemTool.remediation
1037
+ };
1038
+ }
1028
1039
  return {
1029
1040
  tool: "no auditor",
1030
1041
  status: "skipped",
@@ -1206,23 +1217,9 @@ const detectOverAbstraction = async (context) => {
1206
1217
 
1207
1218
  //#endregion
1208
1219
  //#region src/engines/ai-slop/comments.ts
1209
- const TRIVIAL_JS_COMMENT_PATTERNS = [
1210
- /\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i,
1211
- /\/\/\s*Import(?:ing|s)?\s+/i,
1212
- /\/\/\s*Defin(?:e|ing)\s+(?:the\s+)?/i,
1213
- /\/\/\s*Initializ(?:e|ing)\s+(?:the\s+)?/i,
1214
- /\/\/\s*Set(?:ting)?\s+\w+\s+to\s+/i,
1215
- /\/\/\s*Return(?:ing|s)?\s+(?:the\s+)?/i,
1216
- /\/\/\s*Check(?:ing)?\s+(?:if|whether)\s+/i,
1217
- /\/\/\s*(?:Loop(?:ing)?\s+through|Iterat(?:e|ing)\s+over)\s+/i,
1218
- /\/\/\s*Creat(?:e|ing)\s+(?:a\s+(?:new\s+)?)?/i,
1219
- /\/\/\s*Updat(?:e|ing)\s+(?:the\s+)?/i,
1220
- /\/\/\s*(?:Delet|Remov)(?:e|ing)\s+(?:the\s+)?/i,
1221
- /\/\/\s*Handl(?:e|ing)\s+(?:the\s+)?/i,
1222
- /\/\/\s*(?:Get(?:ting)?|Fetch(?:ing)?)\s+(?:the\s+)?/i,
1223
- /\/\/\s*(?:Increment|Decrement)(?:ing)?\s+/i
1224
- ];
1225
- const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, /^#\s*(?:Import|Define|Initialize|Return|Check|Create|Update|Delete|Handle|Get|Fetch)/i];
1220
+ 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";
1221
+ 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")];
1222
+ 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")];
1226
1223
  const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
1227
1224
  const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
1228
1225
  const MAX_TRIVIAL_COMMENT_LENGTH = 60;
@@ -1298,6 +1295,18 @@ const JS_EXTENSIONS$1 = new Set([
1298
1295
  ".cjs"
1299
1296
  ]);
1300
1297
  const CONSOLE_LOG_PATTERN = /\bconsole\.(?:log|debug|info|trace|dir|table)\s*\(/;
1298
+ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
1299
+ filePath,
1300
+ engine: "ai-slop",
1301
+ rule,
1302
+ severity,
1303
+ message,
1304
+ help,
1305
+ line,
1306
+ column: 0,
1307
+ category: "AI Slop",
1308
+ fixable
1309
+ });
1301
1310
  const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
1302
1311
  const SCRIPT_DIR_PATTERN = /(?:^|\/)(scripts|bin)\//;
1303
1312
  const detectConsoleLeftovers = (content, relativePath, ext) => {
@@ -1312,18 +1321,7 @@ const detectConsoleLeftovers = (content, relativePath, ext) => {
1312
1321
  if (CONSOLE_LOG_PATTERN.test(trimmed)) {
1313
1322
  if (/console\.(?:error|warn)\s*\(/.test(trimmed)) continue;
1314
1323
  if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) continue;
1315
- diagnostics.push({
1316
- filePath: relativePath,
1317
- engine: "ai-slop",
1318
- rule: "ai-slop/console-leftover",
1319
- severity: "warning",
1320
- message: "console.log/debug/info statement left in production code",
1321
- help: "Remove debugging console statements or replace with a proper logger",
1322
- line: i + 1,
1323
- column: 0,
1324
- category: "AI Slop",
1325
- fixable: true
1326
- });
1324
+ diagnostics.push(slop(relativePath, i + 1, "ai-slop/console-leftover", "warning", "console.log/debug/info statement left in production code", "Remove debugging console statements or replace with a proper logger", true));
1327
1325
  }
1328
1326
  }
1329
1327
  return diagnostics;
@@ -1344,18 +1342,7 @@ const detectTodoStubs = (content, relativePath) => {
1344
1342
  for (let i = 0; i < lines.length; i++) {
1345
1343
  const trimmed = lines[i].trim();
1346
1344
  if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
1347
- if (TODO_PATTERN.test(trimmed)) diagnostics.push({
1348
- filePath: relativePath,
1349
- engine: "ai-slop",
1350
- rule: "ai-slop/todo-stub",
1351
- severity: "info",
1352
- message: "Unresolved TODO/FIXME/HACK comment indicates incomplete code",
1353
- help: "Resolve the TODO or create a tracked issue for it",
1354
- line: i + 1,
1355
- column: 0,
1356
- category: "AI Slop",
1357
- fixable: false
1358
- });
1345
+ if (TODO_PATTERN.test(trimmed)) diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
1359
1346
  }
1360
1347
  return diagnostics;
1361
1348
  };
@@ -1365,42 +1352,9 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
1365
1352
  for (let i = 0; i < lines.length; i++) {
1366
1353
  const trimmed = lines[i].trim();
1367
1354
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
1368
- 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({
1369
- filePath: relativePath,
1370
- engine: "ai-slop",
1371
- rule: "ai-slop/unreachable-code",
1372
- severity: "warning",
1373
- message: "Code after return/throw statement is unreachable",
1374
- help: "Remove the unreachable code or restructure the control flow",
1375
- line: i + 2,
1376
- column: 0,
1377
- category: "AI Slop",
1378
- fixable: false
1379
- });
1380
- 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({
1381
- filePath: relativePath,
1382
- engine: "ai-slop",
1383
- rule: "ai-slop/constant-condition",
1384
- severity: "warning",
1385
- message: "Conditional with a constant value — likely debugging leftover",
1386
- help: "Remove the constant condition or replace with proper logic",
1387
- line: i + 1,
1388
- column: 0,
1389
- category: "AI Slop",
1390
- fixable: false
1391
- });
1392
- if (JS_EXTENSIONS$1.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push({
1393
- filePath: relativePath,
1394
- engine: "ai-slop",
1395
- rule: "ai-slop/empty-function",
1396
- severity: "info",
1397
- message: "Empty function body — possible stub or unfinished implementation",
1398
- help: "Implement the function body or add a comment explaining why it's empty",
1399
- line: i + 1,
1400
- column: 0,
1401
- category: "AI Slop",
1402
- fixable: false
1403
- });
1355
+ 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));
1356
+ 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));
1357
+ 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));
1404
1358
  }
1405
1359
  return diagnostics;
1406
1360
  };
@@ -1412,46 +1366,13 @@ const detectUnsafeTypePatterns = (content, relativePath, ext) => {
1412
1366
  const lines = content.split("\n");
1413
1367
  for (let i = 0; i < lines.length; i++) {
1414
1368
  const trimmed = lines[i].trim();
1415
- if (/\/\/\s*@ts-(?:ignore|expect-error)/.test(trimmed) || /\/\*\s*@ts-(?:ignore|expect-error)/.test(trimmed)) diagnostics.push({
1416
- filePath: relativePath,
1417
- engine: "ai-slop",
1418
- rule: "ai-slop/ts-directive",
1419
- severity: "info",
1420
- message: "@ts-ignore/@ts-expect-error suppresses type checking — review if still needed",
1421
- help: "Fix the underlying type issue instead of suppressing the error",
1422
- line: i + 1,
1423
- column: 0,
1424
- category: "AI Slop",
1425
- fixable: false
1426
- });
1369
+ if (/\/\/\s*@ts-(?:ignore|expect-error)/.test(trimmed) || /\/\*\s*@ts-(?:ignore|expect-error)/.test(trimmed)) diagnostics.push(slop(relativePath, i + 1, "ai-slop/ts-directive", "info", "@ts-ignore/@ts-expect-error suppresses type checking — review if still needed", "Fix the underlying type issue instead of suppressing the error", false));
1427
1370
  if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1428
1371
  if (/\bRegExp\b|new\s+RegExp|\/.*\\b/.test(trimmed)) continue;
1429
1372
  if (/["'`].*\\b.*["'`]/.test(trimmed)) continue;
1430
- if (asAnyPattern.test(trimmed)) diagnostics.push({
1431
- filePath: relativePath,
1432
- engine: "ai-slop",
1433
- rule: "ai-slop/unsafe-type-assertion",
1434
- severity: "warning",
1435
- message: `'as any' bypasses type safety`,
1436
- help: "Use a proper type or a more specific assertion",
1437
- line: i + 1,
1438
- column: 0,
1439
- category: "AI Slop",
1440
- fixable: false
1441
- });
1373
+ if (asAnyPattern.test(trimmed)) diagnostics.push(slop(relativePath, i + 1, "ai-slop/unsafe-type-assertion", "warning", `'as any' bypasses type safety`, "Use a proper type or a more specific assertion", false));
1442
1374
  if (doubleAssertPattern.test(trimmed)) {
1443
- if (!(/\.query[(<]/.test(trimmed) || /result\[0\]/.test(trimmed) || /rows\s/.test(trimmed))) diagnostics.push({
1444
- filePath: relativePath,
1445
- engine: "ai-slop",
1446
- rule: "ai-slop/double-type-assertion",
1447
- severity: "warning",
1448
- message: `Double type assertion (as unknown as X) bypasses type checking`,
1449
- help: "Refactor to avoid needing a double assertion. If this is an ORM query return, consider a typed wrapper function",
1450
- line: i + 1,
1451
- column: 0,
1452
- category: "AI Slop",
1453
- fixable: false
1454
- });
1375
+ if (!(/\.query[(<]/.test(trimmed) || /result\[0\]/.test(trimmed) || /rows\s/.test(trimmed))) diagnostics.push(slop(relativePath, i + 1, "ai-slop/double-type-assertion", "warning", `Double type assertion (as unknown as X) bypasses type checking`, "Refactor to avoid needing a double assertion. If this is an ORM query return, consider a typed wrapper function", false));
1455
1376
  }
1456
1377
  }
1457
1378
  return diagnostics;
@@ -1587,6 +1508,7 @@ const CROSS_REFERENCE_PHRASES = [
1587
1508
  ];
1588
1509
  const JUSTIFICATION_OPENERS = [/^(The idea here|The trick is|This was needed|Originally,?)/i];
1589
1510
  const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
1511
+ const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:)\b/i;
1590
1512
  const MEANINGFUL_JSDOC_TAGS = new Set([
1591
1513
  "deprecated",
1592
1514
  "see",
@@ -1797,6 +1719,23 @@ const looksLikeLicenseHeader = (block) => {
1797
1719
  const text = block.rawLines.join(" ").toLowerCase();
1798
1720
  return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
1799
1721
  };
1722
+ const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
1723
+ const isBareSectionLabel = (prose) => {
1724
+ if (!BARE_LABEL_RE.test(prose)) return false;
1725
+ if (prose.endsWith(".")) return false;
1726
+ if (prose.split(/\s+/).length > 3) return false;
1727
+ return true;
1728
+ };
1729
+ const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
1730
+ const nextLineLooksLikeDataEntry = (nextLine) => {
1731
+ if (nextLine === null) return false;
1732
+ if (!DATA_ENTRY_START.test(nextLine)) return false;
1733
+ const trimmed = nextLine.trim();
1734
+ if (trimmed.startsWith("case ")) return true;
1735
+ if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
1736
+ if (/^\w+\s*:/.test(trimmed)) return true;
1737
+ return false;
1738
+ };
1800
1739
  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));
1801
1740
  const detectNarrativeInBlock = (block, ext) => {
1802
1741
  if (looksLikeLicenseHeader(block)) return {
@@ -1819,6 +1758,10 @@ const detectNarrativeInBlock = (block, ext) => {
1819
1758
  matched: true,
1820
1759
  reason: "phase/section header"
1821
1760
  };
1761
+ if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine)) return {
1762
+ matched: true,
1763
+ reason: "bare section label"
1764
+ };
1822
1765
  if (block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
1823
1766
  matched: true,
1824
1767
  reason: block.kind === "jsdoc" ? "JSDoc preamble before declaration" : "multi-line preamble before declaration"
@@ -1838,10 +1781,17 @@ const detectNarrativeInBlock = (block, ext) => {
1838
1781
  matched: true,
1839
1782
  reason: "explanatory preamble"
1840
1783
  };
1841
- if (block.prose.filter((l) => l.length > 0).length >= 5) return {
1784
+ const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
1785
+ const joinedProse = block.prose.join(" ");
1786
+ const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joinedProse);
1787
+ if (nonEmptyProseCount >= 5) return {
1842
1788
  matched: true,
1843
1789
  reason: "long narrative block"
1844
1790
  };
1791
+ if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line") return {
1792
+ matched: true,
1793
+ reason: "multi-line narrative prose"
1794
+ };
1845
1795
  return {
1846
1796
  matched: false,
1847
1797
  reason: ""
@@ -2601,6 +2551,130 @@ const checkComplexity = async (context) => {
2601
2551
  return diagnostics;
2602
2552
  };
2603
2553
 
2554
+ //#endregion
2555
+ //#region src/engines/code-quality/duplicate-block.ts
2556
+ const WINDOW_SIZE = 10;
2557
+ const MIN_DISTINCT_LINES = 7;
2558
+ const SOURCE_EXTS$1 = new Set([
2559
+ ".ts",
2560
+ ".tsx",
2561
+ ".js",
2562
+ ".jsx",
2563
+ ".mjs",
2564
+ ".cjs"
2565
+ ]);
2566
+ const MEANINGFUL_LINE = /\S/;
2567
+ const normaliseLine = (line) => line.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, "\"L\"").replace(/\b\d+(?:\.\d+)?\b/g, "0").replace(/\s+/g, " ").trim();
2568
+ const isTrivialLine = (line) => {
2569
+ const trimmed = line.trim();
2570
+ if (trimmed.length === 0) return true;
2571
+ if (trimmed === "{" || trimmed === "}" || trimmed === "});" || trimmed === "},") return true;
2572
+ if (trimmed.startsWith("//")) return true;
2573
+ if (trimmed.startsWith("/*") || trimmed.startsWith("*")) return true;
2574
+ return false;
2575
+ };
2576
+ const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
2577
+ const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
2578
+ const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
2579
+ const findSuppressedLines = (lines) => {
2580
+ const suppressed = /* @__PURE__ */ new Set();
2581
+ for (let i = 0; i < lines.length; i++) {
2582
+ if (!SUPPRESS_RE.test(lines[i])) continue;
2583
+ let depth = 0;
2584
+ let started = false;
2585
+ for (let j = i + 1; j < lines.length; j++) {
2586
+ suppressed.add(j + 1);
2587
+ const opens = (lines[j].match(/\{/g) ?? []).length;
2588
+ const closes = (lines[j].match(/\}/g) ?? []).length;
2589
+ depth += opens - closes;
2590
+ if (opens > 0) started = true;
2591
+ if (started && depth <= 0) break;
2592
+ }
2593
+ }
2594
+ return suppressed;
2595
+ };
2596
+ const collectMeaningfulLines = (content) => {
2597
+ if (fileHasSuppression(content)) return [];
2598
+ const lines = content.split("\n");
2599
+ const suppressed = findSuppressedLines(lines);
2600
+ const hits = [];
2601
+ for (let i = 0; i < lines.length - WINDOW_SIZE + 1; i++) {
2602
+ if (suppressed.has(i + 1)) continue;
2603
+ const window = lines.slice(i, i + WINDOW_SIZE);
2604
+ if (window.some((l) => !MEANINGFUL_LINE.test(l))) continue;
2605
+ if (window.every(isTrivialLine)) continue;
2606
+ const normalised = window.map(normaliseLine);
2607
+ if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
2608
+ if (new Set(normalised).size < MIN_DISTINCT_LINES) continue;
2609
+ hits.push({
2610
+ startLine: i + 1,
2611
+ normalised
2612
+ });
2613
+ }
2614
+ return hits;
2615
+ };
2616
+ const findDuplicateBlocks = (content, relativePath) => {
2617
+ const blocks = collectMeaningfulLines(content);
2618
+ const seen = /* @__PURE__ */ new Map();
2619
+ const reports = [];
2620
+ const reportedCurrent = /* @__PURE__ */ new Set();
2621
+ for (const block of blocks) {
2622
+ const key = block.normalised.join("\n");
2623
+ const prior = seen.get(key);
2624
+ if (prior === void 0) {
2625
+ seen.set(key, block.startLine);
2626
+ continue;
2627
+ }
2628
+ if (block.startLine - prior < WINDOW_SIZE) continue;
2629
+ if (reportedCurrent.has(prior)) continue;
2630
+ const last = reports[reports.length - 1];
2631
+ if (last && block.startLine - last.currentStart < WINDOW_SIZE && prior - last.priorStart < WINDOW_SIZE) {
2632
+ last.priorEnd = Math.max(last.priorEnd, prior + WINDOW_SIZE - 1);
2633
+ last.currentEnd = Math.max(last.currentEnd, block.startLine + WINDOW_SIZE - 1);
2634
+ continue;
2635
+ }
2636
+ reportedCurrent.add(prior);
2637
+ reports.push({
2638
+ priorStart: prior,
2639
+ priorEnd: prior + WINDOW_SIZE - 1,
2640
+ currentStart: block.startLine,
2641
+ currentEnd: block.startLine + WINDOW_SIZE - 1
2642
+ });
2643
+ }
2644
+ return reports.map((r) => {
2645
+ return {
2646
+ filePath: relativePath,
2647
+ engine: "code-quality",
2648
+ rule: "code-quality/duplicate-block",
2649
+ severity: "warning",
2650
+ message: `${r.currentEnd - r.currentStart + 1}-line block at line ${r.currentStart} duplicates a block starting at line ${r.priorStart}. Extract a shared helper.`,
2651
+ help: `Pull the shared logic into a function both sites can call. Keeps one version of the truth and makes future changes one-shot instead of N-shot.`,
2652
+ line: r.currentStart,
2653
+ column: 0,
2654
+ category: "Complexity",
2655
+ fixable: false
2656
+ };
2657
+ });
2658
+ };
2659
+ const detectDuplicateBlocks = async (context) => {
2660
+ const files = getSourceFiles(context);
2661
+ const diagnostics = [];
2662
+ for (const filePath of files) {
2663
+ const ext = path.extname(filePath).toLowerCase();
2664
+ if (!SOURCE_EXTS$1.has(ext)) continue;
2665
+ if (isAutoGenerated(filePath)) continue;
2666
+ let content;
2667
+ try {
2668
+ content = fs.readFileSync(filePath, "utf-8");
2669
+ } catch {
2670
+ continue;
2671
+ }
2672
+ const relative = path.relative(context.rootDirectory, filePath);
2673
+ diagnostics.push(...findDuplicateBlocks(content, relative));
2674
+ }
2675
+ return diagnostics;
2676
+ };
2677
+
2604
2678
  //#endregion
2605
2679
  //#region src/engines/code-quality/knip.ts
2606
2680
  const KNIP_MESSAGE_MAP = {
@@ -2626,6 +2700,10 @@ const getIssueItems = (fileIssue, issueType) => {
2626
2700
  const items = fileIssue[issueType];
2627
2701
  return Array.isArray(items) ? items : [];
2628
2702
  };
2703
+ const shouldIncludeIssue = (issueType, filePath) => {
2704
+ if (issueType !== "binaries") return true;
2705
+ return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
2706
+ };
2629
2707
  const DEPENDENCY_HELP = {
2630
2708
  dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `npx aislop fix`.",
2631
2709
  devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `npx aislop fix`.",
@@ -2635,6 +2713,7 @@ const DEPENDENCY_HELP = {
2635
2713
  };
2636
2714
  const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
2637
2715
  const diagnostics = [];
2716
+ if (!shouldIncludeIssue(issueType, fileIssue.file)) return diagnostics;
2638
2717
  const issues = getIssueItems(fileIssue, issueType);
2639
2718
  const category = isDependencyType(issueType) ? "Dependencies" : "Dead Code";
2640
2719
  const severity = issueType === "unlisted" || issueType === "unresolved" ? "error" : "warning";
@@ -2775,6 +2854,78 @@ const runKnip = async (rootDirectory) => {
2775
2854
  }
2776
2855
  };
2777
2856
 
2857
+ //#endregion
2858
+ //#region src/engines/code-quality/repeated-chained-call.ts
2859
+ const METHOD_CALL_RE = /^\s*\.([A-Za-z_$][\w$]*)\s*\(/;
2860
+ const CHAIN_THRESHOLD = 5;
2861
+ const hasOnlyLiteralDifferences = (lines) => {
2862
+ const normalised = lines.map((l) => l.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, "\"L\"").trim().replace(/;\s*$/, ""));
2863
+ return new Set(normalised).size === 1;
2864
+ };
2865
+ const findRepeatedChains = (content, relativePath) => {
2866
+ const diagnostics = [];
2867
+ const lines = content.split("\n");
2868
+ let i = 0;
2869
+ while (i < lines.length) {
2870
+ const match = lines[i].match(METHOD_CALL_RE);
2871
+ if (!match) {
2872
+ i += 1;
2873
+ continue;
2874
+ }
2875
+ const methodName = match[1];
2876
+ const runStart = i;
2877
+ let runEnd = i;
2878
+ while (runEnd + 1 < lines.length) {
2879
+ const next = lines[runEnd + 1].match(METHOD_CALL_RE);
2880
+ if (!next || next[1] !== methodName) break;
2881
+ runEnd += 1;
2882
+ }
2883
+ const runLength = runEnd - runStart + 1;
2884
+ if (runLength >= CHAIN_THRESHOLD && hasOnlyLiteralDifferences(lines.slice(runStart, runEnd + 1))) {
2885
+ diagnostics.push({
2886
+ filePath: relativePath,
2887
+ engine: "code-quality",
2888
+ rule: "code-quality/repeated-chained-call",
2889
+ severity: "warning",
2890
+ message: `${runLength} consecutive \`.${methodName}()\` calls that differ only in string literals. Extract a data table + loop.`,
2891
+ help: `Move the per-call args into an array and call \`.${methodName}()\` in a \`for\` loop. Keeps the registration in one place and lets you document the table once.`,
2892
+ line: runStart + 1,
2893
+ column: 0,
2894
+ category: "Complexity",
2895
+ fixable: false
2896
+ });
2897
+ i = runEnd + 1;
2898
+ } else i += 1;
2899
+ }
2900
+ return diagnostics;
2901
+ };
2902
+ const SOURCE_EXTS = new Set([
2903
+ ".ts",
2904
+ ".tsx",
2905
+ ".js",
2906
+ ".jsx",
2907
+ ".mjs",
2908
+ ".cjs"
2909
+ ]);
2910
+ const detectRepeatedChainedCalls = async (context) => {
2911
+ const files = getSourceFiles(context);
2912
+ const diagnostics = [];
2913
+ for (const filePath of files) {
2914
+ const ext = path.extname(filePath).toLowerCase();
2915
+ if (!SOURCE_EXTS.has(ext)) continue;
2916
+ if (isAutoGenerated(filePath)) continue;
2917
+ let content;
2918
+ try {
2919
+ content = fs.readFileSync(filePath, "utf-8");
2920
+ } catch {
2921
+ continue;
2922
+ }
2923
+ const relative = path.relative(context.rootDirectory, filePath);
2924
+ diagnostics.push(...findRepeatedChains(content, relative));
2925
+ }
2926
+ return diagnostics;
2927
+ };
2928
+
2778
2929
  //#endregion
2779
2930
  //#region src/engines/code-quality/index.ts
2780
2931
  const codeQualityEngine = {
@@ -2784,6 +2935,8 @@ const codeQualityEngine = {
2784
2935
  const promises = [];
2785
2936
  if (context.languages.includes("typescript") || context.languages.includes("javascript")) promises.push(runKnip(context.rootDirectory));
2786
2937
  promises.push(checkComplexity(context));
2938
+ promises.push(detectRepeatedChainedCalls(context));
2939
+ promises.push(detectDuplicateBlocks(context));
2787
2940
  const results = await Promise.allSettled(promises);
2788
2941
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
2789
2942
  return {
@@ -4032,6 +4185,54 @@ const maskStringsAndComments = (content, ext) => {
4032
4185
  if (family === "js") return maskJs(content);
4033
4186
  return maskSimple(content, family);
4034
4187
  };
4188
+ const handleQuotesAndComments = (content, i, tplStack, mask) => {
4189
+ const len = content.length;
4190
+ const c = content[i];
4191
+ const next = content[i + 1];
4192
+ if (c === "\"" || c === "'") {
4193
+ const strStart = i;
4194
+ const end = consumeQuotedString(content, i, c);
4195
+ mask(strStart + 1, end - 1);
4196
+ return {
4197
+ handled: true,
4198
+ nextI: end
4199
+ };
4200
+ }
4201
+ if (c === "`") {
4202
+ const scan = consumeTemplateString(content, i + 1);
4203
+ mask(i + 1, scan.maskEnd);
4204
+ if (scan.openedInterp) tplStack.push(0);
4205
+ return {
4206
+ handled: true,
4207
+ nextI: scan.resumeAt
4208
+ };
4209
+ }
4210
+ if (c === "/" && next === "/") {
4211
+ const strStart = i;
4212
+ let k = i;
4213
+ while (k < len && content[k] !== "\n") k++;
4214
+ mask(strStart, k);
4215
+ return {
4216
+ handled: true,
4217
+ nextI: k
4218
+ };
4219
+ }
4220
+ if (c === "/" && next === "*") {
4221
+ const strStart = i;
4222
+ let k = i + 2;
4223
+ while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
4224
+ if (k < len - 1) k += 2;
4225
+ mask(strStart, k);
4226
+ return {
4227
+ handled: true,
4228
+ nextI: k
4229
+ };
4230
+ }
4231
+ return {
4232
+ handled: false,
4233
+ nextI: i
4234
+ };
4235
+ };
4035
4236
  const maskJs = (content) => {
4036
4237
  const out = content.split("");
4037
4238
  const len = content.length;
@@ -4042,7 +4243,6 @@ const maskJs = (content) => {
4042
4243
  };
4043
4244
  while (i < len) {
4044
4245
  const c = content[i];
4045
- const next = content[i + 1];
4046
4246
  if (tplStack.length > 0) {
4047
4247
  if (c === "{") {
4048
4248
  tplStack[tplStack.length - 1]++;
@@ -4062,61 +4262,10 @@ const maskJs = (content) => {
4062
4262
  i++;
4063
4263
  continue;
4064
4264
  }
4065
- if (c === "\"" || c === "'") {
4066
- const strStart = i;
4067
- i = consumeQuotedString(content, i, c);
4068
- mask(strStart + 1, i - 1);
4069
- continue;
4070
- }
4071
- if (c === "`") {
4072
- const scan = consumeTemplateString(content, i + 1);
4073
- mask(i + 1, scan.maskEnd);
4074
- if (scan.openedInterp) tplStack.push(0);
4075
- i = scan.resumeAt;
4076
- continue;
4077
- }
4078
- if (c === "/" && next === "/") {
4079
- const strStart = i;
4080
- while (i < len && content[i] !== "\n") i++;
4081
- mask(strStart, i);
4082
- continue;
4083
- }
4084
- if (c === "/" && next === "*") {
4085
- const strStart = i;
4086
- i += 2;
4087
- while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
4088
- if (i < len - 1) i += 2;
4089
- mask(strStart, i);
4090
- continue;
4091
- }
4092
- i++;
4093
- continue;
4094
4265
  }
4095
- if (c === "\"" || c === "'") {
4096
- const strStart = i;
4097
- i = consumeQuotedString(content, i, c);
4098
- mask(strStart + 1, i - 1);
4099
- continue;
4100
- }
4101
- if (c === "`") {
4102
- const scan = consumeTemplateString(content, i + 1);
4103
- mask(i + 1, scan.maskEnd);
4104
- if (scan.openedInterp) tplStack.push(0);
4105
- i = scan.resumeAt;
4106
- continue;
4107
- }
4108
- if (c === "/" && next === "/") {
4109
- const strStart = i;
4110
- while (i < len && content[i] !== "\n") i++;
4111
- mask(strStart, i);
4112
- continue;
4113
- }
4114
- if (c === "/" && next === "*") {
4115
- const strStart = i;
4116
- i += 2;
4117
- while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
4118
- if (i < len - 1) i += 2;
4119
- mask(strStart, i);
4266
+ const handled = handleQuotesAndComments(content, i, tplStack, mask);
4267
+ if (handled.handled) {
4268
+ i = handled.nextI;
4120
4269
  continue;
4121
4270
  }
4122
4271
  i++;
@@ -5111,11 +5260,14 @@ const scanCommand = async (directory, config, options) => {
5111
5260
  const projectInfo = await discoverProject(resolvedDir);
5112
5261
  let files;
5113
5262
  if (options.staged) {
5114
- files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir));
5263
+ files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
5115
5264
  if (!options.json) log.muted(`Scope: ${files.length} staged file(s)`);
5116
5265
  } else if (options.changes) {
5117
- files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir));
5266
+ files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], config.exclude);
5118
5267
  if (!options.json) log.muted(`Scope: ${files.length} changed file(s)`);
5268
+ } else {
5269
+ files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], config.exclude);
5270
+ if (!options.json) log.muted(`Scope: ${files.length} file(s) after exclusions`);
5119
5271
  }
5120
5272
  const configDir = findConfigDir(resolvedDir);
5121
5273
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
@@ -5189,7 +5341,7 @@ const scanCommand = async (directory, config, options) => {
5189
5341
  });
5190
5342
  }
5191
5343
  if (options.json) {
5192
- const { buildJsonOutput } = await import("./json-DcE9soYJ.js");
5344
+ const { buildJsonOutput } = await import("./json-ZItDVIZL.js");
5193
5345
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
5194
5346
  console.log(JSON.stringify(jsonOut, null, 2));
5195
5347
  return { exitCode };