@methiu/raigal 0.2.2 → 0.2.3

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
@@ -84,7 +84,7 @@ const getActiveToken = () => {
84
84
  };
85
85
  //#endregion
86
86
  //#region src/version.ts
87
- const APP_VERSION = "0.2.2";
87
+ const APP_VERSION = "0.2.3";
88
88
  //#endregion
89
89
  //#region src/cloud/contract.ts
90
90
  const Severity = z.enum(["error", "warning"]);
@@ -347,7 +347,9 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
347
347
  if (!header.kid) throw new JwtVerificationError("JWT header missing 'kid'", "unknown_kid");
348
348
  const publicKey = getPublicKey(header.kid);
349
349
  if (!publicKey) throw new JwtVerificationError(`Unknown or untrusted signing key id (kid: ${header.kid})`, "unknown_kid");
350
- if (!verify(null, Buffer.from(`${headerB64}.${payloadB64}`, "utf-8"), publicKey, Buffer.from(signatureB64, "base64url"))) throw new JwtVerificationError("Invalid JWT cryptographic signature", "invalid_signature");
350
+ const signedData = Buffer.from(`${headerB64}.${payloadB64}`, "utf-8");
351
+ const signature = Buffer.from(signatureB64, "base64url");
352
+ if (!verify(null, signedData, publicKey, signature)) throw new JwtVerificationError("Invalid JWT cryptographic signature", "invalid_signature");
351
353
  let rawPayload;
352
354
  try {
353
355
  rawPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf-8"));
@@ -359,7 +361,7 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
359
361
  const claims = parsed.data;
360
362
  const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
361
363
  if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
362
- if (!isVersionAtLeast("0.2.2", claims.min_cli_version)) throw new JwtVerificationError(`CLI version ${APP_VERSION} is below required minimum ${claims.min_cli_version}. Run "raigal update" or "npm install -g @methiu/raigal@latest".`, "cli_version_unsupported");
364
+ if (!isVersionAtLeast("0.2.3", claims.min_cli_version)) throw new JwtVerificationError(`CLI version ${APP_VERSION} is below required minimum ${claims.min_cli_version}. Run "raigal update" or "npm install -g @methiu/raigal@latest".`, "cli_version_unsupported");
363
365
  const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
364
366
  const grace = options.allowGraceSeconds ?? 0;
365
367
  if (now > claims.exp + grace) {
@@ -393,7 +395,7 @@ const fetchGitHubOidcToken = async () => {
393
395
  };
394
396
  //#endregion
395
397
  //#region src/cloud/entitlement.ts
396
- const GRACE_PERIOD_SECONDS = 4320 * 60;
398
+ const GRACE_PERIOD_SECONDS = 259200;
397
399
  var MissingCredentialError = class extends Error {
398
400
  constructor(message = "No organization credential found") {
399
401
  super(message);
@@ -518,7 +520,8 @@ const getEntitlement = async () => {
518
520
  }
519
521
  } else oidcToken = await fetchGitHubOidcToken();
520
522
  try {
521
- const response = await fetchEntitlementFromApi(activeToken?.token ?? "", oidcToken);
523
+ const tokenToPass = activeToken?.token ?? "";
524
+ const response = await fetchEntitlementFromApi(tokenToPass, oidcToken);
522
525
  const claims = verifyEntitlementJwt(response.entitlement, { nowSeconds: now });
523
526
  saveCachedEntitlement(response.entitlement, response.refresh_after);
524
527
  return {
@@ -532,7 +535,7 @@ const getEntitlement = async () => {
532
535
  };
533
536
  //#endregion
534
537
  //#region src/cloud/owner.ts
535
- const GITHUB_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/.\s]+?)(?:\.git)?\s*$/;
538
+ const GITHUB_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
536
539
  const parseGitRemote = (rawUrl) => {
537
540
  if (!rawUrl) return {
538
541
  owner: null,
@@ -813,7 +816,7 @@ const DEFAULT_CONFIG = {
813
816
  //#endregion
814
817
  //#region src/config/extends.ts
815
818
  const MAX_DEPTH$1 = 5;
816
- const MAX_CONFIG_BYTES = 1024 * 1024;
819
+ const MAX_CONFIG_BYTES = 1048576;
817
820
  const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
818
821
  const deepMerge = (...sources) => {
819
822
  const result = {};
@@ -994,6 +997,9 @@ const CiSchema = z.object({
994
997
  format: z.enum(["json"]).default("json")
995
998
  }).strict();
996
999
  const TelemetrySchema = z.object({ enabled: z.boolean().default(true) }).strict();
1000
+ const BaselineConfigSchema = z.object({ mode: z.enum(["strict", "relaxed"]).default("relaxed") }).strict();
1001
+ const LicensingConfigSchema = z.object({ onLapse: z.enum(["fail-open", "fail-closed"]).default("fail-open") }).strict();
1002
+ const WorkspaceConfigSchema = z.object({ minimumFloor: z.number().min(0).max(100).default(50) }).strict();
997
1003
  const RuleSeverityOverride = z.enum([
998
1004
  "error",
999
1005
  "warning",
@@ -1064,6 +1070,9 @@ const AislopConfigSchema = z.object({
1064
1070
  format: "json"
1065
1071
  })),
1066
1072
  telemetry: TelemetrySchema.default(() => ({ enabled: true })),
1073
+ baseline: BaselineConfigSchema.optional(),
1074
+ licensing: LicensingConfigSchema.optional(),
1075
+ workspace: WorkspaceConfigSchema.optional(),
1067
1076
  rules: RulesSchema,
1068
1077
  overrides: z.array(FileOverrideSchema).default(() => []),
1069
1078
  exclude: z.array(z.string()).default(() => [
@@ -1097,6 +1106,9 @@ const KNOWN_KEYS_BY_PATH = {
1097
1106
  "scoring",
1098
1107
  "ci",
1099
1108
  "telemetry",
1109
+ "baseline",
1110
+ "licensing",
1111
+ "workspace",
1100
1112
  "rules",
1101
1113
  "exclude",
1102
1114
  "include"
@@ -1147,7 +1159,10 @@ const KNOWN_KEYS_BY_PATH = {
1147
1159
  ],
1148
1160
  "scoring.thresholds": ["good", "ok"],
1149
1161
  ci: ["failBelow", "format"],
1150
- telemetry: ["enabled"]
1162
+ telemetry: ["enabled"],
1163
+ baseline: ["mode"],
1164
+ licensing: ["onLapse"],
1165
+ workspace: ["minimumFloor"]
1151
1166
  };
1152
1167
  const levenshtein = (a, b) => {
1153
1168
  const m = a.length;
@@ -1277,7 +1292,8 @@ const loadConfig = (directory) => {
1277
1292
  const configPath = findConfigFile(directory);
1278
1293
  if (!configPath) return DEFAULT_CONFIG;
1279
1294
  try {
1280
- return validateConfig(loadConfigChain(configPath), configPath);
1295
+ const merged = loadConfigChain(configPath);
1296
+ return validateConfig(merged, configPath);
1281
1297
  } catch (error) {
1282
1298
  if (error instanceof ConfigError) throw error;
1283
1299
  throw new ConfigError(configPath, `Failed to parse ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
@@ -1455,7 +1471,7 @@ const filterExcludedDiagnostics = (diagnostics, rootDirectory, excludePatterns)
1455
1471
  };
1456
1472
  //#endregion
1457
1473
  //#region src/utils/project-path-safety.ts
1458
- const MAX_PROJECT_CONFIG_BYTES = 1024 * 1024;
1474
+ const MAX_PROJECT_CONFIG_BYTES = 1048576;
1459
1475
  const isWithinDirectory$2 = (rootDirectory, candidate) => {
1460
1476
  const relative = path.relative(rootDirectory, candidate);
1461
1477
  return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
@@ -1542,7 +1558,7 @@ const isAutoGenerated = (filePath) => {
1542
1558
  };
1543
1559
  //#endregion
1544
1560
  //#region src/utils/git-ignore.ts
1545
- const MAX_BUFFER$1 = 50 * 1024 * 1024;
1561
+ const MAX_BUFFER$1 = 52428800;
1546
1562
  const NOT_IGNORED_ARGUMENTS = [
1547
1563
  "ls-files",
1548
1564
  "--cached",
@@ -1653,7 +1669,7 @@ const dropGitIgnoredPaths = (rootDirectory, absolutePaths) => {
1653
1669
  };
1654
1670
  //#endregion
1655
1671
  //#region src/utils/project-file-list.ts
1656
- const MAX_BUFFER = 50 * 1024 * 1024;
1672
+ const MAX_BUFFER = 52428800;
1657
1673
  const normalizePruneDirectories = (directories) => new Set([...directories].map((directory) => directory.toLowerCase()));
1658
1674
  const isInPrunedDirectory = (filePath, pruneDirectories) => {
1659
1675
  const pathSegments = filePath.split("/");
@@ -2567,7 +2583,7 @@ const tokenizeJs = (content, ext = ".ts") => {
2567
2583
  };
2568
2584
  //#endregion
2569
2585
  //#region src/utils/source-masker.ts
2570
- const JS_EXTS$2 = /* @__PURE__ */ new Set([
2586
+ const JS_EXTS$3 = /* @__PURE__ */ new Set([
2571
2587
  ".ts",
2572
2588
  ".tsx",
2573
2589
  ".js",
@@ -2575,7 +2591,7 @@ const JS_EXTS$2 = /* @__PURE__ */ new Set([
2575
2591
  ".mjs",
2576
2592
  ".cjs"
2577
2593
  ]);
2578
- const PY_EXTS = /* @__PURE__ */ new Set([".py"]);
2594
+ const PY_EXTS$1 = /* @__PURE__ */ new Set([".py"]);
2579
2595
  const RB_EXTS = /* @__PURE__ */ new Set([".rb"]);
2580
2596
  const PHP_EXTS = /* @__PURE__ */ new Set([".php"]);
2581
2597
  const CSHARP_EXTS = /* @__PURE__ */ new Set([".cs"]);
@@ -2599,8 +2615,8 @@ const C_STYLE_COMMENT_EXTS = /* @__PURE__ */ new Set([
2599
2615
  ".zig"
2600
2616
  ]);
2601
2617
  const familyForExt = (ext) => {
2602
- if (JS_EXTS$2.has(ext)) return "js";
2603
- if (PY_EXTS.has(ext)) return "py";
2618
+ if (JS_EXTS$3.has(ext)) return "js";
2619
+ if (PY_EXTS$1.has(ext)) return "py";
2604
2620
  if (RB_EXTS.has(ext)) return "rb";
2605
2621
  if (PHP_EXTS.has(ext)) return "php";
2606
2622
  if (CSHARP_EXTS.has(ext)) return "csharp";
@@ -2832,7 +2848,7 @@ const getCommentLines = (content, ext) => {
2832
2848
  };
2833
2849
  //#endregion
2834
2850
  //#region src/engines/ai-slop/abstractions.ts
2835
- const JS_EXTS$1 = /* @__PURE__ */ new Set([
2851
+ const JS_EXTS$2 = /* @__PURE__ */ new Set([
2836
2852
  ".ts",
2837
2853
  ".tsx",
2838
2854
  ".js",
@@ -2843,11 +2859,11 @@ const JS_EXTS$1 = /* @__PURE__ */ new Set([
2843
2859
  const THIN_WRAPPER_PATTERNS = [
2844
2860
  {
2845
2861
  pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
2846
- extensions: JS_EXTS$1
2862
+ extensions: JS_EXTS$2
2847
2863
  },
2848
2864
  {
2849
2865
  pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
2850
- extensions: JS_EXTS$1
2866
+ extensions: JS_EXTS$2
2851
2867
  },
2852
2868
  {
2853
2869
  pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
@@ -4095,7 +4111,7 @@ const scriptKindFor$1 = (ext) => {
4095
4111
  }
4096
4112
  };
4097
4113
  const lineFor$1 = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
4098
- const makeDiagnostic = (filePath, line, rule, message, help) => ({
4114
+ const makeDiagnostic$1 = (filePath, line, rule, message, help) => ({
4099
4115
  filePath,
4100
4116
  engine: "ai-slop",
4101
4117
  rule,
@@ -4145,7 +4161,7 @@ const detectRedundantTryCatch = (sourceFile, relativePath) => {
4145
4161
  if (ts.isTryStatement(node) && node.catchClause && !node.finallyBlock) {
4146
4162
  const catchNameNode = node.catchClause.variableDeclaration?.name;
4147
4163
  const [onlyStatement] = node.catchClause.block.statements;
4148
- if (catchNameNode && ts.isIdentifier(catchNameNode) && node.catchClause.block.statements.length === 1 && onlyStatement && (isRethrowStatement(onlyStatement, catchNameNode.text) || isPromiseRejectRethrow(onlyStatement, catchNameNode.text))) diagnostics.push(makeDiagnostic(relativePath, lineFor$1(sourceFile, node.catchClause), "ai-slop/redundant-try-catch", "Catch block only rethrows the same error", "Remove the try/catch or add useful context, cleanup, or recovery. Rethrowing unchanged errors is usually defensive agent noise."));
4164
+ if (catchNameNode && ts.isIdentifier(catchNameNode) && node.catchClause.block.statements.length === 1 && onlyStatement && (isRethrowStatement(onlyStatement, catchNameNode.text) || isPromiseRejectRethrow(onlyStatement, catchNameNode.text))) diagnostics.push(makeDiagnostic$1(relativePath, lineFor$1(sourceFile, node.catchClause), "ai-slop/redundant-try-catch", "Catch block only rethrows the same error", "Remove the try/catch or add useful context, cleanup, or recovery. Rethrowing unchanged errors is usually defensive agent noise."));
4149
4165
  }
4150
4166
  ts.forEachChild(node, visit);
4151
4167
  };
@@ -4163,7 +4179,7 @@ const detectPrimitiveCoercions = (sourceFile, relativePath) => {
4163
4179
  const [arg] = child.arguments;
4164
4180
  if (arg && ts.isIdentifier(arg)) {
4165
4181
  const primitive = params.get(arg.text);
4166
- if (primitive && child.expression.text === COERCION_CTORS[primitive]) diagnostics.push(makeDiagnostic(relativePath, lineFor$1(sourceFile, child), "ai-slop/redundant-type-coercion", `Parameter '${arg.text}' is already typed as ${primitive} but is coerced again`, "Trust the typed boundary or validate unknown input before this function. Re-coercing already typed parameters is usually defensive agent noise."));
4182
+ if (primitive && child.expression.text === COERCION_CTORS[primitive]) diagnostics.push(makeDiagnostic$1(relativePath, lineFor$1(sourceFile, child), "ai-slop/redundant-type-coercion", `Parameter '${arg.text}' is already typed as ${primitive} but is coerced again`, "Trust the typed boundary or validate unknown input before this function. Re-coercing already typed parameters is usually defensive agent noise."));
4167
4183
  }
4168
4184
  }
4169
4185
  ts.forEachChild(child, visitBody);
@@ -4206,7 +4222,7 @@ const detectDuplicateExportedTypes = (parsedSources) => {
4206
4222
  continue;
4207
4223
  }
4208
4224
  if (previous.filePath === declaration.filePath) continue;
4209
- diagnostics.push(makeDiagnostic(declaration.filePath, declaration.line, "ai-slop/duplicate-type-declaration", `Exported type '${declaration.name}' duplicates an existing declaration`, `Reuse or import the existing type from ${previous.filePath} instead of re-declaring the same shape in another file.`));
4225
+ diagnostics.push(makeDiagnostic$1(declaration.filePath, declaration.line, "ai-slop/duplicate-type-declaration", `Exported type '${declaration.name}' duplicates an existing declaration`, `Reuse or import the existing type from ${previous.filePath} instead of re-declaring the same shape in another file.`));
4210
4226
  }
4211
4227
  }
4212
4228
  return diagnostics;
@@ -4308,7 +4324,7 @@ const detectDuplicateImports = async (context) => {
4308
4324
  rule: "ai-slop/duplicate-import",
4309
4325
  severity: "warning",
4310
4326
  message: `"${spec}" is also imported on line ${firstLine}. Merge into a single import statement.`,
4311
- help: "Two imports from the same module split readers' attention and grow the import block. Run aislop fix to merge them automatically.",
4327
+ help: "Two imports from the same module split readers' attention and grow the import block. Run raigal fix to merge them automatically.",
4312
4328
  line: dup.line,
4313
4329
  column: 1,
4314
4330
  category: "AI Slop",
@@ -6016,7 +6032,8 @@ const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith
6016
6032
  const RUNTIME_BUILTINS = /* @__PURE__ */ new Set(["bun"]);
6017
6033
  const isJsBuiltin = (spec) => {
6018
6034
  if (RUNTIME_BUILTINS.has(spec)) return true;
6019
- return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
6035
+ const stripped = spec.startsWith("node:") ? spec.slice(5) : spec;
6036
+ return isBuiltin(stripped) || isBuiltin(spec);
6020
6037
  };
6021
6038
  const VIRTUAL_MODULE_PREFIXES = [
6022
6039
  "astro:",
@@ -6228,7 +6245,8 @@ const isLocalAliasReplacement = (rawValue, configDirectory, rootDirectory) => {
6228
6245
  if (!target) return false;
6229
6246
  if (!target.startsWith(".") && !target.startsWith("/") && !target.startsWith("~/") && !target.startsWith("@/")) return false;
6230
6247
  const normalized = target.startsWith("~/") || target.startsWith("@/") ? target.slice(2) : target;
6231
- return isRootBoundedTarget(path.isAbsolute(normalized) ? normalized : path.resolve(configDirectory, normalized), rootDirectory);
6248
+ const candidate = path.isAbsolute(normalized) ? normalized : path.resolve(configDirectory, normalized);
6249
+ return isRootBoundedTarget(candidate, rootDirectory);
6232
6250
  };
6233
6251
  const hasValidPackageImportTarget = (value, packageDirectory, rootDirectory) => {
6234
6252
  if (!isRootBoundedTarget(packageDirectory, rootDirectory)) return false;
@@ -7348,7 +7366,9 @@ const isRustDocCommentLine = (line) => {
7348
7366
  };
7349
7367
  const collectBlocks = (sourceLines, syntax, content, ext) => {
7350
7368
  const blocks = [];
7351
- const commentLines = getCommentLines(content ?? sourceLines.join("\n"), ext ?? (syntax.linePrefixes.includes("//") ? ".ts" : ".py"));
7369
+ const actualContent = content ?? sourceLines.join("\n");
7370
+ const actualExt = ext ?? (syntax.linePrefixes.includes("//") ? ".ts" : ".py");
7371
+ const commentLines = getCommentLines(actualContent, actualExt);
7352
7372
  let i = 0;
7353
7373
  while (i < sourceLines.length) {
7354
7374
  const line = sourceLines[i];
@@ -7676,7 +7696,8 @@ const detectNarrativeComments = async (context) => {
7676
7696
  } catch {
7677
7697
  continue;
7678
7698
  }
7679
- const blocks = collectBlocks(content.split("\n"), syntax, content, ext);
7699
+ const lines = content.split("\n");
7700
+ const blocks = collectBlocks(lines, syntax, content, ext);
7680
7701
  for (const block of blocks) {
7681
7702
  const { matched, reason } = detectNarrativeInBlock(block, ext);
7682
7703
  if (!matched) continue;
@@ -8148,7 +8169,7 @@ const detectRustPatterns = async (context) => {
8148
8169
  };
8149
8170
  //#endregion
8150
8171
  //#region src/engines/ai-slop/silent-recovery.ts
8151
- const JS_EXTS = /* @__PURE__ */ new Set([
8172
+ const JS_EXTS$1 = /* @__PURE__ */ new Set([
8152
8173
  ".ts",
8153
8174
  ".tsx",
8154
8175
  ".js",
@@ -8343,7 +8364,7 @@ const detectSilentRecovery = async (context) => {
8343
8364
  for (const filePath of files) {
8344
8365
  if (isAutoGenerated(filePath)) continue;
8345
8366
  const ext = path.extname(filePath);
8346
- const isJs = JS_EXTS.has(ext);
8367
+ const isJs = JS_EXTS$1.has(ext);
8347
8368
  if (!isJs && !(ext === ".py")) continue;
8348
8369
  const relPath = relativePosix(context.rootDirectory, filePath);
8349
8370
  if (isNonProductionPath(relPath)) continue;
@@ -8628,7 +8649,7 @@ const detectUnusedCss = async (context) => {
8628
8649
  rule: "ai-slop/unused-css",
8629
8650
  severity: "warning",
8630
8651
  message: `CSS class '.${def.name}' is defined but never referenced in the codebase`,
8631
- help: "Remove the dead CSS rule, or confirm the class is used dynamically (and add an aislop-ignore comment if so)",
8652
+ help: "Remove the dead CSS rule, or confirm the class is used dynamically (and add a raigal-ignore comment if so)",
8632
8653
  line: def.line,
8633
8654
  column: 0,
8634
8655
  category: "AI Slop",
@@ -9628,7 +9649,8 @@ const checkFunctionDiagnostics = (relativePath, fn, limits, ext) => {
9628
9649
  const results = [];
9629
9650
  const multiplier = functionLocMultiplier(fn, ext);
9630
9651
  const fnMax = functionLocBudget(fn, ext, limits.maxFunctionLoc);
9631
- if (exceedsLimit(fn.lineCount - fn.templateLines, limits.maxFunctionLoc, multiplier)) results.push({
9652
+ const effectiveLineCount = fn.lineCount - fn.templateLines;
9653
+ if (exceedsLimit(effectiveLineCount, limits.maxFunctionLoc, multiplier)) results.push({
9632
9654
  filePath: relativePath,
9633
9655
  engine: "code-quality",
9634
9656
  rule: "complexity/function-too-long",
@@ -9718,14 +9740,14 @@ const isTrivialLine = (line) => {
9718
9740
  };
9719
9741
  const SVG_MARKUP_RE = /<\/?(?:svg|path|polyline|line|circle|rect|g)\b|(?:xmlns|viewBox|stroke(?:-width|-linecap|-linejoin)?|fill|fill-opacity|d|points|x1|x2|y1|y2)=/;
9720
9742
  const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
9721
- const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
9722
- const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
9743
+ const SUPPRESS_RE = /(?:raigal|aislop)[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
9744
+ const FILE_SUPPRESS_RE = /(?:raigal|aislop)[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
9723
9745
  const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
9724
9746
  const isLowSignalMarkupWindow = (lines) => {
9725
9747
  return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
9726
9748
  };
9727
9749
  const isLowSignalDataWindow = (lines) => {
9728
- return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= WINDOW_SIZE - 1;
9750
+ return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= 9;
9729
9751
  };
9730
9752
  const findSuppressedLines = (lines) => {
9731
9753
  const suppressed = /* @__PURE__ */ new Set();
@@ -9757,7 +9779,7 @@ const collectMeaningfulLines = (content) => {
9757
9779
  if (isLowSignalDataWindow(window)) continue;
9758
9780
  if (window.every(isTrivialLine)) continue;
9759
9781
  const normalised = window.map(normaliseLine);
9760
- if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
9782
+ if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < 9) continue;
9761
9783
  if (new Set(normalised).size < MIN_DISTINCT_LINES) continue;
9762
9784
  hits.push({
9763
9785
  startLine: i + 1,
@@ -10045,8 +10067,8 @@ const shouldIncludeIssue = (issueType, filePath) => {
10045
10067
  return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
10046
10068
  };
10047
10069
  const DEPENDENCY_HELP = {
10048
- dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
10049
- devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
10070
+ dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `raigal fix`.",
10071
+ devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `raigal fix`.",
10050
10072
  unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
10051
10073
  unresolved: "This import cannot be resolved. Check for typos or missing packages.",
10052
10074
  binaries: "This binary is used but its package is not in package.json."
@@ -10888,7 +10910,7 @@ const runGofmt = async (context) => {
10888
10910
  rule: "go-formatting",
10889
10911
  severity: "warning",
10890
10912
  message: "Go file is not formatted correctly",
10891
- help: "Run `aislop fix` to auto-format with gofmt",
10913
+ help: "Run `raigal fix` to auto-format with gofmt",
10892
10914
  line: 0,
10893
10915
  column: 0,
10894
10916
  category: "Format",
@@ -10907,7 +10929,8 @@ const resolvePackageRoot = (startFile) => {
10907
10929
  while (true) {
10908
10930
  const packageJsonPath = path.join(current, "package.json");
10909
10931
  if (fs.existsSync(packageJsonPath)) try {
10910
- if (JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")).name === "aislop") return current;
10932
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
10933
+ if (packageJson.name === "aislop" || packageJson.name === "@methiu/raigal" || packageJson.name === "raigal") return current;
10911
10934
  } catch {}
10912
10935
  const parent = path.dirname(current);
10913
10936
  if (parent === current) break;
@@ -11018,7 +11041,7 @@ const parseRuffFormatOutput = (output, rootDir) => {
11018
11041
  rule: "python-formatting",
11019
11042
  severity: "warning",
11020
11043
  message: "Python file is not formatted correctly",
11021
- help: "Run `aislop fix` to auto-format with ruff",
11044
+ help: "Run `raigal fix` to auto-format with ruff",
11022
11045
  line: 0,
11023
11046
  column: 0,
11024
11047
  category: "Format",
@@ -11091,7 +11114,8 @@ const parseClangTidyOutput = (output, rootDirectory) => {
11091
11114
  const runClangTidy = async (context) => {
11092
11115
  const compileCommandsDir = findCompileCommandsDir(context);
11093
11116
  if (!compileCommandsDir) return [];
11094
- const sources = filterSourcesInDatabase(findCppSources(context).filter((f) => CPP_IMPL_EXTENSIONS.has(path.extname(f).toLowerCase())), readCompileCommandsFiles(compileCommandsDir));
11117
+ const implSources = findCppSources(context).filter((f) => CPP_IMPL_EXTENSIONS.has(path.extname(f).toLowerCase()));
11118
+ const sources = filterSourcesInDatabase(implSources, readCompileCommandsFiles(compileCommandsDir));
11095
11119
  if (sources.length === 0) return [];
11096
11120
  const diagnostics = [];
11097
11121
  for (const chunk of chunkFilePaths(sources, CLANG_TIDY_CHUNK_MAX_FILES)) try {
@@ -11461,7 +11485,7 @@ const runExpoDoctor = async (context) => {
11461
11485
  rule: "expo-doctor/config-error",
11462
11486
  severity: "warning",
11463
11487
  message: configError,
11464
- help: "Install project dependencies, then re-run `aislop scan`.",
11488
+ help: "Install project dependencies, then re-run `raigal scan`.",
11465
11489
  line: 0,
11466
11490
  column: 0,
11467
11491
  category: "Expo",
@@ -12425,7 +12449,8 @@ const resolveTrustedTscPath = () => {
12425
12449
  };
12426
12450
  const isReferenceOnlyConfig = (tsconfigPath) => {
12427
12451
  try {
12428
- const parsed = parseJsonc(fs.readFileSync(tsconfigPath, "utf-8"));
12452
+ const raw = fs.readFileSync(tsconfigPath, "utf-8");
12453
+ const parsed = parseJsonc(raw);
12429
12454
  if (!parsed) return false;
12430
12455
  return Array.isArray(parsed.references) && !parsed.files && !parsed.include && !parsed.extends;
12431
12456
  } catch {
@@ -12568,6 +12593,174 @@ const lintEngine = {
12568
12593
  }
12569
12594
  };
12570
12595
  //#endregion
12596
+ //#region src/engines/security/ai-security.ts
12597
+ const JS_EXTS = /* @__PURE__ */ new Set([
12598
+ ".ts",
12599
+ ".tsx",
12600
+ ".js",
12601
+ ".jsx",
12602
+ ".mjs",
12603
+ ".cjs"
12604
+ ]);
12605
+ const PY_EXTS = /* @__PURE__ */ new Set([".py"]);
12606
+ const UNICODE_TAG_RE = /[\u{E0000}-\u{E007F}]/gu;
12607
+ const hasRuleIgnore = (lines, lineIndex, ruleSuffix) => {
12608
+ const start = Math.max(0, lineIndex - 2);
12609
+ const re = new RegExp(`(?:raigal|aislop)[- ]ignore(?:-file)?\\b.*(?:${ruleSuffix}|security\\/${ruleSuffix})`, "i");
12610
+ return lines.slice(start, lineIndex + 1).some((line) => re.test(line));
12611
+ };
12612
+ const hasFileIgnore = (lines, ruleSuffix) => {
12613
+ const re = new RegExp(`(?:raigal|aislop)[- ]ignore-file\\b.*(?:${ruleSuffix}|security\\/${ruleSuffix})`, "i");
12614
+ return lines.slice(0, 10).some((line) => re.test(line));
12615
+ };
12616
+ const makeDiagnostic = (filePath, rule, message, help, line, column) => ({
12617
+ filePath,
12618
+ engine: "security",
12619
+ rule,
12620
+ severity: "error",
12621
+ message,
12622
+ help,
12623
+ line,
12624
+ column,
12625
+ category: "Security",
12626
+ fixable: false
12627
+ });
12628
+ const P_BYPASS = "bypassPermissions";
12629
+ const P_EXEC_TYPE = "executor_type";
12630
+ const P_LOCAL = "local";
12631
+ const P_USE_DOCKER = "use_docker";
12632
+ const P_AUTO_RUN = "auto_run";
12633
+ const P_UNSAFE_MODE = "unsafe_mode";
12634
+ const P_PYTHON_REPL = "PythonREPLTool";
12635
+ const UNSANDBOXED_TOOL_PATTERNS = [
12636
+ {
12637
+ pattern: new RegExp(`\\bpermission_?mode["']?\\s*[:=]\\s*["']${P_BYPASS}["']`, "i"),
12638
+ message: `permission_mode '${P_BYPASS}' bypasses agent sandboxing and human confirmation`,
12639
+ help: `Do not use ${P_BYPASS} mode in production; use standard interactive approval`
12640
+ },
12641
+ {
12642
+ pattern: new RegExp(`\\b${P_EXEC_TYPE}["']?\\s*[:=]\\s*["']${P_LOCAL}["']`, "i"),
12643
+ message: `${P_EXEC_TYPE}='${P_LOCAL}' executes LLM-generated code directly on host machine without container isolation`,
12644
+ help: "Configure agent to use DockerCommandLineCodeExecutor or a containerized sandbox environment"
12645
+ },
12646
+ {
12647
+ pattern: new RegExp(`\\b(?:LocalCommandLineCodeExecutor|LocalCodeExecutor)\\s*\\(`),
12648
+ message: "Local code executor executes LLM-generated code on host machine without container isolation",
12649
+ help: "Use DockerCommandLineCodeExecutor or an isolated execution sandbox"
12650
+ },
12651
+ {
12652
+ pattern: new RegExp(`\\b${P_USE_DOCKER}\\s*[:=]\\s*(?:False|false)\\b|["']${P_USE_DOCKER}["']\\s*:\\s*(?:False|false)\\b`),
12653
+ message: `${P_USE_DOCKER}=False disables container sandboxing for agent code execution`,
12654
+ help: `Set ${P_USE_DOCKER}=True to isolate agent execution inside Docker containers`
12655
+ },
12656
+ {
12657
+ pattern: new RegExp(`\\b(?:interpreter\\.)?${P_AUTO_RUN}\\s*[:=]\\s*(?:True|true)\\b`),
12658
+ message: `${P_AUTO_RUN}=True allows autonomous shell execution without human confirmation`,
12659
+ help: `Keep ${P_AUTO_RUN}=False so dangerous commands require explicit human review`
12660
+ },
12661
+ {
12662
+ pattern: new RegExp(`\\bCodeInterpreterTool\\s*\\([^)]*${P_UNSAFE_MODE}\\s*=\\s*(?:True|true)\\b|\\b${P_UNSAFE_MODE}\\s*[:=]\\s*(?:True|true)\\b`),
12663
+ message: `${P_UNSAFE_MODE}=True allows code interpreter tool to execute without Docker isolation`,
12664
+ help: `Disable ${P_UNSAFE_MODE} and isolate code execution in containerized environments`
12665
+ },
12666
+ {
12667
+ pattern: new RegExp(`\\b(?:${P_PYTHON_REPL}|PythonAstREPLTool)\\s*\\(`),
12668
+ message: `${P_PYTHON_REPL} executes arbitrary Python code in host process without container isolation`,
12669
+ help: `Use containerized sandbox execution rather than unconfined ${P_PYTHON_REPL} in production`
12670
+ }
12671
+ ];
12672
+ const P_SYS = "system";
12673
+ const P_DEV = "developer";
12674
+ const P_SM = "SystemMessage";
12675
+ const PROMPT_INJECTION_JS_PATTERNS = [
12676
+ new RegExp(`(?:role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?content\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`)`, "g"),
12677
+ new RegExp(`(?:content\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`[^}{;]{0,120}?role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
12678
+ new RegExp(`(?:role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?content\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["']))`, "g"),
12679
+ new RegExp(`(?:content\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["'])[^}{;]{0,120}?role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
12680
+ new RegExp(`\\b(?:${P_SYS}|systemPrompt|system_prompt|system_instruction)\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\``, "g"),
12681
+ new RegExp(`\\b(?:${P_SYS}|systemPrompt|system_prompt|system_instruction)\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["'])`, "g"),
12682
+ new RegExp(`\\b(?:new\\s+)?${P_SM}\\s*\\(\\s*(?:\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`|["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["']|\\{\\s*content\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`)`, "g"),
12683
+ new RegExp(`\\[\\s*["']${P_SYS}["']\\s*,\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`\\s*\\]`, "g")
12684
+ ];
12685
+ const PROMPT_INJECTION_PY_PATTERNS = [
12686
+ new RegExp(`(?:["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?["']content["']\\s*:\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["']))`, "g"),
12687
+ new RegExp(`(?:["']content["']\\s*:\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])[^}{;]{0,120}?["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
12688
+ new RegExp(`(?:["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?["']content["']\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\())`, "g"),
12689
+ new RegExp(`(?:["']content["']\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\()[^}{;]{0,120}?["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
12690
+ new RegExp(`\\b(?:${P_SYS}|system_prompt|system_message|system_instruction)\\s*=\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])`, "g"),
12691
+ new RegExp(`\\b(?:${P_SYS}|system_prompt|system_message|system_instruction)\\s*=\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\()`, "g"),
12692
+ new RegExp(`\\b${P_SM}\\s*\\(\\s*(?:content\\s*=\\s*)?f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])`, "g"),
12693
+ new RegExp(`\\b${P_SM}\\s*\\(\\s*(?:content\\s*=\\s*)?(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\()`, "g"),
12694
+ new RegExp(`\\(\\s*["']${P_SYS}["']\\s*,\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])\\s*\\)`, "g")
12695
+ ];
12696
+ const checkInvisibleUnicode = (relPath, content, lines) => {
12697
+ if (hasFileIgnore(lines, "invisible-unicode-tags")) return [];
12698
+ const results = [];
12699
+ const tagRegex = new RegExp(UNICODE_TAG_RE.source, UNICODE_TAG_RE.flags);
12700
+ for (const match of content.matchAll(tagRegex)) {
12701
+ const matchIndex = match.index ?? 0;
12702
+ const line = content.slice(0, matchIndex).split("\n").length;
12703
+ const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
12704
+ if (hasRuleIgnore(lines, line - 1, "invisible-unicode-tags")) continue;
12705
+ results.push(makeDiagnostic(relPath, "security/invisible-unicode-tags", "Invisible Unicode Tag character (U+E0000-U+E007F) detected: potential stealth prompt injection or ASCII smuggling", "Remove invisible Unicode tag characters. Unicode Tag block (U+E0000-U+E007F) has no legitimate printable presentation in source code", line, col));
12706
+ }
12707
+ return results;
12708
+ };
12709
+ const checkUnsandboxedTools = (relPath, content, lines) => {
12710
+ if (hasFileIgnore(lines, "unsandboxed-agent-tool")) return [];
12711
+ const results = [];
12712
+ for (const { pattern, message, help } of UNSANDBOXED_TOOL_PATTERNS) {
12713
+ const regex = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
12714
+ for (const match of content.matchAll(regex)) {
12715
+ const matchIndex = match.index ?? 0;
12716
+ const line = content.slice(0, matchIndex).split("\n").length;
12717
+ const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
12718
+ if (hasRuleIgnore(lines, line - 1, "unsandboxed-agent-tool")) continue;
12719
+ results.push(makeDiagnostic(relPath, "security/unsandboxed-agent-tool", message, help, line, col));
12720
+ }
12721
+ }
12722
+ return results;
12723
+ };
12724
+ const checkPromptInjection = (relPath, content, lines, isJs) => {
12725
+ if (hasFileIgnore(lines, "prompt-injection")) return [];
12726
+ const results = [];
12727
+ const patterns = isJs ? PROMPT_INJECTION_JS_PATTERNS : PROMPT_INJECTION_PY_PATTERNS;
12728
+ for (const pattern of patterns) {
12729
+ const regex = new RegExp(pattern.source, pattern.flags);
12730
+ for (const match of content.matchAll(regex)) {
12731
+ const matchIndex = match.index ?? 0;
12732
+ const line = content.slice(0, matchIndex).split("\n").length;
12733
+ const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
12734
+ if (hasRuleIgnore(lines, line - 1, "prompt-injection")) continue;
12735
+ results.push(makeDiagnostic(relPath, "security/prompt-injection", "Untrusted variable interpolation into system prompt creates prompt injection vulnerability", "Keep system prompts static. Pass dynamic user inputs via user messages or input parameters with strict delimiters", line, col));
12736
+ }
12737
+ }
12738
+ return results;
12739
+ };
12740
+ const scanAiSecurity = async (context) => {
12741
+ const files = getSourceFiles(context);
12742
+ const diagnostics = [];
12743
+ for (const filePath of files) {
12744
+ const ext = path.extname(filePath);
12745
+ let content;
12746
+ try {
12747
+ content = fs.readFileSync(filePath, "utf-8");
12748
+ } catch {
12749
+ continue;
12750
+ }
12751
+ const relativePath = relativePosix(context.rootDirectory, filePath);
12752
+ const lines = content.split("\n");
12753
+ diagnostics.push(...checkInvisibleUnicode(relativePath, content, lines));
12754
+ const isJs = JS_EXTS.has(ext);
12755
+ const isPy = PY_EXTS.has(ext);
12756
+ if (isJs || isPy) {
12757
+ diagnostics.push(...checkUnsandboxedTools(relativePath, content, lines));
12758
+ diagnostics.push(...checkPromptInjection(relativePath, content, lines, isJs));
12759
+ }
12760
+ }
12761
+ return diagnostics;
12762
+ };
12763
+ //#endregion
12571
12764
  //#region src/ui/invocation.ts
12572
12765
  const detectInvocation = () => "raigal";
12573
12766
  //#endregion
@@ -12950,7 +13143,7 @@ const parseJsAudit = (output, source, manifest) => {
12950
13143
  rule: "security/dependency-audit-skipped",
12951
13144
  severity: "info",
12952
13145
  message: `Dependency audit skipped (${source}): lockfile is missing`,
12953
- help: errorDetail ?? "Generate a lockfile, then re-run `aislop scan` for dependency vulnerability checks.",
13146
+ help: errorDetail ?? "Generate a lockfile, then re-run `raigal scan` for dependency vulnerability checks.",
12954
13147
  line: 0,
12955
13148
  column: 0,
12956
13149
  category: "Security",
@@ -13124,10 +13317,11 @@ const auditSkippedDiagnostic = (source, help) => ({
13124
13317
  });
13125
13318
  const runNpmAudit = async (rootDir, timeout) => {
13126
13319
  try {
13127
- return parseJsAudit((await runSubprocess("npm", ["audit", "--json"], {
13320
+ const result = await runSubprocess("npm", ["audit", "--json"], {
13128
13321
  cwd: rootDir,
13129
13322
  timeout
13130
- })).stdout, "npm audit", readRuntimeDependencies(rootDir));
13323
+ });
13324
+ return parseJsAudit(result.stdout, "npm audit", readRuntimeDependencies(rootDir));
13131
13325
  } catch (error) {
13132
13326
  return [auditSkippedDiagnostic("npm audit", `Failed to run npm audit: ${errorMessageOf(error)}`)];
13133
13327
  }
@@ -13148,10 +13342,11 @@ const runBunAudit = async (rootDir, timeout) => {
13148
13342
  const runPnpmAuditWithFallback = async (rootDir, timeout) => {
13149
13343
  const canFallbackToNpm = fs.existsSync(path.join(rootDir, "package-lock.json"));
13150
13344
  try {
13151
- const diagnostics = parseJsAudit((await runSubprocess("pnpm", ["audit", "--json"], {
13345
+ const result = await runSubprocess("pnpm", ["audit", "--json"], {
13152
13346
  cwd: rootDir,
13153
13347
  timeout
13154
- })).stdout, "pnpm audit", readRuntimeDependencies(rootDir));
13348
+ });
13349
+ const diagnostics = parseJsAudit(result.stdout, "pnpm audit", readRuntimeDependencies(rootDir));
13155
13350
  if (diagnostics.some((d) => d.rule === "security/dependency-audit-skipped")) {
13156
13351
  if (canFallbackToNpm) return runNpmAudit(rootDir, timeout);
13157
13352
  return diagnostics;
@@ -13508,7 +13703,7 @@ const RISKY_PATTERNS = [
13508
13703
  //#region src/engines/security/risky.ts
13509
13704
  const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
13510
13705
  const start = Math.max(0, lineIndex - 2);
13511
- return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
13706
+ return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|raigal-ignore|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
13512
13707
  };
13513
13708
  const isStructuredDataScript = (content, matchIndex) => {
13514
13709
  const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
@@ -13830,7 +14025,11 @@ const securityEngine = {
13830
14025
  name: "security",
13831
14026
  async run(context) {
13832
14027
  const diagnostics = [];
13833
- const promises = [scanSecrets(context), detectRiskyConstructs(context)];
14028
+ const promises = [
14029
+ scanSecrets(context),
14030
+ detectRiskyConstructs(context),
14031
+ scanAiSecurity(context)
14032
+ ];
13834
14033
  if (context.config.security.audit) promises.push(runDependencyAudit(context));
13835
14034
  const results = await Promise.allSettled(promises);
13836
14035
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -13954,7 +14153,10 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
13954
14153
  failed: true,
13955
14154
  skipReason: r.reason instanceof Error ? r.reason.message : String(r.reason)
13956
14155
  });
13957
- return dedupeEquivalenceDiagnostics(dedupeOverlappingComments(dedupeCSharpAsync(stripSecretDiagnostics(finalResults))));
14156
+ const stripped = stripSecretDiagnostics(finalResults);
14157
+ const csharp = dedupeCSharpAsync(stripped);
14158
+ const comments = dedupeOverlappingComments(csharp);
14159
+ return dedupeEquivalenceDiagnostics(comments);
13958
14160
  };
13959
14161
  const stripSecretDiagnostics = (results) => {
13960
14162
  for (const result of results) for (const d of result.diagnostics) if (d.redactSource || isSecretClassRule(d.rule)) {
@@ -14100,6 +14302,9 @@ const RULE_SCORE_IMPACTS = {
14100
14302
  "security/shell-injection": strict("Interpolated shell commands can become command execution."),
14101
14303
  "security/unsafe-deserialization": strict("Deserializing untrusted data with a legacy .NET formatter can become remote code execution."),
14102
14304
  "security/unsafe-c-call": strict("Unbounded C string and process calls can become buffer overflows or command execution."),
14305
+ "security/prompt-injection": strict("Dynamic user interpolation in system/developer prompts enables prompt injection attacks."),
14306
+ "security/unsandboxed-agent-tool": strict("Unsandboxed agent code execution or tool permission bypass allows arbitrary host commands."),
14307
+ "security/invisible-unicode-tags": strict("Invisible Unicode Tag characters (U+E0000-U+E007F) hide stealth prompt injection payloads."),
14103
14308
  "security/dependency-audit-skipped": advisory("An unavailable audit is visibility loss, not evidence of a vulnerability."),
14104
14309
  "dotnet/projects-skipped": advisory("Skipping unrestored C# projects is visibility loss, not evidence of a defect."),
14105
14310
  "cppcheck/chunks-skipped": advisory("A skipped cppcheck chunk is visibility loss, not evidence that the skipped files have a defect."),
@@ -14176,7 +14381,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
14176
14381
  const capScale = Math.max(1, fileCount.effectiveSourceFileCount / CAP_REFERENCE_FILES);
14177
14382
  const defaultRuleCap = typeof maxPerRule === "number" && maxPerRule > 0 ? maxPerRule : null;
14178
14383
  const capForRule = (key) => {
14179
- const ruleCap = scoreImpactForRule(key.slice(key.indexOf(":") + 1)).cap;
14384
+ const rule = key.slice(key.indexOf(":") + 1);
14385
+ const ruleCap = scoreImpactForRule(rule).cap;
14180
14386
  if (typeof ruleCap === "number") return defaultRuleCap ? Math.min(defaultRuleCap, ruleCap) : ruleCap;
14181
14387
  return defaultRuleCap;
14182
14388
  };
@@ -14289,8 +14495,10 @@ const detectSourceLanguages = (sourceFiles) => {
14289
14495
  const detectLanguages = (directory, sourceFiles) => {
14290
14496
  const languages = new Set(detectSourceLanguages(sourceFiles));
14291
14497
  for (const [file, lang] of Object.entries(LANGUAGE_SIGNALS)) if (fs.existsSync(path.join(directory, file))) languages.add(lang);
14292
- if (readPackageJson(path.join(directory, "package.json"))) if (fs.existsSync(path.join(directory, "tsconfig.json"))) languages.add("typescript");
14293
- else languages.add("javascript");
14498
+ if (readPackageJson(path.join(directory, "package.json"))) {
14499
+ if (fs.existsSync(path.join(directory, "tsconfig.json"))) languages.add("typescript");
14500
+ else languages.add("javascript");
14501
+ }
14294
14502
  for (const signal of PYTHON_SIGNALS) if (fs.existsSync(path.join(directory, signal))) {
14295
14503
  languages.add("python");
14296
14504
  break;
@@ -14481,14 +14689,19 @@ const hasExistingLiteralBackslashPath = (cwd, fingerprint) => {
14481
14689
  }
14482
14690
  };
14483
14691
  const migrateMarkerlessFingerprint = (cwd, fingerprint) => hasExistingLiteralBackslashPath(cwd, fingerprint) ? fingerprint : normalizeLegacyFingerprint(fingerprint);
14484
- const BASELINE_REL = path.join(".aislop", "baseline.json");
14485
- const baselinePath = (cwd) => path.join(cwd, BASELINE_REL);
14692
+ const baselinePath = (cwd) => {
14693
+ const raigalCandidate = path.join(cwd, ".raigal", "baseline.json");
14694
+ if (fs.existsSync(raigalCandidate)) return raigalCandidate;
14695
+ const raigalDir = path.join(cwd, ".raigal");
14696
+ if (fs.existsSync(raigalDir)) return raigalCandidate;
14697
+ return path.join(cwd, ".aislop", "baseline.json");
14698
+ };
14486
14699
  const readBaseline = (cwd) => {
14487
14700
  const raw = readIfExists(baselinePath(cwd));
14488
14701
  if (!raw) return null;
14489
14702
  try {
14490
14703
  const parsed = JSON.parse(raw);
14491
- if (parsed.schema !== "aislop.baseline.v2" && parsed.schema !== "aislop.baseline.v1") return null;
14704
+ if (parsed.schema !== "aislop.baseline.v2" && parsed.schema !== "aislop.baseline.v1" && parsed.schema !== "raigal.baseline.v2" && parsed.schema !== "raigal.baseline.v1") return null;
14492
14705
  const findingFingerprints = parsed.findingFingerprints ?? [];
14493
14706
  const needsWindowsMigration = parsed.pathFormat !== "posix";
14494
14707
  return {
@@ -14673,7 +14886,9 @@ const summariseDiagnostics = (diagnostics, rootDirectory) => {
14673
14886
  const runScan = async (cwd) => {
14674
14887
  const project = await discoverProject(cwd);
14675
14888
  const config = loadConfig(cwd);
14676
- const diagnostics = (await runEngines(buildEngineContext(project.rootDirectory, project, config), enabledEnginesFromConfig(config))).flatMap((r) => r.diagnostics);
14889
+ const context = buildEngineContext(project.rootDirectory, project, config);
14890
+ const enabled = enabledEnginesFromConfig(config);
14891
+ const diagnostics = (await runEngines(context, enabled)).flatMap((r) => r.diagnostics);
14677
14892
  const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
14678
14893
  const errorCount = diagnostics.filter((d) => d.severity === "error").length;
14679
14894
  const failBelow = config.ci.failBelow;
@@ -14688,12 +14903,11 @@ const runScan = async (cwd) => {
14688
14903
  }
14689
14904
  };
14690
14905
  };
14691
- const aislopScanInputSchema = z$1.object({ path: z$1.string().optional().describe("Project directory to scan, confined to the MCP server's cwd. Defaults to that cwd.") });
14692
- const aislopScanTool = {
14906
+ const raigalScanInputSchema = z$1.object({ path: z$1.string().optional().describe("Project directory to scan, confined to the MCP server's cwd. Defaults to that cwd.") });
14907
+ const raigalScanTool = {
14693
14908
  name: "raigal_scan",
14694
14909
  description: "Scan a project with Raigal. Runs the deterministic engines (format, lint, code-quality, ai-slop, security, architecture), returns a 0–100 score and the top findings. Use this before deciding whether the agent's recent changes are ready to ship.",
14695
- inputSchema: aislopScanInputSchema,
14696
- name: "aislop_scan"
14910
+ inputSchema: raigalScanInputSchema
14697
14911
  };
14698
14912
  const handleRaigalScan = async (input) => {
14699
14913
  const cwd = resolveCwd(input.path);
@@ -14708,7 +14922,7 @@ const handleRaigalScan = async (input) => {
14708
14922
  ...summary
14709
14923
  };
14710
14924
  };
14711
- const aislopFixInputSchema = z$1.object({
14925
+ const raigalFixInputSchema = z$1.object({
14712
14926
  path: z$1.string().optional().describe("Project directory to fix, confined to the MCP server's cwd. Defaults to that cwd."),
14713
14927
  force: z$1.boolean().optional().describe("Run aggressive fixes (dependency audit overrides, unused-file removal, framework alignment). Off by default; on means writes to package.json and may delete files.")
14714
14928
  });
@@ -14738,11 +14952,10 @@ const runRaigalFix = (cwd, force) => {
14738
14952
  }));
14739
14953
  });
14740
14954
  };
14741
- const aislopFixTool = {
14955
+ const raigalFixTool = {
14742
14956
  name: "raigal_fix",
14743
14957
  description: "Apply mechanical fixes (formatting, unused imports, narrative comments, duplicate imports, etc.). Returns counts before/after so the agent can see how many issues remain. Use BEFORE handing off to the agent — saves tokens by clearing what the CLI handles deterministically.",
14744
- inputSchema: aislopFixInputSchema,
14745
- name: "aislop_fix"
14958
+ inputSchema: raigalFixInputSchema
14746
14959
  };
14747
14960
  const handleRaigalFix = async (input) => {
14748
14961
  const cwd = resolveCwd(input.path);
@@ -14764,12 +14977,11 @@ const handleRaigalFix = async (input) => {
14764
14977
  elided: summary.elided
14765
14978
  };
14766
14979
  };
14767
- const aislopWhyInputSchema = z$1.object({ rule_id: z$1.string().describe("Full rule id (e.g. `ai-slop/narrative-comment`, `complexity/function-too-long`, `security/sql-injection`).") });
14768
- const aislopWhyTool = {
14980
+ const raigalWhyInputSchema = z$1.object({ rule_id: z$1.string().describe("Full rule id (e.g. `ai-slop/narrative-comment`, `complexity/function-too-long`, `security/sql-injection`).") });
14981
+ const raigalWhyTool = {
14769
14982
  name: "raigal_why",
14770
14983
  description: "Explain a Raigal rule: what it catches, why an AI agent typically produces it, severity, and whether it's auto-fixable. Use when a finding's message alone isn't enough to act on.",
14771
- inputSchema: aislopWhyInputSchema,
14772
- name: "aislop_why"
14984
+ inputSchema: raigalWhyInputSchema
14773
14985
  };
14774
14986
  const handleRaigalWhy = (input) => {
14775
14987
  const ruleId = input.rule_id.trim();
@@ -14782,12 +14994,11 @@ const handleRaigalWhy = (input) => {
14782
14994
  hint: "Run `raigal rules` for the full list of rules and their auto-fix status. The /patterns page has bad/good code examples for every named ai-slop pattern."
14783
14995
  };
14784
14996
  };
14785
- const aislopBaselineInputSchema = z$1.object({ path: z$1.string().optional().describe("Project directory, confined to the MCP server's cwd. Defaults to that cwd.") });
14786
- const aislopBaselineTool = {
14997
+ const raigalBaselineInputSchema = z$1.object({ path: z$1.string().optional().describe("Project directory, confined to the MCP server's cwd. Defaults to that cwd.") });
14998
+ const raigalBaselineTool = {
14787
14999
  name: "raigal_baseline",
14788
15000
  description: "Read the project's baseline (the last captured score the per-edit hook compares against). Returns score / lastScanAt / fileCount, or null if no baseline exists yet (run `raigal hook baseline` to capture).",
14789
- inputSchema: aislopBaselineInputSchema,
14790
- name: "aislop_baseline"
15001
+ inputSchema: raigalBaselineInputSchema
14791
15002
  };
14792
15003
  const handleRaigalBaseline = (input) => {
14793
15004
  const baseline = readBaseline(resolveCwd(input.path));
@@ -15057,10 +15268,11 @@ const track = (input) => {
15057
15268
  cachedInstallId = ensured.installId;
15058
15269
  installCreated = ensured.created;
15059
15270
  }
15060
- const { clean, dropped } = redactProperties({
15271
+ const merged = {
15061
15272
  ...baseProperties(cachedInstallId),
15062
15273
  ...input.properties
15063
- });
15274
+ };
15275
+ const { clean, dropped } = redactProperties(merged);
15064
15276
  if (isDebug()) {
15065
15277
  const compact = JSON.stringify({
15066
15278
  event: input.event,
@@ -15164,37 +15376,21 @@ const buildServer = () => {
15164
15376
  name: "raigal",
15165
15377
  version: APP_VERSION
15166
15378
  });
15167
- server.registerTool(aislopScanTool.name, {
15168
- description: aislopScanTool.description,
15169
- inputSchema: aislopScanInputSchema.shape
15170
- }, (input) => instrument("aislop_scan", () => handleRaigalScan(input)));
15171
- server.registerTool(aislopFixTool.name, {
15172
- description: aislopFixTool.description,
15173
- inputSchema: aislopFixInputSchema.shape
15174
- }, (input) => instrument("aislop_fix", () => handleRaigalFix(input)));
15175
- server.registerTool(aislopWhyTool.name, {
15176
- description: aislopWhyTool.description,
15177
- inputSchema: aislopWhyInputSchema.shape
15178
- }, (input) => instrument("aislop_why", () => handleRaigalWhy(input)));
15179
- server.registerTool(aislopBaselineTool.name, {
15180
- description: aislopBaselineTool.description,
15181
- inputSchema: aislopBaselineInputSchema.shape
15182
- }, (input) => instrument("aislop_baseline", () => handleRaigalBaseline(input)));
15183
- server.registerTool("raigal_scan", {
15184
- description: aislopScanTool.description,
15185
- inputSchema: aislopScanInputSchema.shape
15379
+ server.registerTool(raigalScanTool.name, {
15380
+ description: raigalScanTool.description,
15381
+ inputSchema: raigalScanInputSchema.shape
15186
15382
  }, (input) => instrument("raigal_scan", () => handleRaigalScan(input)));
15187
- server.registerTool("raigal_fix", {
15188
- description: aislopFixTool.description,
15189
- inputSchema: aislopFixInputSchema.shape
15383
+ server.registerTool(raigalFixTool.name, {
15384
+ description: raigalFixTool.description,
15385
+ inputSchema: raigalFixInputSchema.shape
15190
15386
  }, (input) => instrument("raigal_fix", () => handleRaigalFix(input)));
15191
- server.registerTool("raigal_why", {
15192
- description: aislopWhyTool.description,
15193
- inputSchema: aislopWhyInputSchema.shape
15387
+ server.registerTool(raigalWhyTool.name, {
15388
+ description: raigalWhyTool.description,
15389
+ inputSchema: raigalWhyInputSchema.shape
15194
15390
  }, (input) => instrument("raigal_why", () => handleRaigalWhy(input)));
15195
- server.registerTool("raigal_baseline", {
15196
- description: aislopBaselineTool.description,
15197
- inputSchema: aislopBaselineInputSchema.shape
15391
+ server.registerTool(raigalBaselineTool.name, {
15392
+ description: raigalBaselineTool.description,
15393
+ inputSchema: raigalBaselineInputSchema.shape
15198
15394
  }, (input) => instrument("raigal_baseline", () => handleRaigalBaseline(input)));
15199
15395
  return server;
15200
15396
  };