aislop 0.10.2 → 0.11.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/mcp.js CHANGED
@@ -16,10 +16,6 @@ import { fileURLToPath } from "node:url";
16
16
  import os from "node:os";
17
17
  import { randomUUID } from "node:crypto";
18
18
 
19
- //#region src/version.ts
20
- const APP_VERSION = "0.10.2";
21
-
22
- //#endregion
23
19
  //#region src/config/defaults.ts
24
20
  const DEFAULT_CONFIG = {
25
21
  version: 1,
@@ -63,7 +59,8 @@ const DEFAULT_CONFIG = {
63
59
  good: 75,
64
60
  ok: 50
65
61
  },
66
- smoothing: 20
62
+ smoothing: 20,
63
+ maxPerRule: 40
67
64
  },
68
65
  ci: {
69
66
  failBelow: 70,
@@ -72,22 +69,6 @@ const DEFAULT_CONFIG = {
72
69
  telemetry: { enabled: true },
73
70
  rules: {}
74
71
  };
75
- const DEFAULT_GITHUB_WORKFLOW_YAML = `name: aislop
76
-
77
- on:
78
- push:
79
- branches: [main]
80
- pull_request:
81
-
82
- jobs:
83
- quality-gate:
84
- runs-on: ubuntu-latest
85
- steps:
86
- - uses: actions/checkout@v4
87
- - uses: scanaislop/aislop@v${APP_VERSION}
88
- with:
89
- version: ${APP_VERSION}
90
- `;
91
72
 
92
73
  //#endregion
93
74
  //#region src/config/extends.ts
@@ -170,7 +151,8 @@ const ScoringSchema = z$1.object({
170
151
  good: 75,
171
152
  ok: 50
172
153
  })),
173
- smoothing: z$1.number().nonnegative().default(20)
154
+ smoothing: z$1.number().nonnegative().default(20),
155
+ maxPerRule: z$1.number().positive().default(40)
174
156
  });
175
157
  const CiSchema = z$1.object({
176
158
  failBelow: z$1.number().default(70),
@@ -210,7 +192,8 @@ const AislopConfigSchema = z$1.object({
210
192
  good: 75,
211
193
  ok: 50
212
194
  },
213
- smoothing: 20
195
+ smoothing: 20,
196
+ maxPerRule: 40
214
197
  })),
215
198
  ci: CiSchema.default(() => ({
216
199
  failBelow: 70,
@@ -979,8 +962,8 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
979
962
 
980
963
  //#endregion
981
964
  //#region src/engines/ai-slop/non-production-paths.ts
982
- const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
983
- const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
965
+ const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|prototypes?|experiments?|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
966
+ const BASENAME_PATTERN = /(?:^|\/)(?:(?:prototype|experiment)(?:[-_.][^/]*)?|(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*)\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example|prototype|experiment)\.[mc]?[jt]sx?$/i;
984
967
  const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
985
968
 
986
969
  //#endregion
@@ -1096,7 +1079,7 @@ const JS_EXTENSIONS$3 = new Set([
1096
1079
  ".mjs",
1097
1080
  ".cjs"
1098
1081
  ]);
1099
- const CONSOLE_LOG_PATTERN = /\bconsole\.(?:log|debug|info|trace|dir|table)\s*\(/;
1082
+ const CONSOLE_CALL_PATTERN = /\bconsole\.(log|debug|info|trace|dir|table)\s*\(/;
1100
1083
  const slop = (filePath, line, rule, severity, message, help, fixable) => ({
1101
1084
  filePath,
1102
1085
  engine: "ai-slop",
@@ -1110,20 +1093,35 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
1110
1093
  fixable
1111
1094
  });
1112
1095
  const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
1096
+ const CLI_ENTRYPOINT_PATTERN = /(?:^|\/)(?:cli|cli[-_.][^/]*|[^/]+[-_]cli)\.[mc]?[jt]sx?$/i;
1097
+ const ENTRYPOINT_GUARD_PATTERN = /\b(?:import\.meta\.main|require\.main\s*===\s*module)\b/;
1098
+ const OPERATIONAL_LOG_PATTERN = /\bconsole\.(?:log|info)\s*\(\s*(?:`|["'])\s*\[[^\]\n]{1,48}\]/;
1099
+ const DEBUG_SIGNAL_PATTERN = /\b(?:debug|dbg|trace|dump|inspect|todo|tmp|temp|remove\s+me|leftover|here|checkpoint)\b/i;
1100
+ const shouldFlagConsoleCall = (trimmed) => {
1101
+ const match = CONSOLE_CALL_PATTERN.exec(trimmed);
1102
+ if (!match) return false;
1103
+ const method = match[1];
1104
+ if (method === "trace" || method === "dir" || method === "table") return true;
1105
+ if (method === "debug") return DEBUG_SIGNAL_PATTERN.test(trimmed) || !OPERATIONAL_LOG_PATTERN.test(trimmed);
1106
+ if (method === "info" || method === "log") {
1107
+ if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) return false;
1108
+ if (OPERATIONAL_LOG_PATTERN.test(trimmed)) return false;
1109
+ return true;
1110
+ }
1111
+ return false;
1112
+ };
1113
1113
  const detectConsoleLeftovers = (content, relativePath, ext) => {
1114
1114
  if (!JS_EXTENSIONS$3.has(ext)) return [];
1115
1115
  if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
1116
- if (isNonProductionPath(relativePath)) return [];
1116
+ if (isNonProductionPath(relativePath) || CLI_ENTRYPOINT_PATTERN.test(relativePath)) return [];
1117
+ if (content.startsWith("#!")) return [];
1118
+ if (ENTRYPOINT_GUARD_PATTERN.test(content)) return [];
1117
1119
  const diagnostics = [];
1118
1120
  const lines = content.split("\n");
1119
1121
  for (let i = 0; i < lines.length; i++) {
1120
1122
  const trimmed = lines[i].trim();
1121
1123
  if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
1122
- if (CONSOLE_LOG_PATTERN.test(trimmed)) {
1123
- if (/console\.(?:error|warn)\s*\(/.test(trimmed)) continue;
1124
- if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) continue;
1125
- 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));
1126
- }
1124
+ if (shouldFlagConsoleCall(trimmed)) 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));
1127
1125
  }
1128
1126
  return diagnostics;
1129
1127
  };
@@ -1151,6 +1149,7 @@ const isGuardedSingleLineExit = (lines, lineIndex) => {
1151
1149
  const control = contextLines.join(" ");
1152
1150
  return /(?:^|[}\s])(?:if|else\s+if|for|while)\s*\(/.test(control) && !/{\s*$/.test(control);
1153
1151
  };
1152
+ const isPropertyNoopAssignment = (trimmed) => /^(?:[\w$]+\.)+[\w$]+\s*=\s*(?:function\s*)?\([^)]*\)\s*(?:=>)?\s*\{\s*\}\s*;?$/.test(trimmed);
1154
1153
  const detectTodoStubs = (content, relativePath) => {
1155
1154
  const diagnostics = [];
1156
1155
  const lines = content.split("\n");
@@ -1172,7 +1171,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
1172
1171
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
1173
1172
  if (JS_EXTENSIONS$3.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !isGuardedSingleLineExit(lines, i) && !isBlockCloserAfterReturn(nextLine) && !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));
1174
1173
  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));
1175
- if (JS_EXTENSIONS$3.has(ext) && /(?:function\s+\w+\s*\([^)]*\)|=>\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));
1174
+ if (JS_EXTENSIONS$3.has(ext) && /(?:function\s+\w+\s*\([^)]*\)|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ") && !isPropertyNoopAssignment(trimmed)) 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));
1176
1175
  }
1177
1176
  return diagnostics;
1178
1177
  };
@@ -1670,7 +1669,7 @@ const JS_RESOLUTION_EXTENSIONS = [
1670
1669
  "/index.js",
1671
1670
  "/index.jsx"
1672
1671
  ];
1673
- const readJson$2 = (filePath) => {
1672
+ const readJson$3 = (filePath) => {
1674
1673
  try {
1675
1674
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1676
1675
  } catch {
@@ -1685,7 +1684,7 @@ const buildAliasMatcher = (key) => {
1685
1684
  return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
1686
1685
  };
1687
1686
  const collectAliasMatchersFromConfig = (configPath, matchers) => {
1688
- const opts = readJson$2(configPath)?.compilerOptions;
1687
+ const opts = readJson$3(configPath)?.compilerOptions;
1689
1688
  if (!opts || typeof opts !== "object") return;
1690
1689
  const configDir = path.dirname(configPath);
1691
1690
  const paths = opts.paths;
@@ -1708,7 +1707,7 @@ const collectTsPathAliases = (rootDir, workspaceDirs) => {
1708
1707
 
1709
1708
  //#endregion
1710
1709
  //#region src/engines/ai-slop/js-workspaces.ts
1711
- const readJson$1 = (filePath) => {
1710
+ const readJson$2 = (filePath) => {
1712
1711
  try {
1713
1712
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
1714
1713
  } catch {
@@ -1728,7 +1727,7 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
1728
1727
  }
1729
1728
  }
1730
1729
  }
1731
- const lerna = readJson$1(path.join(rootDir, "lerna.json"));
1730
+ const lerna = readJson$2(path.join(rootDir, "lerna.json"));
1732
1731
  if (lerna && Array.isArray(lerna.packages)) {
1733
1732
  for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
1734
1733
  }
@@ -2129,7 +2128,7 @@ const JS_EXTENSIONS$1 = new Set([
2129
2128
  ".cjs"
2130
2129
  ]);
2131
2130
  const PY_EXTENSIONS$2 = new Set([".py"]);
2132
- const readJson = (filePath) => {
2131
+ const readJson$1 = (filePath) => {
2133
2132
  try {
2134
2133
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
2135
2134
  } catch {
@@ -2173,7 +2172,7 @@ const collectNestedManifests = (rootDir, jsDeps) => {
2173
2172
  const full = path.join(dir, entry.name);
2174
2173
  if (entry.isDirectory()) walk(full, depth + 1);
2175
2174
  else if (entry.name === "package.json" && depth > 0) {
2176
- const wsPkg = readJson(full);
2175
+ const wsPkg = readJson$1(full);
2177
2176
  if (!wsPkg) continue;
2178
2177
  if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
2179
2178
  addDepsFromPkg(wsPkg, jsDeps);
@@ -2185,13 +2184,13 @@ const collectNestedManifests = (rootDir, jsDeps) => {
2185
2184
  const collectJsDeps = (rootDir, jsDeps) => {
2186
2185
  const pkgPath = path.join(rootDir, "package.json");
2187
2186
  if (!fs.existsSync(pkgPath)) return false;
2188
- const pkg = readJson(pkgPath);
2187
+ const pkg = readJson$1(pkgPath);
2189
2188
  if (!pkg || typeof pkg !== "object") return false;
2190
2189
  addDepsFromPkg(pkg, jsDeps);
2191
2190
  if (typeof pkg.name === "string") jsDeps.add(pkg.name);
2192
2191
  const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
2193
2192
  for (const wsDir of workspaceDirs) {
2194
- const wsPkg = readJson(path.join(wsDir, "package.json"));
2193
+ const wsPkg = readJson$1(path.join(wsDir, "package.json"));
2195
2194
  if (!wsPkg) continue;
2196
2195
  if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
2197
2196
  addDepsFromPkg(wsPkg, jsDeps);
@@ -2220,7 +2219,11 @@ const VIRTUAL_MODULE_PREFIXES = [
2220
2219
  "astro:",
2221
2220
  "virtual:",
2222
2221
  "bun:",
2223
- "file:"
2222
+ "file:",
2223
+ "http:",
2224
+ "https:",
2225
+ "jsr:",
2226
+ "npm:"
2224
2227
  ];
2225
2228
  const isJsVirtualModule = (spec, manifest) => {
2226
2229
  if (VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p))) return true;
@@ -2349,7 +2352,7 @@ const checkPyImport = (spec, manifest) => {
2349
2352
  return root;
2350
2353
  };
2351
2354
  const detectHallucinatedImports = async (context) => {
2352
- const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
2355
+ const rootPkg = readJson$1(path.join(context.rootDirectory, "package.json"));
2353
2356
  const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
2354
2357
  const manifest = loadManifest(context.rootDirectory);
2355
2358
  if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
@@ -2454,6 +2457,9 @@ const VENDOR_API_DOMAINS = [
2454
2457
  ];
2455
2458
  const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
2456
2459
  const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
2460
+ const PROVIDER_ID_RE = /^(?:price|prod|cus|sub|acct|org|app|tenant|workspace|project|client|key|tok|token|sk|pk)_[A-Za-z0-9][A-Za-z0-9_-]{7,}$/i;
2461
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2462
+ const READABLE_KEY_RE = /^[a-z][a-z0-9]*(?:[_-][a-z0-9]+){2,}$/;
2457
2463
  const HARDCODED_URL_FINDING = {
2458
2464
  rule: "ai-slop/hardcoded-url",
2459
2465
  message: "Hardcoded environment URL in production code",
@@ -2491,8 +2497,10 @@ const safeUrlHost = (urlText) => {
2491
2497
  }
2492
2498
  };
2493
2499
  const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
2500
+ const TEMPLATE_INTERPOLATION_START = "${";
2494
2501
  const shouldFlagUrlLiteral = (line, urlText) => {
2495
2502
  if (isEnvBackedLine(line)) return false;
2503
+ if (urlText.includes(TEMPLATE_INTERPOLATION_START) && /\bnew\s+URL\s*\(/.test(line)) return false;
2496
2504
  const host = safeUrlHost(urlText);
2497
2505
  if (!host) return false;
2498
2506
  if (PLACEHOLDER_HOSTS.has(host)) return false;
@@ -2507,7 +2515,11 @@ const hasUsefulIdShape = (value) => {
2507
2515
  if (ENV_VAR_NAME_RE.test(value)) return false;
2508
2516
  if (/^https?:\/\//i.test(value)) return false;
2509
2517
  if (/^[A-Za-z]+$/.test(value)) return false;
2510
- return /[0-9]/.test(value);
2518
+ if (READABLE_KEY_RE.test(value) && !PROVIDER_ID_RE.test(value)) return false;
2519
+ if (PROVIDER_ID_RE.test(value)) return true;
2520
+ if (UUID_RE.test(value)) return true;
2521
+ if (!/[0-9]/.test(value)) return false;
2522
+ return value.length >= 24 && !/[_-]/.test(value) && /[a-z]/.test(value) && /[A-Z]/.test(value);
2511
2523
  };
2512
2524
  const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
2513
2525
  const diagnostics = [];
@@ -4458,8 +4470,8 @@ const shouldIncludeIssue = (issueType, filePath) => {
4458
4470
  return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
4459
4471
  };
4460
4472
  const DEPENDENCY_HELP = {
4461
- dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `npx aislop fix`.",
4462
- devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `npx aislop fix`.",
4473
+ dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
4474
+ devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
4463
4475
  unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
4464
4476
  unresolved: "This import cannot be resolved. Check for typos or missing packages.",
4465
4477
  binaries: "This binary is used but its package is not in package.json."
@@ -4766,7 +4778,7 @@ const parseBiomeJsonOutput = (output, rootDir) => {
4766
4778
  rule: "formatting",
4767
4779
  severity,
4768
4780
  message,
4769
- help: "Run `npx aislop fix` to auto-format",
4781
+ help: "Run `aislop fix` to auto-format",
4770
4782
  line: entry.location?.start?.line ?? 0,
4771
4783
  column: entry.location?.start?.column ?? 0,
4772
4784
  category: "Format",
@@ -4795,7 +4807,7 @@ const FORMATTERS = {
4795
4807
  rule: "rust-formatting",
4796
4808
  severity: "warning",
4797
4809
  message: "Rust file is not formatted correctly",
4798
- help: "Run `npx aislop fix` to auto-format with rustfmt",
4810
+ help: "Run `aislop fix` to auto-format with rustfmt",
4799
4811
  line: parseInt(match[2], 10),
4800
4812
  column: 0,
4801
4813
  category: "Format",
@@ -4828,7 +4840,7 @@ const FORMATTERS = {
4828
4840
  rule: offense.cop_name ?? "ruby-formatting",
4829
4841
  severity: "warning",
4830
4842
  message: offense.message ?? "Ruby formatting issue",
4831
- help: "Run `npx aislop fix` to auto-format",
4843
+ help: "Run `aislop fix` to auto-format",
4832
4844
  line: offense.location?.start_line ?? 0,
4833
4845
  column: offense.location?.start_column ?? 0,
4834
4846
  category: "Format",
@@ -4859,7 +4871,7 @@ const FORMATTERS = {
4859
4871
  rule: "php-formatting",
4860
4872
  severity: "warning",
4861
4873
  message: "PHP file is not formatted correctly",
4862
- help: "Run `npx aislop fix` to auto-format",
4874
+ help: "Run `aislop fix` to auto-format",
4863
4875
  line: 0,
4864
4876
  column: 0,
4865
4877
  category: "Format",
@@ -4903,7 +4915,7 @@ const runGofmt = async (context) => {
4903
4915
  rule: "go-formatting",
4904
4916
  severity: "warning",
4905
4917
  message: "Go file is not formatted correctly",
4906
- help: "Run `npx aislop fix` to auto-format with gofmt",
4918
+ help: "Run `aislop fix` to auto-format with gofmt",
4907
4919
  line: 0,
4908
4920
  column: 0,
4909
4921
  category: "Format",
@@ -4995,7 +5007,7 @@ const parseRuffFormatOutput = (output, rootDir) => {
4995
5007
  rule: "python-formatting",
4996
5008
  severity: "warning",
4997
5009
  message: "Python file is not formatted correctly",
4998
- help: "Run `npx aislop fix` to auto-format with ruff",
5010
+ help: "Run `aislop fix` to auto-format with ruff",
4999
5011
  line: 0,
5000
5012
  column: 0,
5001
5013
  category: "Format",
@@ -5273,6 +5285,7 @@ const AMBIENT_GLOBAL_DEPS = [
5273
5285
  const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
5274
5286
  const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
5275
5287
  const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
5288
+ const SUPABASE_FUNCTION_PATH_RE = /(?:^|\/)supabase\/functions\/[^/]+\/.+\.[cm]?[jt]sx?$/;
5276
5289
  const detectAmbientSources = (rootDir) => {
5277
5290
  const found = /* @__PURE__ */ new Set();
5278
5291
  const skipDirs = new Set([
@@ -5317,6 +5330,36 @@ const detectAmbientSources = (rootDir) => {
5317
5330
  const extractNoUndefIdentifier = (message) => {
5318
5331
  return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
5319
5332
  };
5333
+ const looksLikeChromeExtensionManifest = (filePath) => {
5334
+ try {
5335
+ const manifest = JSON.parse(fs.readFileSync(filePath, "utf-8"));
5336
+ return typeof manifest.manifest_version === "number" && ("background" in manifest || "content_scripts" in manifest || "permissions" in manifest);
5337
+ } catch {
5338
+ return false;
5339
+ }
5340
+ };
5341
+ const chromeExtensionFileCache = /* @__PURE__ */ new Map();
5342
+ const isChromeExtensionFile = (rootDir, relativeFilePath) => {
5343
+ const cacheKey = `${rootDir}:${relativeFilePath.split(path.sep).join("/")}`;
5344
+ const cached = chromeExtensionFileCache.get(cacheKey);
5345
+ if (cached !== void 0) return cached;
5346
+ const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
5347
+ const root = path.resolve(rootDir);
5348
+ let dir = path.dirname(path.resolve(absolute));
5349
+ let matched = false;
5350
+ while (true) {
5351
+ const relativeToRoot = path.relative(root, dir);
5352
+ if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) break;
5353
+ if (looksLikeChromeExtensionManifest(path.join(dir, "manifest.json"))) {
5354
+ matched = true;
5355
+ break;
5356
+ }
5357
+ if (dir === root) break;
5358
+ dir = path.dirname(dir);
5359
+ }
5360
+ chromeExtensionFileCache.set(cacheKey, matched);
5361
+ return matched;
5362
+ };
5320
5363
  const isAmbientFalsePositive = (rule, message, sources) => {
5321
5364
  if (rule !== "eslint/no-undef") return false;
5322
5365
  const ident = extractNoUndefIdentifier(message);
@@ -5325,9 +5368,19 @@ const isAmbientFalsePositive = (rule, message, sources) => {
5325
5368
  if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
5326
5369
  return false;
5327
5370
  };
5371
+ const isRuntimeGlobalFalsePositive = (rule, message, rootDir, relativeFilePath) => {
5372
+ if (rule !== "eslint/no-undef") return false;
5373
+ const ident = extractNoUndefIdentifier(message);
5374
+ if (!ident) return false;
5375
+ const normalized = relativeFilePath.split(path.sep).join("/");
5376
+ if (ident === "Deno" && SUPABASE_FUNCTION_PATH_RE.test(normalized)) return true;
5377
+ if (ident === "chrome" && isChromeExtensionFile(rootDir, relativeFilePath)) return true;
5378
+ return false;
5379
+ };
5328
5380
  const sstReferencedFiles = /* @__PURE__ */ new Map();
5329
5381
  const clearSstReferenceCache = () => {
5330
5382
  sstReferencedFiles.clear();
5383
+ chromeExtensionFileCache.clear();
5331
5384
  };
5332
5385
  const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
5333
5386
  const cached = sstReferencedFiles.get(relativeFilePath);
@@ -5379,6 +5432,32 @@ const collectPackageNames = (dir) => {
5379
5432
  }
5380
5433
  return names;
5381
5434
  };
5435
+ const readJson = (filePath) => {
5436
+ const raw = readTextFile$1(filePath);
5437
+ if (!raw) return null;
5438
+ try {
5439
+ const parsed = JSON.parse(raw);
5440
+ return parsed && typeof parsed === "object" ? parsed : null;
5441
+ } catch {
5442
+ return null;
5443
+ }
5444
+ };
5445
+ const hasBunRuntime = (rootDir, projectFiles) => {
5446
+ if (fs.existsSync(path.join(rootDir, "bun.lock")) || fs.existsSync(path.join(rootDir, "bun.lockb")) || fs.existsSync(path.join(rootDir, "bunfig.toml"))) return true;
5447
+ const hasBunFiles = projectFiles.some((filePath) => /(?:^|\/)bunfig\.toml$|(?:^|\/)bun\.lockb?$/.test(filePath));
5448
+ const pkg = readJson(path.join(rootDir, "package.json"));
5449
+ if (!pkg) return hasBunFiles;
5450
+ if (typeof pkg.packageManager === "string" && /^bun@/i.test(pkg.packageManager)) return true;
5451
+ const scripts = pkg.scripts;
5452
+ if (scripts && typeof scripts === "object") {
5453
+ for (const command of Object.values(scripts)) if (typeof command === "string" && /(?:^|[;&|()\s])bunx?\s/.test(command)) return true;
5454
+ }
5455
+ return hasBunFiles;
5456
+ };
5457
+ const hasDenoRuntime = (rootDir, projectFiles) => {
5458
+ if (fs.existsSync(path.join(rootDir, "deno.json")) || fs.existsSync(path.join(rootDir, "deno.jsonc"))) return true;
5459
+ return projectFiles.some((filePath) => /(?:^|\/)deno\.jsonc?$/.test(filePath));
5460
+ };
5382
5461
  const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
5383
5462
  const collectAmbientGlobals = (rootDir) => {
5384
5463
  const globals = /* @__PURE__ */ new Set();
@@ -5390,7 +5469,8 @@ const collectAmbientGlobals = (rootDir) => {
5390
5469
  for (const match of content.matchAll(AMBIENT_GLOBAL_RE)) globals.add(match[1]);
5391
5470
  }
5392
5471
  const deps = collectPackageNames(rootDir);
5393
- if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
5472
+ if (deps.has("@types/bun") || deps.has("bun-types") || hasBunRuntime(rootDir, projectFiles)) globals.add("Bun");
5473
+ if (hasDenoRuntime(rootDir, projectFiles)) globals.add("Deno");
5394
5474
  if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
5395
5475
  "$app",
5396
5476
  "$config",
@@ -5488,6 +5568,37 @@ const detectTestFramework = (rootDir) => {
5488
5568
  return null;
5489
5569
  };
5490
5570
  const getOxlintTargets = (context) => getSourceFiles(context).filter((filePath) => OXLINT_EXTENSIONS.has(path.extname(filePath).toLowerCase())).filter((filePath) => !isAutoGenerated(filePath)).map((filePath) => path.relative(context.rootDirectory, filePath).split(path.sep).join("/"));
5571
+ const toDiagnostic = (d) => {
5572
+ const { plugin, rule } = parseRuleCode(d.code);
5573
+ const label = d.labels[0];
5574
+ return {
5575
+ filePath: d.filename,
5576
+ engine: "lint",
5577
+ rule: `${plugin}/${rule}`,
5578
+ severity: d.severity,
5579
+ message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
5580
+ help: d.help || "",
5581
+ line: label?.span.line ?? 0,
5582
+ column: label?.span.column ?? 0,
5583
+ category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
5584
+ fixable: false
5585
+ };
5586
+ };
5587
+ const shouldKeepOxlintDiagnostic = (context, ambientSources, seen, d) => {
5588
+ const relativePath = path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath;
5589
+ if (isExcludedFromScan(relativePath)) return false;
5590
+ if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
5591
+ if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
5592
+ if (isRuntimeGlobalFalsePositive(d.rule, d.message, context.rootDirectory, relativePath)) return false;
5593
+ if (isSolidRefFalsePositive(context, d)) return false;
5594
+ if (isContextualTypeScriptFalsePositive(d)) return false;
5595
+ if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
5596
+ if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
5597
+ const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
5598
+ if (seen.has(key)) return false;
5599
+ seen.add(key);
5600
+ return true;
5601
+ };
5491
5602
  const runOxlint = async (context) => {
5492
5603
  const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
5493
5604
  const framework = context.frameworks.find((f) => f !== "none");
@@ -5524,34 +5635,7 @@ const runOxlint = async (context) => {
5524
5635
  return [];
5525
5636
  }
5526
5637
  const seen = /* @__PURE__ */ new Set();
5527
- return output.diagnostics.map((d) => {
5528
- const { plugin, rule } = parseRuleCode(d.code);
5529
- const label = d.labels[0];
5530
- return {
5531
- filePath: d.filename,
5532
- engine: "lint",
5533
- rule: `${plugin}/${rule}`,
5534
- severity: d.severity,
5535
- message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
5536
- help: d.help || "",
5537
- line: label?.span.line ?? 0,
5538
- column: label?.span.column ?? 0,
5539
- category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
5540
- fixable: false
5541
- };
5542
- }).filter((d) => {
5543
- if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
5544
- if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
5545
- if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
5546
- if (isSolidRefFalsePositive(context, d)) return false;
5547
- if (isContextualTypeScriptFalsePositive(d)) return false;
5548
- if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
5549
- if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
5550
- const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
5551
- if (seen.has(key)) return false;
5552
- seen.add(key);
5553
- return true;
5554
- });
5638
+ return output.diagnostics.map(toDiagnostic).filter((d) => shouldKeepOxlintDiagnostic(context, ambientSources, seen, d));
5555
5639
  } finally {
5556
5640
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
5557
5641
  }
@@ -5602,7 +5686,7 @@ const lintEngine = {
5602
5686
  promises.push(runOxlint(context));
5603
5687
  if (context.config.lint.typecheck) promises.push(import("./typecheck-By967nny.js").then((mod) => mod.runTypecheck(context)));
5604
5688
  }
5605
- if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-T4DswmX5.js").then((mod) => mod.runExpoDoctor(context)));
5689
+ if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-BM2JR6f6.js").then((mod) => mod.runExpoDoctor(context)));
5606
5690
  if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
5607
5691
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
5608
5692
  if (languages.includes("rust") && installedTools.cargo) promises.push(runGenericLinter(context, "rust"));
@@ -5620,7 +5704,7 @@ const lintEngine = {
5620
5704
 
5621
5705
  //#endregion
5622
5706
  //#region src/ui/invocation.ts
5623
- const detectInvocation = () => "npx aislop";
5707
+ const detectInvocation = () => "aislop";
5624
5708
 
5625
5709
  //#endregion
5626
5710
  //#region src/engines/security/audit.ts
@@ -5772,7 +5856,7 @@ const parseJsAudit = (output, source) => {
5772
5856
  rule: "security/dependency-audit-skipped",
5773
5857
  severity: "info",
5774
5858
  message: `Dependency audit skipped (${source}): lockfile is missing`,
5775
- help: error.detail ?? "Generate a lockfile, then re-run `npx aislop scan` for dependency vulnerability checks.",
5859
+ help: error.detail ?? "Generate a lockfile, then re-run `aislop scan` for dependency vulnerability checks.",
5776
5860
  line: 0,
5777
5861
  column: 0,
5778
5862
  category: "Security",
@@ -5889,6 +5973,194 @@ const runCargoAudit = async (rootDir, timeout) => {
5889
5973
  }
5890
5974
  };
5891
5975
 
5976
+ //#endregion
5977
+ //#region src/engines/security/html-safety.ts
5978
+ const SAFE_EMPTY_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:""|''|``)\s*;?/;
5979
+ const SAFE_SANITIZED_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:escapeHtml|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)\s*;?(?:\n|$)/;
5980
+ const SANITIZER_EXPR_RE = /^(?:escapeHtml|escapeHTML|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)$/;
5981
+ const IDENT_RE = /^[A-Za-z_$][\w$]*$/;
5982
+ const STATIC_STRING_RE = /^(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\$])*`)$/;
5983
+ const NUMERICISH_EXPR_RE = /^(?:[-+]?\d+(?:\.\d+)?|[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*(?:\s*\|\|\s*[-+]?\d+(?:\.\d+)?)?)$/;
5984
+ const NUMERICISH_NAME_RE = /(?:^|\.)(?:count|length|size|width|height|top|right|bottom|left|duration|elapsed|timestamp|time|ms|port|pid|attempt|attempts|index|total|x|y)$|(?:count|length|size|width|height|duration|elapsed|timestamp|time|port|pid|attempt|index|total)$/i;
5985
+ const SAFE_FORMAT_CALL_RE = /^(?:format[A-Z]\w*|fmt[A-Z]?\w*)\s*\((.*)\)$/;
5986
+ const consumeQuotedLiteral = (content, startIndex, quote) => {
5987
+ let i = startIndex + 1;
5988
+ while (i < content.length) {
5989
+ const char = content[i];
5990
+ if (char === "\\") {
5991
+ i += 2;
5992
+ continue;
5993
+ }
5994
+ if (char === quote) return { endIndex: i };
5995
+ if (char === "\n") return null;
5996
+ i++;
5997
+ }
5998
+ return null;
5999
+ };
6000
+ const consumeTemplateLiteral = (content, startIndex) => {
6001
+ const openIndex = content.indexOf("`", startIndex);
6002
+ if (openIndex === -1) return null;
6003
+ let i = openIndex + 1;
6004
+ while (i < content.length) {
6005
+ const char = content[i];
6006
+ if (char === "\\") {
6007
+ i += 2;
6008
+ continue;
6009
+ }
6010
+ if (char === "`") return {
6011
+ body: content.slice(openIndex + 1, i),
6012
+ endIndex: i
6013
+ };
6014
+ i++;
6015
+ }
6016
+ return null;
6017
+ };
6018
+ const assignmentTailIsClosed = (content, endIndex) => /^\s*(?:;[^\n]*)?(?:\n|$)/.test(content.slice(endIndex + 1));
6019
+ const assignmentRhsStart = (content, matchIndex) => {
6020
+ const match = /^\.innerHTML\s*=\s*/.exec(content.slice(matchIndex));
6021
+ return match ? matchIndex + match[0].length : null;
6022
+ };
6023
+ const templateExpressions = (templateBody) => [...templateBody.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
6024
+ const staticTernaryRe = /^\s*[^?]+\?\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*:\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*$/;
6025
+ const splitTopLevelTernary = (expr) => {
6026
+ let quote = null;
6027
+ let depth = 0;
6028
+ let question = -1;
6029
+ let colon = -1;
6030
+ for (let i = 0; i < expr.length; i++) {
6031
+ const char = expr[i];
6032
+ if (char === "\\") {
6033
+ i++;
6034
+ continue;
6035
+ }
6036
+ if ((char === "'" || char === "\"" || char === "`") && quote === null) {
6037
+ quote = char;
6038
+ continue;
6039
+ }
6040
+ if (char === quote) {
6041
+ quote = null;
6042
+ continue;
6043
+ }
6044
+ if (quote) continue;
6045
+ if (char === "(" || char === "[" || char === "{") depth++;
6046
+ else if (char === ")" || char === "]" || char === "}") depth = Math.max(0, depth - 1);
6047
+ else if (char === "?" && depth === 0 && question === -1) question = i;
6048
+ else if (char === ":" && depth === 0 && question !== -1) {
6049
+ colon = i;
6050
+ break;
6051
+ }
6052
+ }
6053
+ if (question === -1 || colon === -1) return null;
6054
+ return {
6055
+ whenTrue: expr.slice(question + 1, colon).trim(),
6056
+ whenFalse: expr.slice(colon + 1).trim()
6057
+ };
6058
+ };
6059
+ const isNumericishExpression = (expr) => {
6060
+ const normalized = expr.trim();
6061
+ if (/^(?:Math\.\w+|Number|parseInt|parseFloat)\s*\(/.test(normalized)) return true;
6062
+ if (!NUMERICISH_EXPR_RE.test(normalized)) return false;
6063
+ return /\d/.test(normalized) || NUMERICISH_NAME_RE.test(normalized);
6064
+ };
6065
+ const isSafeTemplateLiteralExpression = (expr, safeNames) => {
6066
+ if (!expr.startsWith("`") || !expr.endsWith("`")) return false;
6067
+ return templateExpressions(expr.slice(1, -1)).every((part) => isSafeHtmlExpression(part, safeNames));
6068
+ };
6069
+ const collectSafeHtmlNames = (content, matchIndex) => {
6070
+ const safeNames = /* @__PURE__ */ new Set();
6071
+ const prefix = content.slice(Math.max(0, matchIndex - 8e3), matchIndex);
6072
+ for (const rawLine of prefix.split("\n")) {
6073
+ const line = rawLine.trim();
6074
+ let match = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
6075
+ if (match) {
6076
+ const [, name, expr] = match;
6077
+ if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
6078
+ else safeNames.delete(name);
6079
+ continue;
6080
+ }
6081
+ match = /^([A-Za-z_$][\w$]*)\s*\+=\s*(.+?)\s*;?$/.exec(line);
6082
+ if (match) {
6083
+ const [, name, expr] = match;
6084
+ if (safeNames.has(name) && isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
6085
+ else safeNames.delete(name);
6086
+ continue;
6087
+ }
6088
+ match = /^([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
6089
+ if (match) {
6090
+ const [, name, expr] = match;
6091
+ if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
6092
+ else safeNames.delete(name);
6093
+ }
6094
+ }
6095
+ return safeNames;
6096
+ };
6097
+ const isSafeHtmlExpression = (expr, safeNames) => {
6098
+ const normalized = expr.trim();
6099
+ if (SANITIZER_EXPR_RE.test(normalized)) return true;
6100
+ if (STATIC_STRING_RE.test(normalized)) return true;
6101
+ if (staticTernaryRe.test(expr)) return true;
6102
+ if (isNumericishExpression(normalized)) return true;
6103
+ if (IDENT_RE.test(normalized) && safeNames.has(normalized)) return true;
6104
+ if (isSafeTemplateLiteralExpression(normalized, safeNames)) return true;
6105
+ const ternary = splitTopLevelTernary(normalized);
6106
+ if (ternary && isSafeHtmlExpression(ternary.whenTrue, safeNames) && isSafeHtmlExpression(ternary.whenFalse, safeNames)) return true;
6107
+ const formatCall = SAFE_FORMAT_CALL_RE.exec(normalized);
6108
+ if (formatCall) return formatCall[1].split(",").map((arg) => arg.trim()).filter((arg) => arg.length > 0).every((arg) => isNumericishExpression(arg) || IDENT_RE.test(arg) && safeNames.has(arg));
6109
+ return false;
6110
+ };
6111
+ const readSingleLineRhs = (content, rhsStart) => {
6112
+ const lineEnd = content.indexOf("\n", rhsStart);
6113
+ const line = content.slice(rhsStart, lineEnd === -1 ? content.length : lineEnd);
6114
+ let quote = null;
6115
+ for (let i = 0; i < line.length; i++) {
6116
+ const char = line[i];
6117
+ if (char === "\\") {
6118
+ i++;
6119
+ continue;
6120
+ }
6121
+ if ((char === "'" || char === "\"" || char === "`") && quote === null) {
6122
+ quote = char;
6123
+ continue;
6124
+ }
6125
+ if (char === quote) {
6126
+ quote = null;
6127
+ continue;
6128
+ }
6129
+ if (char === ";" && quote === null) return line.slice(0, i).trim();
6130
+ }
6131
+ return line.trim();
6132
+ };
6133
+ const isSafeMapJoinHtmlAssignment = (content, rhsStart) => {
6134
+ const head = content.slice(rhsStart);
6135
+ const mapMatch = /^[A-Za-z_$][\w$.]*\.map\(\s*[A-Za-z_$][\w$]*\s*=>\s*`/.exec(head);
6136
+ if (!mapMatch) return false;
6137
+ const template = consumeTemplateLiteral(content, rhsStart + mapMatch[0].length - 1);
6138
+ if (!template) return false;
6139
+ if (!/^\s*\)\.join\(\s*(?:""|'')\s*\)/.test(content.slice(template.endIndex + 1))) return false;
6140
+ const safeNames = collectSafeHtmlNames(content, rhsStart);
6141
+ return templateExpressions(template.body).every((expr) => isSafeHtmlExpression(expr, safeNames));
6142
+ };
6143
+ const isSafeInnerHtmlAssignment = (content, matchIndex) => {
6144
+ const tail = content.slice(matchIndex);
6145
+ if (SAFE_EMPTY_INNER_HTML_RE.test(tail) || SAFE_SANITIZED_INNER_HTML_RE.test(tail)) return true;
6146
+ const rhsStart = assignmentRhsStart(content, matchIndex);
6147
+ if (rhsStart === null) return false;
6148
+ const first = content[rhsStart];
6149
+ const safeNames = collectSafeHtmlNames(content, matchIndex);
6150
+ if (isSafeHtmlExpression(readSingleLineRhs(content, rhsStart), safeNames)) return true;
6151
+ if (isSafeMapJoinHtmlAssignment(content, rhsStart)) return true;
6152
+ if (first === "'" || first === "\"") {
6153
+ const quoted = consumeQuotedLiteral(content, rhsStart, first);
6154
+ return Boolean(quoted && assignmentTailIsClosed(content, quoted.endIndex));
6155
+ }
6156
+ if (first !== "`") return false;
6157
+ const template = consumeTemplateLiteral(content, rhsStart);
6158
+ if (!template || !assignmentTailIsClosed(content, template.endIndex)) return false;
6159
+ const expressions = templateExpressions(template.body);
6160
+ if (expressions.length === 0) return true;
6161
+ return expressions.every((expr) => isSafeHtmlExpression(expr, safeNames));
6162
+ };
6163
+
5892
6164
  //#endregion
5893
6165
  //#region src/engines/security/risky.ts
5894
6166
  const ev = "eval";
@@ -6014,6 +6286,30 @@ const isStructuredDataScript = (content, matchIndex) => {
6014
6286
  const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
6015
6287
  return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
6016
6288
  };
6289
+ const isSafeShellSpawnArray = (content, matchIndex) => /^spawn\s*\(\s*\[/.test(content.slice(matchIndex)) && !/^\s*spawn\s*\(\s*\[\s*["'](?:sh|bash|zsh|cmd|cmd\.exe|powershell|pwsh)["']\s*,\s*["'](?:-c|\/c|\/C)["']/i.test(content.slice(matchIndex)) && !/shell\s*:\s*true\b/.test(content.slice(matchIndex, matchIndex + 500));
6290
+ const PLACEHOLDER_EXPR_RE = /^(?:placeholders?|placeholderList|bindMarkers?|bindingMarkers?|bindPlaceholders?|bindingPlaceholders?|parameterPlaceholders?|sqlPlaceholders?)(?:\.\w+\([^)]*\))?$/i;
6291
+ const SQL_PLACEHOLDER_LITERAL_RE = /["'](?:\?|\$\d+|\$\{[^}]+\})["']/;
6292
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6293
+ const isGeneratedPlaceholderList = (content, matchIndex, placeholderExpr) => {
6294
+ const name = placeholderExpr.match(/^([A-Za-z_$][\w$]*)/)?.[1];
6295
+ if (!name) return false;
6296
+ const prefix = content.slice(Math.max(0, matchIndex - 4e3), matchIndex);
6297
+ const declarationRe = new RegExp(`\\b(?:const|let|var)\\s+${escapeRegExp(name)}\\s*=\\s*([^;\\n]+)`, "g");
6298
+ const declaration = [...prefix.matchAll(declarationRe)].at(-1);
6299
+ if (!declaration) return false;
6300
+ const expr = declaration[1];
6301
+ if (!/\.join\s*\(/.test(expr)) return false;
6302
+ return /\.map\s*\(/.test(expr) && /=>/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr) || /\.fill\s*\(/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr);
6303
+ };
6304
+ const isSafeSqlPlaceholderTemplate = (content, matchIndex) => {
6305
+ const template = consumeTemplateLiteral(content, matchIndex);
6306
+ if (!template) return false;
6307
+ const afterTemplate = content.slice(template.endIndex + 1);
6308
+ if (!(/^\s*,/.test(afterTemplate) || /^\s*\)\s*\.(?:all|get|run|values)\s*\(/.test(afterTemplate))) return false;
6309
+ const expressions = [...template.body.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
6310
+ if (expressions.length === 0) return false;
6311
+ return expressions.every((expr) => PLACEHOLDER_EXPR_RE.test(expr) && isGeneratedPlaceholderList(content, matchIndex, expr));
6312
+ };
6017
6313
  const detectRiskyConstructs = async (context) => {
6018
6314
  const files = getSourceFiles(context);
6019
6315
  const diagnostics = [];
@@ -6038,8 +6334,11 @@ const detectRiskyConstructs = async (context) => {
6038
6334
  const line = content.slice(0, match.index).split("\n").length;
6039
6335
  if (name === "innerhtml") {
6040
6336
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
6337
+ if (isSafeInnerHtmlAssignment(content, match.index)) continue;
6041
6338
  if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
6042
6339
  }
6340
+ if (name === "sql-injection" && isSafeSqlPlaceholderTemplate(content, match.index)) continue;
6341
+ if (name === "shell-injection" && isSafeShellSpawnArray(content, match.index)) continue;
6043
6342
  if (name === "dangerously-set-innerhtml") {
6044
6343
  if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
6045
6344
  if (isStructuredDataScript(content, match.index)) continue;
@@ -6136,7 +6435,28 @@ const PLACEHOLDER_EXACT = new Set([
6136
6435
  "todo",
6137
6436
  "replace_me"
6138
6437
  ]);
6438
+ const PLACEHOLDER_URL_PARTS = new Set([
6439
+ "example",
6440
+ "host",
6441
+ "localhost",
6442
+ "pass",
6443
+ "password",
6444
+ "pw",
6445
+ "user",
6446
+ "username"
6447
+ ]);
6448
+ const isPlaceholderCredentialUrl = (matchedText) => {
6449
+ const credentialMatch = matchedText.match(/^[a-z]+:\/\/([^:@/\s]+):([^@/\s]+)@/i);
6450
+ if (credentialMatch) return PLACEHOLDER_URL_PARTS.has(credentialMatch[1].toLowerCase()) && PLACEHOLDER_URL_PARTS.has(credentialMatch[2].toLowerCase());
6451
+ try {
6452
+ const parsed = new URL(matchedText);
6453
+ return PLACEHOLDER_URL_PARTS.has(parsed.username.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.password.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.hostname.toLowerCase());
6454
+ } catch {
6455
+ return false;
6456
+ }
6457
+ };
6139
6458
  const isPlaceholderValue = (matchedText) => {
6459
+ if (isPlaceholderCredentialUrl(matchedText)) return true;
6140
6460
  if (/env\(/i.test(matchedText)) return true;
6141
6461
  if (matchedText.includes("process.env")) return true;
6142
6462
  if (matchedText.includes("os.environ")) return true;
@@ -6257,23 +6577,36 @@ const STYLE_RULES = new Set([
6257
6577
  "complexity/function-too-long"
6258
6578
  ]);
6259
6579
  const STYLE_WEIGHT = .5;
6580
+ const COMMENT_STYLE_RULE_CAP = 12;
6581
+ const COMMENT_STYLE_RULES = new Set(["ai-slop/trivial-comment", "ai-slop/narrative-comment"]);
6260
6582
  const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
6261
6583
  if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
6262
6584
  const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
6263
6585
  return Math.max(1, filesWithDiagnostics);
6264
6586
  };
6265
- const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing) => {
6587
+ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing, maxPerRule) => {
6266
6588
  if (diagnostics.length === 0) return {
6267
6589
  score: PERFECT_SCORE,
6268
6590
  label: "Healthy"
6269
6591
  };
6270
- let deductions = 0;
6592
+ const deductionsByRule = /* @__PURE__ */ new Map();
6271
6593
  for (const d of diagnostics) {
6272
6594
  const engineWeight = weights[d.engine] ?? 1;
6273
6595
  const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
6274
6596
  const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
6275
- deductions += severityPenalty * engineWeight * styleFactor;
6276
- }
6597
+ const key = `${d.engine}:${d.rule}`;
6598
+ deductionsByRule.set(key, (deductionsByRule.get(key) ?? 0) + severityPenalty * engineWeight * styleFactor);
6599
+ }
6600
+ const defaultRuleCap = typeof maxPerRule === "number" && maxPerRule > 0 ? maxPerRule : null;
6601
+ const capForRule = (key) => {
6602
+ const rule = key.slice(key.indexOf(":") + 1);
6603
+ if (COMMENT_STYLE_RULES.has(rule)) return defaultRuleCap ? Math.min(defaultRuleCap, COMMENT_STYLE_RULE_CAP) : COMMENT_STYLE_RULE_CAP;
6604
+ return defaultRuleCap;
6605
+ };
6606
+ const deductions = [...deductionsByRule.entries()].reduce((total, [key, value]) => {
6607
+ const cap = capForRule(key);
6608
+ return total + (cap ? Math.min(value, cap) : value);
6609
+ }, 0);
6277
6610
  const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
6278
6611
  const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
6279
6612
  const issueDensity = Math.min(1, diagnostics.length / (effectiveFileCount + smoothingConstant));
@@ -6516,6 +6849,107 @@ const readBaseline = (cwd) => {
6516
6849
  }
6517
6850
  };
6518
6851
 
6852
+ //#endregion
6853
+ //#region src/output/finding-assessment.ts
6854
+ const FINDING_KIND_LABELS = {
6855
+ "confirmed-defect": "confirmed defects",
6856
+ "conservative-security": "conservative security",
6857
+ "style-policy": "style/policy",
6858
+ "ai-slop-indicator": "AI-slop indicators"
6859
+ };
6860
+ const STYLE_POLICY_RULES = new Set([
6861
+ "ai-slop/trivial-comment",
6862
+ "ai-slop/narrative-comment",
6863
+ "ai-slop/meta-comment",
6864
+ "ai-slop/console-leftover",
6865
+ "ai-slop/ts-directive",
6866
+ "complexity/file-too-large",
6867
+ "complexity/function-too-long",
6868
+ "complexity/deep-nesting",
6869
+ "complexity/too-many-params",
6870
+ "code-quality/duplicate-block",
6871
+ "eslint/no-empty",
6872
+ "eslint/no-unused-vars",
6873
+ "eslint/no-useless-escape",
6874
+ "eslint/no-unused-expressions",
6875
+ "unicorn/no-useless-fallback-in-spread",
6876
+ "unicorn/prefer-string-starts-ends-with",
6877
+ "unicorn/no-new-array",
6878
+ "unicorn/no-useless-spread"
6879
+ ]);
6880
+ const CONFIRMED_DEFECT_RULES = new Set([
6881
+ "ai-slop/hallucinated-import",
6882
+ "eslint/no-undef",
6883
+ "eslint/no-unreachable",
6884
+ "security/vulnerable-dependency"
6885
+ ]);
6886
+ const LOW_CONFIDENCE_SECURITY_RULES = new Set(["security/innerhtml", "security/dangerously-set-innerhtml"]);
6887
+ const confidenceFor = (diagnostic, kind) => {
6888
+ if (kind === "confirmed-defect") return "high";
6889
+ if (kind === "style-policy") return "medium";
6890
+ if (kind === "conservative-security") {
6891
+ if (LOW_CONFIDENCE_SECURITY_RULES.has(diagnostic.rule)) return "medium";
6892
+ return diagnostic.severity === "error" ? "high" : "medium";
6893
+ }
6894
+ return diagnostic.severity === "error" ? "high" : "medium";
6895
+ };
6896
+ const classifyKind = (diagnostic) => {
6897
+ if (CONFIRMED_DEFECT_RULES.has(diagnostic.rule)) return "confirmed-defect";
6898
+ if (diagnostic.engine === "security") return "conservative-security";
6899
+ if (STYLE_POLICY_RULES.has(diagnostic.rule)) return "style-policy";
6900
+ if (diagnostic.engine === "format" || diagnostic.engine === "code-quality") return "style-policy";
6901
+ if (diagnostic.engine === "ai-slop") return "ai-slop-indicator";
6902
+ if (diagnostic.severity === "error") return "confirmed-defect";
6903
+ return "style-policy";
6904
+ };
6905
+ const assessDiagnostic = (diagnostic) => {
6906
+ const kind = classifyKind(diagnostic);
6907
+ return {
6908
+ kind,
6909
+ confidence: confidenceFor(diagnostic, kind),
6910
+ label: FINDING_KIND_LABELS[kind]
6911
+ };
6912
+ };
6913
+ const summarizeFindingAssessments = (diagnostics) => {
6914
+ const byKind = {
6915
+ "confirmed-defect": 0,
6916
+ "conservative-security": 0,
6917
+ "style-policy": 0,
6918
+ "ai-slop-indicator": 0
6919
+ };
6920
+ const byConfidence = {
6921
+ high: 0,
6922
+ medium: 0,
6923
+ low: 0
6924
+ };
6925
+ const rows = /* @__PURE__ */ new Map();
6926
+ for (const diagnostic of diagnostics) {
6927
+ const assessment = assessDiagnostic(diagnostic);
6928
+ byKind[assessment.kind]++;
6929
+ byConfidence[assessment.confidence]++;
6930
+ const row = rows.get(assessment.kind) ?? {
6931
+ kind: assessment.kind,
6932
+ label: assessment.label,
6933
+ count: 0,
6934
+ errors: 0,
6935
+ warnings: 0,
6936
+ info: 0,
6937
+ fixable: 0
6938
+ };
6939
+ row.count++;
6940
+ if (diagnostic.severity === "error") row.errors++;
6941
+ else if (diagnostic.severity === "warning") row.warnings++;
6942
+ else row.info++;
6943
+ if (diagnostic.fixable) row.fixable++;
6944
+ rows.set(assessment.kind, row);
6945
+ }
6946
+ return {
6947
+ rows: [...rows.values()].sort((a, b) => b.count - a.count),
6948
+ byKind,
6949
+ byConfidence
6950
+ };
6951
+ };
6952
+
6519
6953
  //#endregion
6520
6954
  //#region src/mcp/tools.ts
6521
6955
  const MAX_FINDINGS = 25;
@@ -6553,27 +6987,32 @@ const summariseDiagnostic = (d, rootDirectory) => ({
6553
6987
  column: d.column,
6554
6988
  rule: d.rule,
6555
6989
  severity: d.severity,
6990
+ assessment: assessDiagnostic(d),
6556
6991
  message: d.message,
6557
6992
  fixable: d.fixable,
6558
6993
  help: d.help || void 0
6559
6994
  });
6560
6995
  const summariseDiagnostics = (diagnostics, rootDirectory) => {
6996
+ const counts = {
6997
+ error: diagnostics.filter((d) => d.severity === "error").length,
6998
+ warning: diagnostics.filter((d) => d.severity === "warning").length,
6999
+ fixable: diagnostics.filter((d) => d.fixable).length,
7000
+ total: diagnostics.length
7001
+ };
7002
+ const findings = diagnostics.slice(0, MAX_FINDINGS).map((d) => summariseDiagnostic(d, rootDirectory));
7003
+ const elided = diagnostics.length > MAX_FINDINGS ? diagnostics.length - MAX_FINDINGS : 0;
6561
7004
  return {
6562
- counts: {
6563
- error: diagnostics.filter((d) => d.severity === "error").length,
6564
- warning: diagnostics.filter((d) => d.severity === "warning").length,
6565
- fixable: diagnostics.filter((d) => d.fixable).length,
6566
- total: diagnostics.length
6567
- },
6568
- findings: diagnostics.slice(0, MAX_FINDINGS).map((d) => summariseDiagnostic(d, rootDirectory)),
6569
- elided: diagnostics.length > MAX_FINDINGS ? diagnostics.length - MAX_FINDINGS : 0
7005
+ counts,
7006
+ findingAssessment: summarizeFindingAssessments(diagnostics),
7007
+ findings,
7008
+ elided
6570
7009
  };
6571
7010
  };
6572
7011
  const runScan = async (cwd) => {
6573
7012
  const project = await discoverProject(cwd);
6574
7013
  const config = loadConfig(cwd);
6575
7014
  const diagnostics = (await runEngines(buildEngineContext(project.rootDirectory, project, config), enabledEnginesFromConfig(config))).flatMap((r) => r.diagnostics);
6576
- const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
7015
+ const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
6577
7016
  const errorCount = diagnostics.filter((d) => d.severity === "error").length;
6578
7017
  const failBelow = config.ci.failBelow;
6579
7018
  return {
@@ -6697,6 +7136,10 @@ const handleAislopBaseline = (input) => {
6697
7136
  };
6698
7137
  };
6699
7138
 
7139
+ //#endregion
7140
+ //#region src/version.ts
7141
+ const APP_VERSION = "0.11.0";
7142
+
6700
7143
  //#endregion
6701
7144
  //#region src/telemetry/env.ts
6702
7145
  const detectPackageManager = (env = process.env) => {