@methiu/raigal 0.2.0 → 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
@@ -12,7 +12,7 @@ import { createPublicKey, randomUUID, verify } from "node:crypto";
12
12
  import { spawn, spawnSync } from "node:child_process";
13
13
  import { z as z$1 } from "zod";
14
14
  import YAML, { parse } from "yaml";
15
- import micromatch from "micromatch";
15
+ import picomatch from "picomatch";
16
16
  import ts from "typescript";
17
17
  import { parse as parse$1 } from "smol-toml";
18
18
  //#region src/cloud/paths.ts
@@ -84,7 +84,7 @@ const getActiveToken = () => {
84
84
  };
85
85
  //#endregion
86
86
  //#region src/version.ts
87
- const APP_VERSION = "0.2.0";
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.0", 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);
@@ -473,6 +475,10 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
473
475
  nowSeconds: now,
474
476
  allowGraceSeconds: GRACE_PERIOD_SECONDS
475
477
  });
478
+ if (graceClaims.plan === "trial" && graceClaims.trial_ends_at && now > Math.floor(new Date(graceClaims.trial_ends_at).getTime() / 1e3)) {
479
+ clearCachedEntitlement();
480
+ throw new LicenceDeniedError("Organization trial has expired", "trial_expired");
481
+ }
476
482
  return {
477
483
  jwt: cached.entitlement,
478
484
  claims: graceClaims,
@@ -480,6 +486,7 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
480
486
  grace: now > graceClaims.exp
481
487
  };
482
488
  } catch (jwtErr) {
489
+ if (jwtErr instanceof LicenceDeniedError) throw jwtErr;
483
490
  if (jwtErr instanceof JwtVerificationError && jwtErr.code === "expired_beyond_grace") throw new LicenceUnverifiableError(`Raigal license could not be verified online and the offline grace period expired on ${(/* @__PURE__ */ new Date((cachedClaims?.exp ?? 0) * 1e3)).toISOString()}. Check network connectivity or Raigal Cloud status.`);
484
491
  }
485
492
  throw new LicenceUnverifiableError(`Raigal license could not be verified online (${err instanceof Error ? err.message : String(err)}). Check network connectivity or Raigal Cloud status.`);
@@ -494,7 +501,8 @@ const getEntitlement = async () => {
494
501
  } catch {
495
502
  cachedClaims = null;
496
503
  }
497
- if (cached && cachedClaims && now < cached.refresh_after) return {
504
+ const isTrialExpired = cachedClaims?.plan === "trial" && cachedClaims.trial_ends_at && now > Math.floor(new Date(cachedClaims.trial_ends_at).getTime() / 1e3);
505
+ if (cached && cachedClaims && !isTrialExpired && now < cached.refresh_after) return {
498
506
  jwt: cached.entitlement,
499
507
  claims: cachedClaims,
500
508
  fromCache: true
@@ -503,7 +511,7 @@ const getEntitlement = async () => {
503
511
  if (!activeToken) {
504
512
  oidcToken = await fetchGitHubOidcToken();
505
513
  if (!oidcToken) {
506
- if (cached && cachedClaims) return {
514
+ if (cached && cachedClaims && !isTrialExpired) return {
507
515
  jwt: cached.entitlement,
508
516
  claims: cachedClaims,
509
517
  fromCache: true
@@ -512,7 +520,8 @@ const getEntitlement = async () => {
512
520
  }
513
521
  } else oidcToken = await fetchGitHubOidcToken();
514
522
  try {
515
- const response = await fetchEntitlementFromApi(activeToken?.token ?? "", oidcToken);
523
+ const tokenToPass = activeToken?.token ?? "";
524
+ const response = await fetchEntitlementFromApi(tokenToPass, oidcToken);
516
525
  const claims = verifyEntitlementJwt(response.entitlement, { nowSeconds: now });
517
526
  saveCachedEntitlement(response.entitlement, response.refresh_after);
518
527
  return {
@@ -526,7 +535,7 @@ const getEntitlement = async () => {
526
535
  };
527
536
  //#endregion
528
537
  //#region src/cloud/owner.ts
529
- 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*$/;
530
539
  const parseGitRemote = (rawUrl) => {
531
540
  if (!rawUrl) return {
532
541
  owner: null,
@@ -664,6 +673,23 @@ const requireEntitlement = async (options = {}) => {
664
673
  try {
665
674
  const entitlement = await getEntitlement();
666
675
  const tokenKind = getActiveToken()?.kind ?? "session";
676
+ if (entitlement.claims.plan === "trial" && entitlement.claims.trial_ends_at) {
677
+ const trialEnd = new Date(entitlement.claims.trial_ends_at).getTime();
678
+ if (Date.now() > trialEnd) {
679
+ const message = "Raigal license denied: Organization trial has expired";
680
+ if (mode === "throw") throw new LicenceDeniedError(message, "trial_expired");
681
+ if (mode === "fatal") {
682
+ process.stderr.write(`${message}\n`);
683
+ process.exit(30);
684
+ }
685
+ process.stderr.write(`[raigal] ${message}\n`);
686
+ return {
687
+ ok: false,
688
+ exitCode: 30,
689
+ message
690
+ };
691
+ }
692
+ }
667
693
  const detectedOwner = detectLocalRepoOwner(directory);
668
694
  if (options.repoOwner) detectedOwner.owner = options.repoOwner.toLowerCase();
669
695
  const ownerCheck = checkOwnerAgainstAllowlist(detectedOwner, entitlement.claims.allowed_owners, tokenKind);
@@ -784,12 +810,13 @@ const DEFAULT_CONFIG = {
784
810
  format: "json"
785
811
  },
786
812
  telemetry: { enabled: true },
787
- rules: {}
813
+ rules: {},
814
+ overrides: []
788
815
  };
789
816
  //#endregion
790
817
  //#region src/config/extends.ts
791
818
  const MAX_DEPTH$1 = 5;
792
- const MAX_CONFIG_BYTES = 1024 * 1024;
819
+ const MAX_CONFIG_BYTES = 1048576;
793
820
  const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
794
821
  const deepMerge = (...sources) => {
795
822
  const result = {};
@@ -970,12 +997,25 @@ const CiSchema = z.object({
970
997
  format: z.enum(["json"]).default("json")
971
998
  }).strict();
972
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();
973
1003
  const RuleSeverityOverride = z.enum([
974
1004
  "error",
975
1005
  "warning",
976
1006
  "off"
977
1007
  ]);
978
1008
  const RulesSchema = z.record(z.string(), RuleSeverityOverride).default(() => ({}));
1009
+ const FileOverrideSchema = z.object({
1010
+ files: z.array(z.string().min(1)).min(1),
1011
+ quality: z.object({
1012
+ maxFunctionLoc: QualitySchema.shape.maxFunctionLoc.unwrap().optional(),
1013
+ maxFileLoc: QualitySchema.shape.maxFileLoc.unwrap().optional(),
1014
+ maxNesting: QualitySchema.shape.maxNesting.unwrap().optional(),
1015
+ maxParams: QualitySchema.shape.maxParams.unwrap().optional()
1016
+ }).optional(),
1017
+ rules: z.record(z.string(), RuleSeverityOverride).optional()
1018
+ });
979
1019
  const AislopConfigSchema = z.object({
980
1020
  version: z.number().default(1),
981
1021
  extends: z.union([z.string(), z.array(z.string())]).optional(),
@@ -1030,7 +1070,11 @@ const AislopConfigSchema = z.object({
1030
1070
  format: "json"
1031
1071
  })),
1032
1072
  telemetry: TelemetrySchema.default(() => ({ enabled: true })),
1073
+ baseline: BaselineConfigSchema.optional(),
1074
+ licensing: LicensingConfigSchema.optional(),
1075
+ workspace: WorkspaceConfigSchema.optional(),
1033
1076
  rules: RulesSchema,
1077
+ overrides: z.array(FileOverrideSchema).default(() => []),
1034
1078
  exclude: z.array(z.string()).default(() => [
1035
1079
  "node_modules",
1036
1080
  ".git",
@@ -1062,6 +1106,9 @@ const KNOWN_KEYS_BY_PATH = {
1062
1106
  "scoring",
1063
1107
  "ci",
1064
1108
  "telemetry",
1109
+ "baseline",
1110
+ "licensing",
1111
+ "workspace",
1065
1112
  "rules",
1066
1113
  "exclude",
1067
1114
  "include"
@@ -1112,7 +1159,10 @@ const KNOWN_KEYS_BY_PATH = {
1112
1159
  ],
1113
1160
  "scoring.thresholds": ["good", "ok"],
1114
1161
  ci: ["failBelow", "format"],
1115
- telemetry: ["enabled"]
1162
+ telemetry: ["enabled"],
1163
+ baseline: ["mode"],
1164
+ licensing: ["onLapse"],
1165
+ workspace: ["minimumFloor"]
1116
1166
  };
1117
1167
  const levenshtein = (a, b) => {
1118
1168
  const m = a.length;
@@ -1242,13 +1292,140 @@ const loadConfig = (directory) => {
1242
1292
  const configPath = findConfigFile(directory);
1243
1293
  if (!configPath) return DEFAULT_CONFIG;
1244
1294
  try {
1245
- return validateConfig(loadConfigChain(configPath), configPath);
1295
+ const merged = loadConfigChain(configPath);
1296
+ return validateConfig(merged, configPath);
1246
1297
  } catch (error) {
1247
1298
  if (error instanceof ConfigError) throw error;
1248
1299
  throw new ConfigError(configPath, `Failed to parse ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
1249
1300
  }
1250
1301
  };
1251
1302
  //#endregion
1303
+ //#region src/utils/globs.ts
1304
+ const MAX_BRACE_EXPANSIONS = 256;
1305
+ const braceListAt = (pattern, start) => {
1306
+ let depth = 0;
1307
+ const commas = [];
1308
+ for (let i = start; i < pattern.length; i += 1) {
1309
+ const char = pattern[i];
1310
+ if (char === "\\") i += 1;
1311
+ else if (char === "{") depth += 1;
1312
+ else if (char === ",") {
1313
+ if (depth === 1) commas.push(i);
1314
+ } else if (char === "}") {
1315
+ depth -= 1;
1316
+ if (depth === 0) return commas.length > 0 ? {
1317
+ start,
1318
+ end: i,
1319
+ commas
1320
+ } : null;
1321
+ }
1322
+ }
1323
+ return null;
1324
+ };
1325
+ const findBraceList = (pattern) => {
1326
+ for (let start = pattern.indexOf("{"); start !== -1; start = pattern.indexOf("{", start + 1)) {
1327
+ if (start > 0 && pattern[start - 1] === "\\") continue;
1328
+ const list = braceListAt(pattern, start);
1329
+ if (list) return list;
1330
+ }
1331
+ return null;
1332
+ };
1333
+ const expandInto = (pattern, results) => {
1334
+ const list = findBraceList(pattern);
1335
+ if (!list) {
1336
+ results.push(pattern);
1337
+ return results.length <= MAX_BRACE_EXPANSIONS;
1338
+ }
1339
+ const bounds = [
1340
+ list.start,
1341
+ ...list.commas,
1342
+ list.end
1343
+ ];
1344
+ const prefix = pattern.slice(0, list.start);
1345
+ const suffix = pattern.slice(list.end + 1);
1346
+ for (let i = 0; i < bounds.length - 1; i += 1) {
1347
+ const option = pattern.slice(bounds[i] + 1, bounds[i + 1]);
1348
+ if (!expandInto(`${prefix}${option}${suffix}`, results)) return false;
1349
+ }
1350
+ return true;
1351
+ };
1352
+ const expandBraceLists = (pattern) => {
1353
+ const results = [];
1354
+ return expandInto(pattern, results) ? results : [pattern];
1355
+ };
1356
+ const isNegated = (pattern) => picomatch.scan(pattern).negated;
1357
+ const matchesGlobList = (value, patterns, options = {}) => {
1358
+ const expanded = patterns.flatMap(expandBraceLists);
1359
+ const positives = expanded.filter((pattern) => !isNegated(pattern));
1360
+ const negatives = expanded.filter(isNegated).map((pattern) => pattern.slice(1));
1361
+ const included = positives.length === 0 || picomatch.isMatch(value, positives, options);
1362
+ const excluded = negatives.length > 0 && picomatch.isMatch(value, negatives, options);
1363
+ return included && !excluded;
1364
+ };
1365
+ //#endregion
1366
+ //#region src/scoring/rule-severity.ts
1367
+ /**
1368
+ * Apply per-rule severity overrides from config: "off" drops the diagnostic,
1369
+ * "error"/"warning" rewrite its severity before scoring and rendering.
1370
+ */
1371
+ const applyRuleSeverities = (diagnostics, overrides) => {
1372
+ if (Object.keys(overrides).length === 0) return diagnostics;
1373
+ const result = [];
1374
+ for (const diagnostic of diagnostics) {
1375
+ const override = overrides[diagnostic.rule];
1376
+ if (!override) {
1377
+ result.push(diagnostic);
1378
+ continue;
1379
+ }
1380
+ if (override === "off") continue;
1381
+ result.push(override === diagnostic.severity ? diagnostic : {
1382
+ ...diagnostic,
1383
+ severity: override
1384
+ });
1385
+ }
1386
+ return result;
1387
+ };
1388
+ //#endregion
1389
+ //#region src/config/overrides.ts
1390
+ const overrideRelativePath = (rootDirectory, filePath) => {
1391
+ const paths = path.win32.isAbsolute(rootDirectory) && !path.posix.isAbsolute(rootDirectory) ? path.win32 : path.posix;
1392
+ const root = rootDirectory.replaceAll("\\", "/");
1393
+ const file = filePath.replaceAll("\\", "/");
1394
+ return paths.normalize(paths.isAbsolute(file) ? paths.relative(root, file) : file).replaceAll("\\", "/");
1395
+ };
1396
+ const matchesEntry = (relativePath, patterns) => matchesGlobList(relativePath, patterns, { dot: true });
1397
+ const resolveFilePolicy = (config, relativePath) => {
1398
+ let quality = config.quality;
1399
+ let rules = config.rules ?? {};
1400
+ if (relativePath === ".." || relativePath.startsWith("../") || path.win32.isAbsolute(relativePath)) return {
1401
+ quality,
1402
+ rules
1403
+ };
1404
+ for (const entry of config.overrides ?? []) {
1405
+ if (!matchesEntry(relativePath, entry.files)) continue;
1406
+ if (entry.quality) quality = {
1407
+ ...quality,
1408
+ ...entry.quality
1409
+ };
1410
+ if (entry.rules) rules = {
1411
+ ...rules,
1412
+ ...entry.rules
1413
+ };
1414
+ }
1415
+ return {
1416
+ quality,
1417
+ rules
1418
+ };
1419
+ };
1420
+ const applyFileOverrides = (diagnostics, context) => {
1421
+ if (!context.config.overrides?.length) return diagnostics;
1422
+ return diagnostics.flatMap((diagnostic) => {
1423
+ const relativePath = overrideRelativePath(context.rootDirectory, diagnostic.filePath);
1424
+ const { rules } = resolveFilePolicy(context.config, relativePath);
1425
+ return applyRuleSeverities([diagnostic], rules);
1426
+ });
1427
+ };
1428
+ //#endregion
1252
1429
  //#region src/utils/paths.ts
1253
1430
  /** Normalize an OS path to forward-slash (POSIX) separators. */
1254
1431
  const toPosix = (p) => p.split(path.sep).join("/");
@@ -1275,13 +1452,13 @@ const normalizeExcludePatterns = (patterns) => patterns.flatMap((pattern) => {
1275
1452
  const normalized = withoutProjectPrefix.slice(0, end);
1276
1453
  if (normalized.length === 0) return [];
1277
1454
  if (normalized.length > 256) return [];
1278
- if (micromatch.scan(normalized).isGlob) return [normalized];
1455
+ if (picomatch.scan(normalized).isGlob) return [normalized];
1279
1456
  if (normalized.startsWith(".") && !normalized.includes("/")) return supportedGlobPatterns([`**/${normalized}`, `**/${normalized}/**`]);
1280
1457
  return supportedGlobPatterns([normalized, `${normalized}/**`]);
1281
1458
  });
1282
1459
  const isPathExcluded = (relativePath, normalizedPatterns) => {
1283
1460
  if (normalizedPatterns.length === 0) return false;
1284
- return micromatch.isMatch(relativePath, normalizedPatterns, { dot: true });
1461
+ return picomatch.isMatch(relativePath, normalizedPatterns, { dot: true });
1285
1462
  };
1286
1463
  const filterExcludedDiagnostics = (diagnostics, rootDirectory, excludePatterns) => {
1287
1464
  if (!excludePatterns || excludePatterns.length === 0) return diagnostics;
@@ -1294,7 +1471,7 @@ const filterExcludedDiagnostics = (diagnostics, rootDirectory, excludePatterns)
1294
1471
  };
1295
1472
  //#endregion
1296
1473
  //#region src/utils/project-path-safety.ts
1297
- const MAX_PROJECT_CONFIG_BYTES = 1024 * 1024;
1474
+ const MAX_PROJECT_CONFIG_BYTES = 1048576;
1298
1475
  const isWithinDirectory$2 = (rootDirectory, candidate) => {
1299
1476
  const relative = path.relative(rootDirectory, candidate);
1300
1477
  return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
@@ -1381,7 +1558,7 @@ const isAutoGenerated = (filePath) => {
1381
1558
  };
1382
1559
  //#endregion
1383
1560
  //#region src/utils/git-ignore.ts
1384
- const MAX_BUFFER$1 = 50 * 1024 * 1024;
1561
+ const MAX_BUFFER$1 = 52428800;
1385
1562
  const NOT_IGNORED_ARGUMENTS = [
1386
1563
  "ls-files",
1387
1564
  "--cached",
@@ -1492,7 +1669,7 @@ const dropGitIgnoredPaths = (rootDirectory, absolutePaths) => {
1492
1669
  };
1493
1670
  //#endregion
1494
1671
  //#region src/utils/project-file-list.ts
1495
- const MAX_BUFFER = 50 * 1024 * 1024;
1672
+ const MAX_BUFFER = 52428800;
1496
1673
  const normalizePruneDirectories = (directories) => new Set([...directories].map((directory) => directory.toLowerCase()));
1497
1674
  const isInPrunedDirectory = (filePath, pruneDirectories) => {
1498
1675
  const pathSegments = filePath.split("/");
@@ -1703,12 +1880,13 @@ const normalizeIncludePatterns = (patterns) => patterns.flatMap((pattern) => {
1703
1880
  const normalized = pattern.trim().replace(/^\.\//, "").replace(/\/$/, "");
1704
1881
  if (normalized === "" || normalized === ".") return ["**"];
1705
1882
  if (normalized.length > 256) return [];
1706
- if (micromatch.scan(normalized).isGlob) return [normalized];
1883
+ if (picomatch.scan(normalized).isGlob) return [normalized];
1707
1884
  return supportedGlobPatterns([normalized, `${normalized}/**`]);
1708
1885
  });
1709
1886
  const createPathMatcher = (patterns) => {
1710
- const matchers = patterns.map((pattern) => micromatch.matcher(pattern, { dot: true }));
1711
- return (filePath) => matchers.some((matches) => matches(filePath));
1887
+ if (patterns.length === 0) return () => false;
1888
+ const isMatch = picomatch(patterns, { dot: true });
1889
+ return (filePath) => isMatch(filePath);
1712
1890
  };
1713
1891
  const filterFiles = (rootDirectory, files, options) => {
1714
1892
  const extraExtensions = new Set(options.extraExtensions);
@@ -2405,7 +2583,7 @@ const tokenizeJs = (content, ext = ".ts") => {
2405
2583
  };
2406
2584
  //#endregion
2407
2585
  //#region src/utils/source-masker.ts
2408
- const JS_EXTS$2 = /* @__PURE__ */ new Set([
2586
+ const JS_EXTS$3 = /* @__PURE__ */ new Set([
2409
2587
  ".ts",
2410
2588
  ".tsx",
2411
2589
  ".js",
@@ -2413,7 +2591,7 @@ const JS_EXTS$2 = /* @__PURE__ */ new Set([
2413
2591
  ".mjs",
2414
2592
  ".cjs"
2415
2593
  ]);
2416
- const PY_EXTS = /* @__PURE__ */ new Set([".py"]);
2594
+ const PY_EXTS$1 = /* @__PURE__ */ new Set([".py"]);
2417
2595
  const RB_EXTS = /* @__PURE__ */ new Set([".rb"]);
2418
2596
  const PHP_EXTS = /* @__PURE__ */ new Set([".php"]);
2419
2597
  const CSHARP_EXTS = /* @__PURE__ */ new Set([".cs"]);
@@ -2437,8 +2615,8 @@ const C_STYLE_COMMENT_EXTS = /* @__PURE__ */ new Set([
2437
2615
  ".zig"
2438
2616
  ]);
2439
2617
  const familyForExt = (ext) => {
2440
- if (JS_EXTS$2.has(ext)) return "js";
2441
- 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";
2442
2620
  if (RB_EXTS.has(ext)) return "rb";
2443
2621
  if (PHP_EXTS.has(ext)) return "php";
2444
2622
  if (CSHARP_EXTS.has(ext)) return "csharp";
@@ -2670,7 +2848,7 @@ const getCommentLines = (content, ext) => {
2670
2848
  };
2671
2849
  //#endregion
2672
2850
  //#region src/engines/ai-slop/abstractions.ts
2673
- const JS_EXTS$1 = /* @__PURE__ */ new Set([
2851
+ const JS_EXTS$2 = /* @__PURE__ */ new Set([
2674
2852
  ".ts",
2675
2853
  ".tsx",
2676
2854
  ".js",
@@ -2681,11 +2859,11 @@ const JS_EXTS$1 = /* @__PURE__ */ new Set([
2681
2859
  const THIN_WRAPPER_PATTERNS = [
2682
2860
  {
2683
2861
  pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
2684
- extensions: JS_EXTS$1
2862
+ extensions: JS_EXTS$2
2685
2863
  },
2686
2864
  {
2687
2865
  pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
2688
- extensions: JS_EXTS$1
2866
+ extensions: JS_EXTS$2
2689
2867
  },
2690
2868
  {
2691
2869
  pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
@@ -3933,7 +4111,7 @@ const scriptKindFor$1 = (ext) => {
3933
4111
  }
3934
4112
  };
3935
4113
  const lineFor$1 = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
3936
- const makeDiagnostic = (filePath, line, rule, message, help) => ({
4114
+ const makeDiagnostic$1 = (filePath, line, rule, message, help) => ({
3937
4115
  filePath,
3938
4116
  engine: "ai-slop",
3939
4117
  rule,
@@ -3983,7 +4161,7 @@ const detectRedundantTryCatch = (sourceFile, relativePath) => {
3983
4161
  if (ts.isTryStatement(node) && node.catchClause && !node.finallyBlock) {
3984
4162
  const catchNameNode = node.catchClause.variableDeclaration?.name;
3985
4163
  const [onlyStatement] = node.catchClause.block.statements;
3986
- 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."));
3987
4165
  }
3988
4166
  ts.forEachChild(node, visit);
3989
4167
  };
@@ -4001,7 +4179,7 @@ const detectPrimitiveCoercions = (sourceFile, relativePath) => {
4001
4179
  const [arg] = child.arguments;
4002
4180
  if (arg && ts.isIdentifier(arg)) {
4003
4181
  const primitive = params.get(arg.text);
4004
- 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."));
4005
4183
  }
4006
4184
  }
4007
4185
  ts.forEachChild(child, visitBody);
@@ -4044,7 +4222,7 @@ const detectDuplicateExportedTypes = (parsedSources) => {
4044
4222
  continue;
4045
4223
  }
4046
4224
  if (previous.filePath === declaration.filePath) continue;
4047
- 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.`));
4048
4226
  }
4049
4227
  }
4050
4228
  return diagnostics;
@@ -4146,7 +4324,7 @@ const detectDuplicateImports = async (context) => {
4146
4324
  rule: "ai-slop/duplicate-import",
4147
4325
  severity: "warning",
4148
4326
  message: `"${spec}" is also imported on line ${firstLine}. Merge into a single import statement.`,
4149
- 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.",
4150
4328
  line: dup.line,
4151
4329
  column: 1,
4152
4330
  category: "AI Slop",
@@ -5854,7 +6032,8 @@ const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith
5854
6032
  const RUNTIME_BUILTINS = /* @__PURE__ */ new Set(["bun"]);
5855
6033
  const isJsBuiltin = (spec) => {
5856
6034
  if (RUNTIME_BUILTINS.has(spec)) return true;
5857
- 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);
5858
6037
  };
5859
6038
  const VIRTUAL_MODULE_PREFIXES = [
5860
6039
  "astro:",
@@ -6066,7 +6245,8 @@ const isLocalAliasReplacement = (rawValue, configDirectory, rootDirectory) => {
6066
6245
  if (!target) return false;
6067
6246
  if (!target.startsWith(".") && !target.startsWith("/") && !target.startsWith("~/") && !target.startsWith("@/")) return false;
6068
6247
  const normalized = target.startsWith("~/") || target.startsWith("@/") ? target.slice(2) : target;
6069
- 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);
6070
6250
  };
6071
6251
  const hasValidPackageImportTarget = (value, packageDirectory, rootDirectory) => {
6072
6252
  if (!isRootBoundedTarget(packageDirectory, rootDirectory)) return false;
@@ -7186,7 +7366,9 @@ const isRustDocCommentLine = (line) => {
7186
7366
  };
7187
7367
  const collectBlocks = (sourceLines, syntax, content, ext) => {
7188
7368
  const blocks = [];
7189
- 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);
7190
7372
  let i = 0;
7191
7373
  while (i < sourceLines.length) {
7192
7374
  const line = sourceLines[i];
@@ -7514,7 +7696,8 @@ const detectNarrativeComments = async (context) => {
7514
7696
  } catch {
7515
7697
  continue;
7516
7698
  }
7517
- const blocks = collectBlocks(content.split("\n"), syntax, content, ext);
7699
+ const lines = content.split("\n");
7700
+ const blocks = collectBlocks(lines, syntax, content, ext);
7518
7701
  for (const block of blocks) {
7519
7702
  const { matched, reason } = detectNarrativeInBlock(block, ext);
7520
7703
  if (!matched) continue;
@@ -7986,7 +8169,7 @@ const detectRustPatterns = async (context) => {
7986
8169
  };
7987
8170
  //#endregion
7988
8171
  //#region src/engines/ai-slop/silent-recovery.ts
7989
- const JS_EXTS = /* @__PURE__ */ new Set([
8172
+ const JS_EXTS$1 = /* @__PURE__ */ new Set([
7990
8173
  ".ts",
7991
8174
  ".tsx",
7992
8175
  ".js",
@@ -8181,7 +8364,7 @@ const detectSilentRecovery = async (context) => {
8181
8364
  for (const filePath of files) {
8182
8365
  if (isAutoGenerated(filePath)) continue;
8183
8366
  const ext = path.extname(filePath);
8184
- const isJs = JS_EXTS.has(ext);
8367
+ const isJs = JS_EXTS$1.has(ext);
8185
8368
  if (!isJs && !(ext === ".py")) continue;
8186
8369
  const relPath = relativePosix(context.rootDirectory, filePath);
8187
8370
  if (isNonProductionPath(relPath)) continue;
@@ -8466,7 +8649,7 @@ const detectUnusedCss = async (context) => {
8466
8649
  rule: "ai-slop/unused-css",
8467
8650
  severity: "warning",
8468
8651
  message: `CSS class '.${def.name}' is defined but never referenced in the codebase`,
8469
- 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)",
8470
8653
  line: def.line,
8471
8654
  column: 0,
8472
8655
  category: "AI Slop",
@@ -9466,7 +9649,8 @@ const checkFunctionDiagnostics = (relativePath, fn, limits, ext) => {
9466
9649
  const results = [];
9467
9650
  const multiplier = functionLocMultiplier(fn, ext);
9468
9651
  const fnMax = functionLocBudget(fn, ext, limits.maxFunctionLoc);
9469
- 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({
9470
9654
  filePath: relativePath,
9471
9655
  engine: "code-quality",
9472
9656
  rule: "complexity/function-too-long",
@@ -9523,10 +9707,10 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
9523
9707
  };
9524
9708
  const checkComplexity = async (context) => {
9525
9709
  const files = getSourceFiles(context);
9526
- const limits = context.config.quality;
9527
9710
  const diagnostics = [];
9528
9711
  for (const filePath of files) {
9529
9712
  if (isAutoGenerated(filePath)) continue;
9713
+ const { quality: limits } = resolveFilePolicy(context.config, overrideRelativePath(context.rootDirectory, filePath));
9530
9714
  diagnostics.push(...checkFileComplexity(filePath, context.rootDirectory, limits));
9531
9715
  }
9532
9716
  return diagnostics;
@@ -9556,14 +9740,14 @@ const isTrivialLine = (line) => {
9556
9740
  };
9557
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)=/;
9558
9742
  const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
9559
- const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
9560
- 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/;
9561
9745
  const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
9562
9746
  const isLowSignalMarkupWindow = (lines) => {
9563
9747
  return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
9564
9748
  };
9565
9749
  const isLowSignalDataWindow = (lines) => {
9566
- 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;
9567
9751
  };
9568
9752
  const findSuppressedLines = (lines) => {
9569
9753
  const suppressed = /* @__PURE__ */ new Set();
@@ -9595,7 +9779,7 @@ const collectMeaningfulLines = (content) => {
9595
9779
  if (isLowSignalDataWindow(window)) continue;
9596
9780
  if (window.every(isTrivialLine)) continue;
9597
9781
  const normalised = window.map(normaliseLine);
9598
- 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;
9599
9783
  if (new Set(normalised).size < MIN_DISTINCT_LINES) continue;
9600
9784
  hits.push({
9601
9785
  startLine: i + 1,
@@ -9883,8 +10067,8 @@ const shouldIncludeIssue = (issueType, filePath) => {
9883
10067
  return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
9884
10068
  };
9885
10069
  const DEPENDENCY_HELP = {
9886
- dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
9887
- 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`.",
9888
10072
  unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
9889
10073
  unresolved: "This import cannot be resolved. Check for typos or missing packages.",
9890
10074
  binaries: "This binary is used but its package is not in package.json."
@@ -10516,13 +10700,23 @@ const withReportPath = async (run) => {
10516
10700
  };
10517
10701
  const UNSUPPORTED_GLOB_SYNTAX = /[?[\]{}!|()\\]/;
10518
10702
  const isDotnetFormatPath = (pattern) => !pattern.startsWith("-") && !UNSUPPORTED_GLOB_SYNTAX.test(pattern);
10703
+ const expandBraces = (pattern, depth = 0) => {
10704
+ if (depth > 10) return [pattern];
10705
+ const match = /\{([^{}]+)\}/.exec(pattern);
10706
+ if (!match) return [pattern];
10707
+ const [fullMatch, options] = match;
10708
+ const parts = options.split(",");
10709
+ const prefix = pattern.slice(0, match.index);
10710
+ const suffix = pattern.slice(match.index + fullMatch.length);
10711
+ return parts.flatMap((part) => expandBraces(`${prefix}${part}${suffix}`, depth + 1));
10712
+ };
10519
10713
  const buildDotnetFormatExcludeScope = (excludePatterns) => {
10520
10714
  const scope = {
10521
10715
  excludeArguments: [],
10522
10716
  unsupportedPatterns: []
10523
10717
  };
10524
10718
  for (const pattern of normalizeExcludePatterns(excludePatterns ?? [])) {
10525
- const expanded = micromatch.braces(pattern, { expand: true });
10719
+ const expanded = expandBraces(pattern);
10526
10720
  if (expanded.every(isDotnetFormatPath)) scope.excludeArguments.push(...expanded);
10527
10721
  else scope.unsupportedPatterns.push(pattern);
10528
10722
  }
@@ -10716,7 +10910,7 @@ const runGofmt = async (context) => {
10716
10910
  rule: "go-formatting",
10717
10911
  severity: "warning",
10718
10912
  message: "Go file is not formatted correctly",
10719
- help: "Run `aislop fix` to auto-format with gofmt",
10913
+ help: "Run `raigal fix` to auto-format with gofmt",
10720
10914
  line: 0,
10721
10915
  column: 0,
10722
10916
  category: "Format",
@@ -10735,7 +10929,8 @@ const resolvePackageRoot = (startFile) => {
10735
10929
  while (true) {
10736
10930
  const packageJsonPath = path.join(current, "package.json");
10737
10931
  if (fs.existsSync(packageJsonPath)) try {
10738
- 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;
10739
10934
  } catch {}
10740
10935
  const parent = path.dirname(current);
10741
10936
  if (parent === current) break;
@@ -10846,7 +11041,7 @@ const parseRuffFormatOutput = (output, rootDir) => {
10846
11041
  rule: "python-formatting",
10847
11042
  severity: "warning",
10848
11043
  message: "Python file is not formatted correctly",
10849
- help: "Run `aislop fix` to auto-format with ruff",
11044
+ help: "Run `raigal fix` to auto-format with ruff",
10850
11045
  line: 0,
10851
11046
  column: 0,
10852
11047
  category: "Format",
@@ -10919,7 +11114,8 @@ const parseClangTidyOutput = (output, rootDirectory) => {
10919
11114
  const runClangTidy = async (context) => {
10920
11115
  const compileCommandsDir = findCompileCommandsDir(context);
10921
11116
  if (!compileCommandsDir) return [];
10922
- 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));
10923
11119
  if (sources.length === 0) return [];
10924
11120
  const diagnostics = [];
10925
11121
  for (const chunk of chunkFilePaths(sources, CLANG_TIDY_CHUNK_MAX_FILES)) try {
@@ -11289,7 +11485,7 @@ const runExpoDoctor = async (context) => {
11289
11485
  rule: "expo-doctor/config-error",
11290
11486
  severity: "warning",
11291
11487
  message: configError,
11292
- help: "Install project dependencies, then re-run `aislop scan`.",
11488
+ help: "Install project dependencies, then re-run `raigal scan`.",
11293
11489
  line: 0,
11294
11490
  column: 0,
11295
11491
  category: "Expo",
@@ -12107,7 +12303,7 @@ const normalizeEslintIgnorePatterns = (patterns) => patterns.map((pattern) => pa
12107
12303
  if (!pattern.includes("*") && !path.extname(pattern)) return [`${pattern}/**`];
12108
12304
  return [pattern];
12109
12305
  });
12110
- const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 && micromatch.isMatch(relativePath, patterns, { dot: true });
12306
+ const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 && picomatch.isMatch(relativePath, patterns, { dot: true });
12111
12307
  const getOxlintTargets = (context) => {
12112
12308
  const eslintIgnorePatterns = normalizeEslintIgnorePatterns(collectEslintIgnorePatterns(context.rootDirectory));
12113
12309
  return 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("/")).filter((filePath) => !isEslintIgnored(filePath, eslintIgnorePatterns));
@@ -12253,7 +12449,8 @@ const resolveTrustedTscPath = () => {
12253
12449
  };
12254
12450
  const isReferenceOnlyConfig = (tsconfigPath) => {
12255
12451
  try {
12256
- const parsed = parseJsonc(fs.readFileSync(tsconfigPath, "utf-8"));
12452
+ const raw = fs.readFileSync(tsconfigPath, "utf-8");
12453
+ const parsed = parseJsonc(raw);
12257
12454
  if (!parsed) return false;
12258
12455
  return Array.isArray(parsed.references) && !parsed.files && !parsed.include && !parsed.extends;
12259
12456
  } catch {
@@ -12396,6 +12593,174 @@ const lintEngine = {
12396
12593
  }
12397
12594
  };
12398
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
12399
12764
  //#region src/ui/invocation.ts
12400
12765
  const detectInvocation = () => "raigal";
12401
12766
  //#endregion
@@ -12778,7 +13143,7 @@ const parseJsAudit = (output, source, manifest) => {
12778
13143
  rule: "security/dependency-audit-skipped",
12779
13144
  severity: "info",
12780
13145
  message: `Dependency audit skipped (${source}): lockfile is missing`,
12781
- 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.",
12782
13147
  line: 0,
12783
13148
  column: 0,
12784
13149
  category: "Security",
@@ -12889,7 +13254,8 @@ const readRuntimeDependencies = (rootDir) => {
12889
13254
  collectRuntimeNames(rootManifest, names);
12890
13255
  const globs = [...npmWorkspaceGlobs(rootManifest), ...pnpmWorkspaceGlobs(rootDir)];
12891
13256
  if (globs.length === 0) return { runtimeDependencies: names };
12892
- for (const relativeDir of micromatch(listPackageDirectories(rootDir), globs)) {
13257
+ const isMatch = picomatch(globs, { dot: true });
13258
+ for (const relativeDir of listPackageDirectories(rootDir).filter((dir) => isMatch(dir))) {
12893
13259
  const memberManifest = readJsonFile(path.join(rootDir, relativeDir, "package.json"));
12894
13260
  if (memberManifest) collectRuntimeNames(memberManifest, names);
12895
13261
  }
@@ -12951,10 +13317,11 @@ const auditSkippedDiagnostic = (source, help) => ({
12951
13317
  });
12952
13318
  const runNpmAudit = async (rootDir, timeout) => {
12953
13319
  try {
12954
- return parseJsAudit((await runSubprocess("npm", ["audit", "--json"], {
13320
+ const result = await runSubprocess("npm", ["audit", "--json"], {
12955
13321
  cwd: rootDir,
12956
13322
  timeout
12957
- })).stdout, "npm audit", readRuntimeDependencies(rootDir));
13323
+ });
13324
+ return parseJsAudit(result.stdout, "npm audit", readRuntimeDependencies(rootDir));
12958
13325
  } catch (error) {
12959
13326
  return [auditSkippedDiagnostic("npm audit", `Failed to run npm audit: ${errorMessageOf(error)}`)];
12960
13327
  }
@@ -12975,10 +13342,11 @@ const runBunAudit = async (rootDir, timeout) => {
12975
13342
  const runPnpmAuditWithFallback = async (rootDir, timeout) => {
12976
13343
  const canFallbackToNpm = fs.existsSync(path.join(rootDir, "package-lock.json"));
12977
13344
  try {
12978
- const diagnostics = parseJsAudit((await runSubprocess("pnpm", ["audit", "--json"], {
13345
+ const result = await runSubprocess("pnpm", ["audit", "--json"], {
12979
13346
  cwd: rootDir,
12980
13347
  timeout
12981
- })).stdout, "pnpm audit", readRuntimeDependencies(rootDir));
13348
+ });
13349
+ const diagnostics = parseJsAudit(result.stdout, "pnpm audit", readRuntimeDependencies(rootDir));
12982
13350
  if (diagnostics.some((d) => d.rule === "security/dependency-audit-skipped")) {
12983
13351
  if (canFallbackToNpm) return runNpmAudit(rootDir, timeout);
12984
13352
  return diagnostics;
@@ -13335,7 +13703,7 @@ const RISKY_PATTERNS = [
13335
13703
  //#region src/engines/security/risky.ts
13336
13704
  const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
13337
13705
  const start = Math.max(0, lineIndex - 2);
13338
- 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));
13339
13707
  };
13340
13708
  const isStructuredDataScript = (content, matchIndex) => {
13341
13709
  const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
@@ -13657,7 +14025,11 @@ const securityEngine = {
13657
14025
  name: "security",
13658
14026
  async run(context) {
13659
14027
  const diagnostics = [];
13660
- const promises = [scanSecrets(context), detectRiskyConstructs(context)];
14028
+ const promises = [
14029
+ scanSecrets(context),
14030
+ detectRiskyConstructs(context),
14031
+ scanAiSecurity(context)
14032
+ ];
13661
14033
  if (context.config.security.audit) promises.push(runDependencyAudit(context));
13662
14034
  const results = await Promise.allSettled(promises);
13663
14035
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -13758,6 +14130,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
13758
14130
  const result = await engine.run(context);
13759
14131
  result.diagnostics = filterExcludedDiagnostics(result.diagnostics, context.rootDirectory, context.excludePatterns);
13760
14132
  result.elapsed = performance.now() - start;
14133
+ result.diagnostics = applyFileOverrides(result.diagnostics, context);
13761
14134
  onComplete?.(result);
13762
14135
  return result;
13763
14136
  } catch (error) {
@@ -13766,6 +14139,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
13766
14139
  diagnostics: [],
13767
14140
  elapsed: performance.now() - start,
13768
14141
  skipped: true,
14142
+ failed: true,
13769
14143
  skipReason: error instanceof Error ? error.message : String(error)
13770
14144
  };
13771
14145
  onComplete?.(result);
@@ -13776,9 +14150,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
13776
14150
  diagnostics: [],
13777
14151
  elapsed: 0,
13778
14152
  skipped: true,
14153
+ failed: true,
13779
14154
  skipReason: r.reason instanceof Error ? r.reason.message : String(r.reason)
13780
14155
  });
13781
- 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);
13782
14160
  };
13783
14161
  const stripSecretDiagnostics = (results) => {
13784
14162
  for (const result of results) for (const d of result.diagnostics) if (d.redactSource || isSecretClassRule(d.rule)) {
@@ -13924,6 +14302,9 @@ const RULE_SCORE_IMPACTS = {
13924
14302
  "security/shell-injection": strict("Interpolated shell commands can become command execution."),
13925
14303
  "security/unsafe-deserialization": strict("Deserializing untrusted data with a legacy .NET formatter can become remote code execution."),
13926
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."),
13927
14308
  "security/dependency-audit-skipped": advisory("An unavailable audit is visibility loss, not evidence of a vulnerability."),
13928
14309
  "dotnet/projects-skipped": advisory("Skipping unrestored C# projects is visibility loss, not evidence of a defect."),
13929
14310
  "cppcheck/chunks-skipped": advisory("A skipped cppcheck chunk is visibility loss, not evidence that the skipped files have a defect."),
@@ -14000,7 +14381,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
14000
14381
  const capScale = Math.max(1, fileCount.effectiveSourceFileCount / CAP_REFERENCE_FILES);
14001
14382
  const defaultRuleCap = typeof maxPerRule === "number" && maxPerRule > 0 ? maxPerRule : null;
14002
14383
  const capForRule = (key) => {
14003
- const ruleCap = scoreImpactForRule(key.slice(key.indexOf(":") + 1)).cap;
14384
+ const rule = key.slice(key.indexOf(":") + 1);
14385
+ const ruleCap = scoreImpactForRule(rule).cap;
14004
14386
  if (typeof ruleCap === "number") return defaultRuleCap ? Math.min(defaultRuleCap, ruleCap) : ruleCap;
14005
14387
  return defaultRuleCap;
14006
14388
  };
@@ -14113,8 +14495,10 @@ const detectSourceLanguages = (sourceFiles) => {
14113
14495
  const detectLanguages = (directory, sourceFiles) => {
14114
14496
  const languages = new Set(detectSourceLanguages(sourceFiles));
14115
14497
  for (const [file, lang] of Object.entries(LANGUAGE_SIGNALS)) if (fs.existsSync(path.join(directory, file))) languages.add(lang);
14116
- if (readPackageJson(path.join(directory, "package.json"))) if (fs.existsSync(path.join(directory, "tsconfig.json"))) languages.add("typescript");
14117
- 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
+ }
14118
14502
  for (const signal of PYTHON_SIGNALS) if (fs.existsSync(path.join(directory, signal))) {
14119
14503
  languages.add("python");
14120
14504
  break;
@@ -14305,14 +14689,19 @@ const hasExistingLiteralBackslashPath = (cwd, fingerprint) => {
14305
14689
  }
14306
14690
  };
14307
14691
  const migrateMarkerlessFingerprint = (cwd, fingerprint) => hasExistingLiteralBackslashPath(cwd, fingerprint) ? fingerprint : normalizeLegacyFingerprint(fingerprint);
14308
- const BASELINE_REL = path.join(".aislop", "baseline.json");
14309
- 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
+ };
14310
14699
  const readBaseline = (cwd) => {
14311
14700
  const raw = readIfExists(baselinePath(cwd));
14312
14701
  if (!raw) return null;
14313
14702
  try {
14314
14703
  const parsed = JSON.parse(raw);
14315
- 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;
14316
14705
  const findingFingerprints = parsed.findingFingerprints ?? [];
14317
14706
  const needsWindowsMigration = parsed.pathFormat !== "posix";
14318
14707
  return {
@@ -14449,6 +14838,8 @@ const buildEngineContext = (rootDirectory, project, config) => {
14449
14838
  frameworks: project.frameworks,
14450
14839
  installedTools: project.installedTools,
14451
14840
  config: {
14841
+ overrides: config.overrides,
14842
+ rules: config.rules,
14452
14843
  quality: config.quality,
14453
14844
  security: config.security,
14454
14845
  lint: config.lint,
@@ -14495,7 +14886,9 @@ const summariseDiagnostics = (diagnostics, rootDirectory) => {
14495
14886
  const runScan = async (cwd) => {
14496
14887
  const project = await discoverProject(cwd);
14497
14888
  const config = loadConfig(cwd);
14498
- 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);
14499
14892
  const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
14500
14893
  const errorCount = diagnostics.filter((d) => d.severity === "error").length;
14501
14894
  const failBelow = config.ci.failBelow;
@@ -14510,12 +14903,11 @@ const runScan = async (cwd) => {
14510
14903
  }
14511
14904
  };
14512
14905
  };
14513
- 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.") });
14514
- 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 = {
14515
14908
  name: "raigal_scan",
14516
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.",
14517
- inputSchema: aislopScanInputSchema,
14518
- name: "aislop_scan"
14910
+ inputSchema: raigalScanInputSchema
14519
14911
  };
14520
14912
  const handleRaigalScan = async (input) => {
14521
14913
  const cwd = resolveCwd(input.path);
@@ -14530,8 +14922,7 @@ const handleRaigalScan = async (input) => {
14530
14922
  ...summary
14531
14923
  };
14532
14924
  };
14533
- const handleAislopScan = handleRaigalScan;
14534
- const aislopFixInputSchema = z$1.object({
14925
+ const raigalFixInputSchema = z$1.object({
14535
14926
  path: z$1.string().optional().describe("Project directory to fix, confined to the MCP server's cwd. Defaults to that cwd."),
14536
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.")
14537
14928
  });
@@ -14561,11 +14952,10 @@ const runRaigalFix = (cwd, force) => {
14561
14952
  }));
14562
14953
  });
14563
14954
  };
14564
- const aislopFixTool = {
14955
+ const raigalFixTool = {
14565
14956
  name: "raigal_fix",
14566
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.",
14567
- inputSchema: aislopFixInputSchema,
14568
- name: "aislop_fix"
14958
+ inputSchema: raigalFixInputSchema
14569
14959
  };
14570
14960
  const handleRaigalFix = async (input) => {
14571
14961
  const cwd = resolveCwd(input.path);
@@ -14587,13 +14977,11 @@ const handleRaigalFix = async (input) => {
14587
14977
  elided: summary.elided
14588
14978
  };
14589
14979
  };
14590
- const handleAislopFix = handleRaigalFix;
14591
- 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`).") });
14592
- 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 = {
14593
14982
  name: "raigal_why",
14594
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.",
14595
- inputSchema: aislopWhyInputSchema,
14596
- name: "aislop_why"
14984
+ inputSchema: raigalWhyInputSchema
14597
14985
  };
14598
14986
  const handleRaigalWhy = (input) => {
14599
14987
  const ruleId = input.rule_id.trim();
@@ -14606,13 +14994,11 @@ const handleRaigalWhy = (input) => {
14606
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."
14607
14995
  };
14608
14996
  };
14609
- const handleAislopWhy = handleRaigalWhy;
14610
- const aislopBaselineInputSchema = z$1.object({ path: z$1.string().optional().describe("Project directory, confined to the MCP server's cwd. Defaults to that cwd.") });
14611
- 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 = {
14612
14999
  name: "raigal_baseline",
14613
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).",
14614
- inputSchema: aislopBaselineInputSchema,
14615
- name: "aislop_baseline"
15001
+ inputSchema: raigalBaselineInputSchema
14616
15002
  };
14617
15003
  const handleRaigalBaseline = (input) => {
14618
15004
  const baseline = readBaseline(resolveCwd(input.path));
@@ -14627,7 +15013,6 @@ const handleRaigalBaseline = (input) => {
14627
15013
  hint: "Run `raigal hook baseline` to capture a baseline."
14628
15014
  };
14629
15015
  };
14630
- const handleAislopBaseline = handleRaigalBaseline;
14631
15016
  //#endregion
14632
15017
  //#region src/telemetry/env.ts
14633
15018
  const ALLOWED_OVERRIDE = /* @__PURE__ */ new Set([
@@ -14766,6 +15151,10 @@ const SAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set([
14766
15151
  "exit_code",
14767
15152
  "duration_ms",
14768
15153
  "error_kind",
15154
+ "error_name",
15155
+ "error_code",
15156
+ "failed_stage",
15157
+ "engines_failed",
14769
15158
  "score",
14770
15159
  "score_bucket",
14771
15160
  "finding_count",
@@ -14879,10 +15268,11 @@ const track = (input) => {
14879
15268
  cachedInstallId = ensured.installId;
14880
15269
  installCreated = ensured.created;
14881
15270
  }
14882
- const { clean, dropped } = redactProperties({
15271
+ const merged = {
14883
15272
  ...baseProperties(cachedInstallId),
14884
15273
  ...input.properties
14885
- });
15274
+ };
15275
+ const { clean, dropped } = redactProperties(merged);
14886
15276
  if (isDebug()) {
14887
15277
  const compact = JSON.stringify({
14888
15278
  event: input.event,
@@ -14986,38 +15376,22 @@ const buildServer = () => {
14986
15376
  name: "raigal",
14987
15377
  version: APP_VERSION
14988
15378
  });
14989
- server.registerTool(aislopScanTool.name, {
14990
- description: aislopScanTool.description,
14991
- inputSchema: aislopScanInputSchema.shape
14992
- }, (input) => instrument("aislop_scan", () => handleAislopScan(input)));
14993
- server.registerTool(aislopFixTool.name, {
14994
- description: aislopFixTool.description,
14995
- inputSchema: aislopFixInputSchema.shape
14996
- }, (input) => instrument("aislop_fix", () => handleAislopFix(input)));
14997
- server.registerTool(aislopWhyTool.name, {
14998
- description: aislopWhyTool.description,
14999
- inputSchema: aislopWhyInputSchema.shape
15000
- }, (input) => instrument("aislop_why", () => handleAislopWhy(input)));
15001
- server.registerTool(aislopBaselineTool.name, {
15002
- description: aislopBaselineTool.description,
15003
- inputSchema: aislopBaselineInputSchema.shape
15004
- }, (input) => instrument("aislop_baseline", () => handleAislopBaseline(input)));
15005
- server.registerTool("raigal_scan", {
15006
- description: aislopScanTool.description,
15007
- inputSchema: aislopScanInputSchema.shape
15008
- }, (input) => instrument("raigal_scan", () => handleAislopScan(input)));
15009
- server.registerTool("raigal_fix", {
15010
- description: aislopFixTool.description,
15011
- inputSchema: aislopFixInputSchema.shape
15012
- }, (input) => instrument("raigal_fix", () => handleAislopFix(input)));
15013
- server.registerTool("raigal_why", {
15014
- description: aislopWhyTool.description,
15015
- inputSchema: aislopWhyInputSchema.shape
15016
- }, (input) => instrument("raigal_why", () => handleAislopWhy(input)));
15017
- server.registerTool("raigal_baseline", {
15018
- description: aislopBaselineTool.description,
15019
- inputSchema: aislopBaselineInputSchema.shape
15020
- }, (input) => instrument("raigal_baseline", () => handleAislopBaseline(input)));
15379
+ server.registerTool(raigalScanTool.name, {
15380
+ description: raigalScanTool.description,
15381
+ inputSchema: raigalScanInputSchema.shape
15382
+ }, (input) => instrument("raigal_scan", () => handleRaigalScan(input)));
15383
+ server.registerTool(raigalFixTool.name, {
15384
+ description: raigalFixTool.description,
15385
+ inputSchema: raigalFixInputSchema.shape
15386
+ }, (input) => instrument("raigal_fix", () => handleRaigalFix(input)));
15387
+ server.registerTool(raigalWhyTool.name, {
15388
+ description: raigalWhyTool.description,
15389
+ inputSchema: raigalWhyInputSchema.shape
15390
+ }, (input) => instrument("raigal_why", () => handleRaigalWhy(input)));
15391
+ server.registerTool(raigalBaselineTool.name, {
15392
+ description: raigalBaselineTool.description,
15393
+ inputSchema: raigalBaselineInputSchema.shape
15394
+ }, (input) => instrument("raigal_baseline", () => handleRaigalBaseline(input)));
15021
15395
  return server;
15022
15396
  };
15023
15397
  const main = async () => {