aislop 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -6
- package/dist/cli.js +1581 -110
- package/dist/expo-doctor-T4DswmX5.js +136 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1308 -67
- package/dist/{json-ZItDVIZL.js → json-BJGLCIK-.js} +1 -1
- package/dist/mcp.js +5115 -0
- package/dist/subprocess-CCnnN_oQ.js +60 -0
- package/dist/typecheck-B1MXNAy-.js +102 -0
- package/dist/typecheck-By967nny.js +102 -0
- package/dist/typecheck-XJMuCczG.js +101 -0
- package/dist/{version-AmNwcw_U.js → version-B9ZchFMv.js} +1 -1
- package/package.json +10 -3
- /package/dist/{json-B51etWTw.js → json-BbMwrgyd.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire } from "node:module";
|
|
2
|
+
import { createRequire, isBuiltin } from "node:module";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import fs from "node:fs";
|
|
@@ -7,7 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import YAML from "yaml";
|
|
8
8
|
import { z } from "zod/v4";
|
|
9
9
|
import { performance } from "node:perf_hooks";
|
|
10
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
+
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
11
11
|
import micromatch from "micromatch";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import ts from "typescript";
|
|
@@ -34,7 +34,11 @@ var __exportAll = (all, no_symbols) => {
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/hooks/feedback.ts
|
|
37
|
+
const fingerprintFinding = (f) => `${f.file}:${f.line}:${f.ruleId}`;
|
|
37
38
|
const MAX_FINDINGS = 20;
|
|
39
|
+
const MAX_NEW_SINCE_BASELINE = 10;
|
|
40
|
+
const REVIEW_TOP_N = 3;
|
|
41
|
+
const REGRESSION_FLAG_THRESHOLD = 5;
|
|
38
42
|
const toFinding = (d, rootDirectory) => {
|
|
39
43
|
if (d.severity !== "error" && d.severity !== "warning") return null;
|
|
40
44
|
const file = path.isAbsolute(d.filePath) ? path.relative(rootDirectory, d.filePath) : d.filePath;
|
|
@@ -64,6 +68,42 @@ const buildNextSteps = (findings) => {
|
|
|
64
68
|
}
|
|
65
69
|
return steps;
|
|
66
70
|
};
|
|
71
|
+
const buildSuggestedActions = (diagnostics, findings, regressed, delta) => {
|
|
72
|
+
const actions = [];
|
|
73
|
+
const fixableDiags = diagnostics.filter((d) => d.fixable);
|
|
74
|
+
if (fixableDiags.length > 0) {
|
|
75
|
+
const ruleIds = Array.from(new Set(fixableDiags.map((d) => d.rule)));
|
|
76
|
+
actions.push({
|
|
77
|
+
id: "run_aislop_fix",
|
|
78
|
+
label: `Run aislop fix to clear ${fixableDiags.length} mechanical finding${fixableDiags.length === 1 ? "" : "s"}.`,
|
|
79
|
+
command: "npx aislop fix",
|
|
80
|
+
rationale: "These findings have deterministic fixes (formatting, unused imports, trivial comments). Running this before any manual work avoids burning agent tokens on what the CLI handles for free.",
|
|
81
|
+
ruleIds
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const archErrors = findings.filter((f) => f.ruleId.startsWith("arch/") && f.severity === "error");
|
|
85
|
+
if (archErrors.length > 0) actions.push({
|
|
86
|
+
id: "review_finding",
|
|
87
|
+
label: `Review ${archErrors.length} architecture rule violation${archErrors.length === 1 ? "" : "s"} — these can't be auto-fixed.`,
|
|
88
|
+
rationale: "Architecture rules encode intentional project structure decisions. The fix usually means moving code, not editing it.",
|
|
89
|
+
ruleIds: Array.from(new Set(archErrors.map((f) => f.ruleId)))
|
|
90
|
+
});
|
|
91
|
+
if (regressed && typeof delta === "number" && delta <= -REGRESSION_FLAG_THRESHOLD && fixableDiags.length === 0) {
|
|
92
|
+
const top = findings.filter((f) => f.severity === "error" || f.severity === "warning").slice(0, REVIEW_TOP_N);
|
|
93
|
+
if (top.length > 0) actions.push({
|
|
94
|
+
id: "review_finding",
|
|
95
|
+
label: `Score dropped ${Math.abs(delta)} points — review the top ${top.length} finding${top.length === 1 ? "" : "s"} from this edit.`,
|
|
96
|
+
rationale: "None of these are auto-fixable. Read each one against the source and decide whether the fix is to change the code or to add a justified suppression with a reason.",
|
|
97
|
+
ruleIds: top.map((f) => f.ruleId)
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (actions.length === 0) actions.push({
|
|
101
|
+
id: "no_action",
|
|
102
|
+
label: typeof delta === "number" ? delta > 0 ? `Score improved by ${delta}. No action needed.` : "Score unchanged. No action needed." : "No findings. No action needed.",
|
|
103
|
+
rationale: "The current scan didn't reveal anything that requires the agent's attention."
|
|
104
|
+
});
|
|
105
|
+
return actions;
|
|
106
|
+
};
|
|
67
107
|
const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
|
|
68
108
|
const all = diagnostics.map((d) => toFinding(d, rootDirectory)).filter((x) => x !== null);
|
|
69
109
|
const capped = all.slice(0, MAX_FINDINGS);
|
|
@@ -74,15 +114,30 @@ const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
|
|
|
74
114
|
fixable: diagnostics.filter((d) => d.fixable).length,
|
|
75
115
|
total: all.length
|
|
76
116
|
};
|
|
117
|
+
const baselineSnapshot = typeof baseline === "number" ? {
|
|
118
|
+
score: baseline,
|
|
119
|
+
findingFingerprints: []
|
|
120
|
+
} : baseline;
|
|
121
|
+
const baselineScore = baselineSnapshot?.score;
|
|
122
|
+
const delta = typeof baselineScore === "number" ? score - baselineScore : void 0;
|
|
123
|
+
const regressed = typeof delta === "number" ? delta < 0 : false;
|
|
124
|
+
let newSinceBaseline;
|
|
125
|
+
if (baselineSnapshot && baselineSnapshot.findingFingerprints.length > 0) {
|
|
126
|
+
const known = new Set(baselineSnapshot.findingFingerprints);
|
|
127
|
+
newSinceBaseline = all.filter((f) => !known.has(fingerprintFinding(f))).slice(0, MAX_NEW_SINCE_BASELINE);
|
|
128
|
+
}
|
|
77
129
|
return {
|
|
78
|
-
schema: "aislop.hook.
|
|
130
|
+
schema: "aislop.hook.v2",
|
|
79
131
|
score,
|
|
80
|
-
baseline,
|
|
81
|
-
|
|
132
|
+
baseline: baselineScore,
|
|
133
|
+
delta,
|
|
134
|
+
regressed,
|
|
82
135
|
counts,
|
|
83
136
|
findings: capped,
|
|
84
137
|
elided,
|
|
85
|
-
|
|
138
|
+
newSinceBaseline,
|
|
139
|
+
nextSteps: buildNextSteps(capped),
|
|
140
|
+
suggestedActions: buildSuggestedActions(diagnostics, capped, regressed, delta)
|
|
86
141
|
};
|
|
87
142
|
};
|
|
88
143
|
|
|
@@ -147,6 +202,7 @@ const DEFAULT_CONFIG = {
|
|
|
147
202
|
maxNesting: 5,
|
|
148
203
|
maxParams: 6
|
|
149
204
|
},
|
|
205
|
+
lint: { typecheck: false },
|
|
150
206
|
security: {
|
|
151
207
|
audit: true,
|
|
152
208
|
auditTimeout: 25e3
|
|
@@ -209,6 +265,48 @@ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
|
|
|
209
265
|
# severity: error
|
|
210
266
|
`;
|
|
211
267
|
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/config/extends.ts
|
|
270
|
+
const MAX_DEPTH = 5;
|
|
271
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
272
|
+
const deepMerge = (...sources) => {
|
|
273
|
+
const result = {};
|
|
274
|
+
for (const source of sources) for (const key of Object.keys(source)) {
|
|
275
|
+
const a = result[key];
|
|
276
|
+
const b = source[key];
|
|
277
|
+
result[key] = isPlainObject(a) && isPlainObject(b) ? deepMerge(a, b) : b;
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
};
|
|
281
|
+
const resolveExtendsRef = (ref, fromDir) => {
|
|
282
|
+
if (ref.startsWith("http://") || ref.startsWith("https://")) throw new Error(`URL-based extends not yet supported: ${ref}`);
|
|
283
|
+
if (ref.startsWith("./") || ref.startsWith("../") || path.isAbsolute(ref)) return path.resolve(fromDir, ref);
|
|
284
|
+
throw new Error(`Package-name extends not yet supported: ${ref} (use a relative path for now)`);
|
|
285
|
+
};
|
|
286
|
+
const normalizeExtends = (raw) => {
|
|
287
|
+
if (raw === void 0 || raw === null) return [];
|
|
288
|
+
if (typeof raw === "string") return [raw];
|
|
289
|
+
if (Array.isArray(raw) && raw.every((s) => typeof s === "string")) return raw;
|
|
290
|
+
throw new Error("`extends` must be a string or array of strings");
|
|
291
|
+
};
|
|
292
|
+
const loadConfigChain = (configPath, visited = /* @__PURE__ */ new Set(), depth = 0) => {
|
|
293
|
+
if (depth > MAX_DEPTH) throw new Error(`extends depth exceeded ${MAX_DEPTH} (cycle or runaway chain): ${configPath}`);
|
|
294
|
+
const absPath = path.resolve(configPath);
|
|
295
|
+
if (visited.has(absPath)) throw new Error(`circular extends detected: ${absPath}`);
|
|
296
|
+
if (!fs.existsSync(absPath)) throw new Error(`extends target not found: ${absPath}`);
|
|
297
|
+
const nextVisited = new Set(visited);
|
|
298
|
+
nextVisited.add(absPath);
|
|
299
|
+
const raw = fs.readFileSync(absPath, "utf-8");
|
|
300
|
+
const parsed = YAML.parse(raw) ?? {};
|
|
301
|
+
const refs = normalizeExtends(parsed.extends);
|
|
302
|
+
const fromDir = path.dirname(absPath);
|
|
303
|
+
const parents = refs.map((ref) => {
|
|
304
|
+
return loadConfigChain(resolveExtendsRef(ref, fromDir), nextVisited, depth + 1);
|
|
305
|
+
});
|
|
306
|
+
const { extends: _drop, ...own } = parsed;
|
|
307
|
+
return deepMerge(...parents, own);
|
|
308
|
+
};
|
|
309
|
+
|
|
212
310
|
//#endregion
|
|
213
311
|
//#region src/config/schema.ts
|
|
214
312
|
const DEFAULT_WEIGHTS = {
|
|
@@ -233,6 +331,7 @@ const QualitySchema = z.object({
|
|
|
233
331
|
maxNesting: z.number().positive().default(5),
|
|
234
332
|
maxParams: z.number().positive().default(6)
|
|
235
333
|
});
|
|
334
|
+
const LintConfigSchema = z.object({ typecheck: z.boolean().default(false) });
|
|
236
335
|
const SecurityConfigSchema = z.object({
|
|
237
336
|
audit: z.boolean().default(true),
|
|
238
337
|
auditTimeout: z.number().positive().default(25e3)
|
|
@@ -270,6 +369,7 @@ const AislopConfigSchema = z.object({
|
|
|
270
369
|
maxNesting: 5,
|
|
271
370
|
maxParams: 6
|
|
272
371
|
})),
|
|
372
|
+
lint: LintConfigSchema.default(() => ({ typecheck: false })),
|
|
273
373
|
security: SecurityConfigSchema.default(() => ({
|
|
274
374
|
audit: true,
|
|
275
375
|
auditTimeout: 25e3
|
|
@@ -343,8 +443,7 @@ const loadConfig = (directory) => {
|
|
|
343
443
|
const configPath = path.join(configDir, CONFIG_FILE);
|
|
344
444
|
if (!fs.existsSync(configPath)) return DEFAULT_CONFIG;
|
|
345
445
|
try {
|
|
346
|
-
|
|
347
|
-
return parseConfig(YAML.parse(raw));
|
|
446
|
+
return parseConfig(loadConfigChain(configPath));
|
|
348
447
|
} catch (error) {
|
|
349
448
|
const msg = error instanceof Error ? error.message : String(error);
|
|
350
449
|
process.stderr.write(` ⚠ Failed to parse ${configPath}: ${msg}\n ⚠ Using default configuration.\n`);
|
|
@@ -423,7 +522,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
|
423
522
|
return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
|
|
424
523
|
};
|
|
425
524
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
426
|
-
const isTestFile = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
525
|
+
const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
427
526
|
const getIgnoredPaths = (rootDirectory, files) => {
|
|
428
527
|
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
429
528
|
const result = spawnSync("git", [
|
|
@@ -497,7 +596,7 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
|
|
|
497
596
|
return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
|
|
498
597
|
};
|
|
499
598
|
return normalizedFiles.filter(({ absolutePath, relativePath }) => {
|
|
500
|
-
return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
|
|
599
|
+
return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile$2(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
|
|
501
600
|
}).map(({ absolutePath }) => absolutePath);
|
|
502
601
|
};
|
|
503
602
|
const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
|
|
@@ -633,13 +732,14 @@ const detectOverAbstraction = async (context) => {
|
|
|
633
732
|
|
|
634
733
|
//#endregion
|
|
635
734
|
//#region src/engines/ai-slop/comments.ts
|
|
735
|
+
const NON_PRODUCTION_DIR_PATTERN$2 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
|
|
636
736
|
const TRIVIAL_VERB_STEMS = "Import|Defin|Initializ|Setting|Set\\s+up|Setup|Return|Check|Loop|Iterat|Creat|Updat|Delet|Remov|Handl|Get|Fetch|Increment|Decrement|Writ|Runn|Run|Pars|Execut|Extract|Sav|Load|Build|Start|Stopp|Stop|Clean(?:up|\\s+up)?|Configur|Validat|Process|Queue|Fire|Emit|Dispatch|Log|Print|Render";
|
|
637
737
|
const TRIVIAL_JS_COMMENT_PATTERNS = [/\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i, new RegExp(`\\/\\/\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
638
738
|
const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, new RegExp(`^#\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
639
739
|
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
|
|
640
740
|
const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
641
741
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
642
|
-
const isJsComment = (trimmed) => trimmed.startsWith("//");
|
|
742
|
+
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
643
743
|
const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
|
|
644
744
|
/**
|
|
645
745
|
* Extract just the comment text after the comment marker.
|
|
@@ -688,13 +788,14 @@ const detectTrivialComments = async (context) => {
|
|
|
688
788
|
const diagnostics = [];
|
|
689
789
|
for (const filePath of files) {
|
|
690
790
|
if (isAutoGenerated(filePath)) continue;
|
|
791
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
792
|
+
if (NON_PRODUCTION_DIR_PATTERN$2.test(relativePath)) continue;
|
|
691
793
|
let content;
|
|
692
794
|
try {
|
|
693
795
|
content = fs.readFileSync(filePath, "utf-8");
|
|
694
796
|
} catch {
|
|
695
797
|
continue;
|
|
696
798
|
}
|
|
697
|
-
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
698
799
|
diagnostics.push(...scanFileForTrivialComments(content, relativePath));
|
|
699
800
|
}
|
|
700
801
|
return diagnostics;
|
|
@@ -702,7 +803,7 @@ const detectTrivialComments = async (context) => {
|
|
|
702
803
|
|
|
703
804
|
//#endregion
|
|
704
805
|
//#region src/engines/ai-slop/dead-patterns.ts
|
|
705
|
-
const JS_EXTENSIONS$
|
|
806
|
+
const JS_EXTENSIONS$4 = new Set([
|
|
706
807
|
".ts",
|
|
707
808
|
".tsx",
|
|
708
809
|
".js",
|
|
@@ -724,11 +825,11 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
|
724
825
|
fixable
|
|
725
826
|
});
|
|
726
827
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
727
|
-
const
|
|
828
|
+
const NON_PRODUCTION_DIR_PATTERN$1 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|cli|cli-[\w-]+|[\w-]+-cli)\//;
|
|
728
829
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
729
|
-
if (!JS_EXTENSIONS$
|
|
830
|
+
if (!JS_EXTENSIONS$4.has(ext)) return [];
|
|
730
831
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
731
|
-
if (
|
|
832
|
+
if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
|
|
732
833
|
const diagnostics = [];
|
|
733
834
|
const lines = content.split("\n");
|
|
734
835
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -768,9 +869,9 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
768
869
|
for (let i = 0; i < lines.length; i++) {
|
|
769
870
|
const trimmed = lines[i].trim();
|
|
770
871
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
771
|
-
if (JS_EXTENSIONS$
|
|
872
|
+
if (JS_EXTENSIONS$4.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !nextLine.startsWith("}") && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push(slop(relativePath, i + 2, "ai-slop/unreachable-code", "warning", "Code after return/throw statement is unreachable", "Remove the unreachable code or restructure the control flow", false));
|
|
772
873
|
if (/\bif\s*\(\s*(?:false|true|0|1)\s*\)/.test(trimmed) && !trimmed.startsWith("//") && !trimmed.startsWith("*") && !/["'`].*\bif\s*\(/.test(trimmed) && !/\/.*\bif\s*\(/.test(trimmed.replace(/\/\/.*$/, ""))) diagnostics.push(slop(relativePath, i + 1, "ai-slop/constant-condition", "warning", "Conditional with a constant value — likely debugging leftover", "Remove the constant condition or replace with proper logic", false));
|
|
773
|
-
if (JS_EXTENSIONS$
|
|
874
|
+
if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
|
|
774
875
|
}
|
|
775
876
|
return diagnostics;
|
|
776
877
|
};
|
|
@@ -778,6 +879,7 @@ const asAnyPattern = new RegExp(`\\bas\\s+any\\b`);
|
|
|
778
879
|
const doubleAssertPattern = new RegExp(`\\bas\\s+unknown\\s+as\\s+`);
|
|
779
880
|
const detectUnsafeTypePatterns = (content, relativePath, ext) => {
|
|
780
881
|
if (ext !== ".ts" && ext !== ".tsx") return [];
|
|
882
|
+
if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
|
|
781
883
|
const diagnostics = [];
|
|
782
884
|
const lines = content.split("\n");
|
|
783
885
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -814,6 +916,74 @@ const detectDeadPatterns = async (context) => {
|
|
|
814
916
|
return diagnostics;
|
|
815
917
|
};
|
|
816
918
|
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/engines/ai-slop/duplicate-imports.ts
|
|
921
|
+
const JS_EXTENSIONS$3 = new Set([
|
|
922
|
+
".ts",
|
|
923
|
+
".tsx",
|
|
924
|
+
".js",
|
|
925
|
+
".jsx",
|
|
926
|
+
".mjs",
|
|
927
|
+
".cjs"
|
|
928
|
+
]);
|
|
929
|
+
const IMPORT_FROM_RE$1 = /^\s*import\s+[^;]*?from\s+["']([^"']+)["']/;
|
|
930
|
+
const extractImportLines = (content) => {
|
|
931
|
+
const lines = content.split("\n");
|
|
932
|
+
const results = [];
|
|
933
|
+
for (let i = 0; i < lines.length; i++) {
|
|
934
|
+
const line = lines[i];
|
|
935
|
+
const match = IMPORT_FROM_RE$1.exec(line);
|
|
936
|
+
if (!match) continue;
|
|
937
|
+
results.push({
|
|
938
|
+
spec: match[1],
|
|
939
|
+
line: i + 1
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
return results;
|
|
943
|
+
};
|
|
944
|
+
const detectDuplicateImports = async (context) => {
|
|
945
|
+
const diagnostics = [];
|
|
946
|
+
const files = getSourceFiles(context);
|
|
947
|
+
for (const filePath of files) {
|
|
948
|
+
if (!JS_EXTENSIONS$3.has(path.extname(filePath))) continue;
|
|
949
|
+
if (isAutoGenerated(filePath)) continue;
|
|
950
|
+
let content;
|
|
951
|
+
try {
|
|
952
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
953
|
+
} catch {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const imports = extractImportLines(content);
|
|
957
|
+
if (imports.length < 2) continue;
|
|
958
|
+
const bySpec = /* @__PURE__ */ new Map();
|
|
959
|
+
for (const imp of imports) {
|
|
960
|
+
const list = bySpec.get(imp.spec) ?? [];
|
|
961
|
+
list.push(imp);
|
|
962
|
+
bySpec.set(imp.spec, list);
|
|
963
|
+
}
|
|
964
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
965
|
+
for (const [spec, occurrences] of bySpec) {
|
|
966
|
+
if (occurrences.length < 2) continue;
|
|
967
|
+
for (const dup of occurrences.slice(1)) {
|
|
968
|
+
const firstLine = occurrences[0].line;
|
|
969
|
+
diagnostics.push({
|
|
970
|
+
filePath: relPath,
|
|
971
|
+
engine: "ai-slop",
|
|
972
|
+
rule: "ai-slop/duplicate-import",
|
|
973
|
+
severity: "warning",
|
|
974
|
+
message: `"${spec}" is also imported on line ${firstLine}. Merge into a single import statement.`,
|
|
975
|
+
help: "Two imports from the same module split readers' attention and grow the import block. Run aislop fix to merge them automatically.",
|
|
976
|
+
line: dup.line,
|
|
977
|
+
column: 1,
|
|
978
|
+
category: "AI Slop",
|
|
979
|
+
fixable: true
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return diagnostics;
|
|
985
|
+
};
|
|
986
|
+
|
|
817
987
|
//#endregion
|
|
818
988
|
//#region src/engines/ai-slop/exceptions.ts
|
|
819
989
|
const SWALLOWED_EXCEPTION_PATTERNS = [
|
|
@@ -904,6 +1074,600 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
904
1074
|
return diagnostics;
|
|
905
1075
|
};
|
|
906
1076
|
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/engines/ai-slop/go-patterns.ts
|
|
1079
|
+
const GO_EXTENSIONS = new Set([".go"]);
|
|
1080
|
+
const PACKAGE_DECL_RE = /^\s*package\s+(\w+)/;
|
|
1081
|
+
const PANIC_CALL_RE = /\bpanic\s*\(/;
|
|
1082
|
+
const COMMENT_LINE_RE$1 = /^\s*\/\//;
|
|
1083
|
+
const NIL_GUARD_RE = /^\s*if\s+[\w.]+(?:\(\))?\s*==\s*nil\s*\{?\s*$/;
|
|
1084
|
+
const SHORT_STRING_PANIC_RE = /\bpanic\s*\(\s*"[^"]{1,40}"\s*\)/;
|
|
1085
|
+
const detectPackageName = (lines) => {
|
|
1086
|
+
for (const line of lines) {
|
|
1087
|
+
const m = PACKAGE_DECL_RE.exec(line);
|
|
1088
|
+
if (m) return m[1];
|
|
1089
|
+
}
|
|
1090
|
+
return null;
|
|
1091
|
+
};
|
|
1092
|
+
const PANIC_INTENT_LOOKBACK = 3;
|
|
1093
|
+
const hasIntentComment$1 = (lines, panicLineIdx) => {
|
|
1094
|
+
for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - PANIC_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE$1.test(lines[j])) return true;
|
|
1095
|
+
return false;
|
|
1096
|
+
};
|
|
1097
|
+
const isNilGuardPanic = (lines, panicLineIdx, line) => {
|
|
1098
|
+
if (!SHORT_STRING_PANIC_RE.test(line)) return false;
|
|
1099
|
+
for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - 2); j--) {
|
|
1100
|
+
const prev = lines[j];
|
|
1101
|
+
if (prev.trim() === "") continue;
|
|
1102
|
+
return NIL_GUARD_RE.test(prev);
|
|
1103
|
+
}
|
|
1104
|
+
return false;
|
|
1105
|
+
};
|
|
1106
|
+
const flagLibraryPanic = (lines, relPath, pkg, out) => {
|
|
1107
|
+
if (pkg === "main") return;
|
|
1108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1109
|
+
const line = lines[i];
|
|
1110
|
+
if (COMMENT_LINE_RE$1.test(line)) continue;
|
|
1111
|
+
PANIC_CALL_RE.lastIndex = 0;
|
|
1112
|
+
if (!PANIC_CALL_RE.test(line)) continue;
|
|
1113
|
+
if (hasIntentComment$1(lines, i)) continue;
|
|
1114
|
+
if (isNilGuardPanic(lines, i, line)) continue;
|
|
1115
|
+
out.push({
|
|
1116
|
+
filePath: relPath,
|
|
1117
|
+
engine: "ai-slop",
|
|
1118
|
+
rule: "ai-slop/go-library-panic",
|
|
1119
|
+
severity: "warning",
|
|
1120
|
+
message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
|
|
1121
|
+
help: "Convert to `return fmt.Errorf(...)` (or a wrapped error) and let the caller decide. Reserve `panic` for genuinely-impossible states (corrupt internal invariants), and mark those with a comment so future readers know it's intentional.",
|
|
1122
|
+
line: i + 1,
|
|
1123
|
+
column: 1,
|
|
1124
|
+
category: "AI Slop",
|
|
1125
|
+
fixable: false
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
const detectGoPatterns = async (context) => {
|
|
1130
|
+
const diagnostics = [];
|
|
1131
|
+
const files = getSourceFiles(context);
|
|
1132
|
+
for (const filePath of files) {
|
|
1133
|
+
if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
1134
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1135
|
+
if (filePath.endsWith("_test.go")) continue;
|
|
1136
|
+
let content;
|
|
1137
|
+
try {
|
|
1138
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1139
|
+
} catch {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
const lines = content.split("\n");
|
|
1143
|
+
const pkg = detectPackageName(lines);
|
|
1144
|
+
if (!pkg) continue;
|
|
1145
|
+
flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
|
|
1146
|
+
}
|
|
1147
|
+
return diagnostics;
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region src/engines/ai-slop/python-data.ts
|
|
1152
|
+
const PYTHON_STDLIB = new Set([
|
|
1153
|
+
"__future__",
|
|
1154
|
+
"_thread",
|
|
1155
|
+
"abc",
|
|
1156
|
+
"argparse",
|
|
1157
|
+
"array",
|
|
1158
|
+
"ast",
|
|
1159
|
+
"asyncio",
|
|
1160
|
+
"atexit",
|
|
1161
|
+
"base64",
|
|
1162
|
+
"binascii",
|
|
1163
|
+
"bisect",
|
|
1164
|
+
"builtins",
|
|
1165
|
+
"bz2",
|
|
1166
|
+
"calendar",
|
|
1167
|
+
"codecs",
|
|
1168
|
+
"collections",
|
|
1169
|
+
"concurrent",
|
|
1170
|
+
"configparser",
|
|
1171
|
+
"contextlib",
|
|
1172
|
+
"contextvars",
|
|
1173
|
+
"copy",
|
|
1174
|
+
"csv",
|
|
1175
|
+
"ctypes",
|
|
1176
|
+
"dataclasses",
|
|
1177
|
+
"datetime",
|
|
1178
|
+
"decimal",
|
|
1179
|
+
"difflib",
|
|
1180
|
+
"dis",
|
|
1181
|
+
"doctest",
|
|
1182
|
+
"email",
|
|
1183
|
+
"encodings",
|
|
1184
|
+
"enum",
|
|
1185
|
+
"errno",
|
|
1186
|
+
"faulthandler",
|
|
1187
|
+
"filecmp",
|
|
1188
|
+
"fileinput",
|
|
1189
|
+
"fnmatch",
|
|
1190
|
+
"fractions",
|
|
1191
|
+
"functools",
|
|
1192
|
+
"gc",
|
|
1193
|
+
"getopt",
|
|
1194
|
+
"getpass",
|
|
1195
|
+
"gettext",
|
|
1196
|
+
"glob",
|
|
1197
|
+
"graphlib",
|
|
1198
|
+
"gzip",
|
|
1199
|
+
"hashlib",
|
|
1200
|
+
"heapq",
|
|
1201
|
+
"hmac",
|
|
1202
|
+
"html",
|
|
1203
|
+
"http",
|
|
1204
|
+
"imaplib",
|
|
1205
|
+
"importlib",
|
|
1206
|
+
"inspect",
|
|
1207
|
+
"io",
|
|
1208
|
+
"ipaddress",
|
|
1209
|
+
"itertools",
|
|
1210
|
+
"json",
|
|
1211
|
+
"keyword",
|
|
1212
|
+
"linecache",
|
|
1213
|
+
"locale",
|
|
1214
|
+
"logging",
|
|
1215
|
+
"lzma",
|
|
1216
|
+
"mailbox",
|
|
1217
|
+
"math",
|
|
1218
|
+
"mimetypes",
|
|
1219
|
+
"mmap",
|
|
1220
|
+
"multiprocessing",
|
|
1221
|
+
"numbers",
|
|
1222
|
+
"operator",
|
|
1223
|
+
"os",
|
|
1224
|
+
"pathlib",
|
|
1225
|
+
"pdb",
|
|
1226
|
+
"pickle",
|
|
1227
|
+
"platform",
|
|
1228
|
+
"plistlib",
|
|
1229
|
+
"pprint",
|
|
1230
|
+
"profile",
|
|
1231
|
+
"pstats",
|
|
1232
|
+
"pty",
|
|
1233
|
+
"queue",
|
|
1234
|
+
"quopri",
|
|
1235
|
+
"random",
|
|
1236
|
+
"re",
|
|
1237
|
+
"readline",
|
|
1238
|
+
"reprlib",
|
|
1239
|
+
"resource",
|
|
1240
|
+
"secrets",
|
|
1241
|
+
"select",
|
|
1242
|
+
"selectors",
|
|
1243
|
+
"shelve",
|
|
1244
|
+
"shlex",
|
|
1245
|
+
"shutil",
|
|
1246
|
+
"signal",
|
|
1247
|
+
"site",
|
|
1248
|
+
"smtplib",
|
|
1249
|
+
"socket",
|
|
1250
|
+
"socketserver",
|
|
1251
|
+
"sqlite3",
|
|
1252
|
+
"ssl",
|
|
1253
|
+
"stat",
|
|
1254
|
+
"statistics",
|
|
1255
|
+
"string",
|
|
1256
|
+
"stringprep",
|
|
1257
|
+
"struct",
|
|
1258
|
+
"subprocess",
|
|
1259
|
+
"sunau",
|
|
1260
|
+
"symtable",
|
|
1261
|
+
"sys",
|
|
1262
|
+
"sysconfig",
|
|
1263
|
+
"syslog",
|
|
1264
|
+
"tarfile",
|
|
1265
|
+
"telnetlib",
|
|
1266
|
+
"tempfile",
|
|
1267
|
+
"termios",
|
|
1268
|
+
"test",
|
|
1269
|
+
"textwrap",
|
|
1270
|
+
"threading",
|
|
1271
|
+
"time",
|
|
1272
|
+
"timeit",
|
|
1273
|
+
"tkinter",
|
|
1274
|
+
"token",
|
|
1275
|
+
"tokenize",
|
|
1276
|
+
"tomllib",
|
|
1277
|
+
"trace",
|
|
1278
|
+
"traceback",
|
|
1279
|
+
"tracemalloc",
|
|
1280
|
+
"tty",
|
|
1281
|
+
"turtle",
|
|
1282
|
+
"types",
|
|
1283
|
+
"typing",
|
|
1284
|
+
"unicodedata",
|
|
1285
|
+
"unittest",
|
|
1286
|
+
"urllib",
|
|
1287
|
+
"uu",
|
|
1288
|
+
"uuid",
|
|
1289
|
+
"venv",
|
|
1290
|
+
"warnings",
|
|
1291
|
+
"wave",
|
|
1292
|
+
"weakref",
|
|
1293
|
+
"webbrowser",
|
|
1294
|
+
"winreg",
|
|
1295
|
+
"winsound",
|
|
1296
|
+
"wsgiref",
|
|
1297
|
+
"xml",
|
|
1298
|
+
"xmlrpc",
|
|
1299
|
+
"zipapp",
|
|
1300
|
+
"zipfile",
|
|
1301
|
+
"zipimport",
|
|
1302
|
+
"zlib",
|
|
1303
|
+
"zoneinfo"
|
|
1304
|
+
]);
|
|
1305
|
+
const PYTHON_IMPORT_TO_PIP = {
|
|
1306
|
+
yaml: "pyyaml",
|
|
1307
|
+
PIL: "pillow",
|
|
1308
|
+
dateutil: "python-dateutil",
|
|
1309
|
+
cv2: "opencv-python",
|
|
1310
|
+
sklearn: "scikit-learn",
|
|
1311
|
+
bs4: "beautifulsoup4",
|
|
1312
|
+
typing_extensions: "typing-extensions",
|
|
1313
|
+
google: "google-api-python-client",
|
|
1314
|
+
jose: "python-jose",
|
|
1315
|
+
jwt: "pyjwt",
|
|
1316
|
+
OpenSSL: "pyopenssl",
|
|
1317
|
+
magic: "python-magic",
|
|
1318
|
+
docx: "python-docx",
|
|
1319
|
+
pptx: "python-pptx",
|
|
1320
|
+
git: "gitpython",
|
|
1321
|
+
socks: "pysocks",
|
|
1322
|
+
redis: "redis"
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
//#endregion
|
|
1326
|
+
//#region src/engines/ai-slop/hallucinated-imports.ts
|
|
1327
|
+
const JS_EXTENSIONS$2 = new Set([
|
|
1328
|
+
".ts",
|
|
1329
|
+
".tsx",
|
|
1330
|
+
".js",
|
|
1331
|
+
".jsx",
|
|
1332
|
+
".mjs",
|
|
1333
|
+
".cjs"
|
|
1334
|
+
]);
|
|
1335
|
+
const PY_EXTENSIONS$2 = new Set([".py"]);
|
|
1336
|
+
const readJson = (filePath) => {
|
|
1337
|
+
try {
|
|
1338
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1339
|
+
} catch {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
const PKG_DEP_SECTIONS = [
|
|
1344
|
+
"dependencies",
|
|
1345
|
+
"devDependencies",
|
|
1346
|
+
"peerDependencies",
|
|
1347
|
+
"optionalDependencies"
|
|
1348
|
+
];
|
|
1349
|
+
const addDepsFromPkg = (pkg, jsDeps) => {
|
|
1350
|
+
for (const section of PKG_DEP_SECTIONS) {
|
|
1351
|
+
const deps = pkg[section];
|
|
1352
|
+
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
1356
|
+
const globs = [];
|
|
1357
|
+
if (rootPkg && typeof rootPkg === "object") {
|
|
1358
|
+
const ws = rootPkg.workspaces;
|
|
1359
|
+
if (Array.isArray(ws)) {
|
|
1360
|
+
for (const g of ws) if (typeof g === "string") globs.push(g);
|
|
1361
|
+
} else if (ws && typeof ws === "object") {
|
|
1362
|
+
const pkgs = ws.packages;
|
|
1363
|
+
if (Array.isArray(pkgs)) {
|
|
1364
|
+
for (const g of pkgs) if (typeof g === "string") globs.push(g);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const lerna = readJson(path.join(rootDir, "lerna.json"));
|
|
1369
|
+
if (lerna && Array.isArray(lerna.packages)) {
|
|
1370
|
+
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
1371
|
+
}
|
|
1372
|
+
try {
|
|
1373
|
+
const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
|
|
1374
|
+
let inPackages = false;
|
|
1375
|
+
for (const rawLine of pnpmWs.split("\n")) {
|
|
1376
|
+
if (/^packages\s*:\s*$/.test(rawLine)) {
|
|
1377
|
+
inPackages = true;
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
if (!inPackages) continue;
|
|
1381
|
+
if (/^\S/.test(rawLine)) break;
|
|
1382
|
+
const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
|
|
1383
|
+
if (m) globs.push(m[1].trim());
|
|
1384
|
+
}
|
|
1385
|
+
} catch {}
|
|
1386
|
+
return globs;
|
|
1387
|
+
};
|
|
1388
|
+
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
1389
|
+
const dirs = [];
|
|
1390
|
+
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
1391
|
+
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
1392
|
+
try {
|
|
1393
|
+
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
1394
|
+
} catch {
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
1398
|
+
return dirs;
|
|
1399
|
+
};
|
|
1400
|
+
const SKIP_DIRS = new Set([
|
|
1401
|
+
"node_modules",
|
|
1402
|
+
".git",
|
|
1403
|
+
"dist",
|
|
1404
|
+
"build",
|
|
1405
|
+
"out",
|
|
1406
|
+
"target",
|
|
1407
|
+
"coverage"
|
|
1408
|
+
]);
|
|
1409
|
+
const NESTED_PKG_JSON_DEPTH = 4;
|
|
1410
|
+
const collectNestedManifests = (rootDir, jsDeps) => {
|
|
1411
|
+
const walk = (dir, depth) => {
|
|
1412
|
+
if (depth > NESTED_PKG_JSON_DEPTH) return;
|
|
1413
|
+
let entries;
|
|
1414
|
+
try {
|
|
1415
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1416
|
+
} catch {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
for (const entry of entries) {
|
|
1420
|
+
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
1421
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1422
|
+
const full = path.join(dir, entry.name);
|
|
1423
|
+
if (entry.isDirectory()) walk(full, depth + 1);
|
|
1424
|
+
else if (entry.name === "package.json" && depth > 0) {
|
|
1425
|
+
const wsPkg = readJson(full);
|
|
1426
|
+
if (!wsPkg) continue;
|
|
1427
|
+
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
1428
|
+
addDepsFromPkg(wsPkg, jsDeps);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
walk(rootDir, 0);
|
|
1433
|
+
};
|
|
1434
|
+
const collectJsDeps = (rootDir, jsDeps) => {
|
|
1435
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
1436
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
1437
|
+
const pkg = readJson(pkgPath);
|
|
1438
|
+
if (!pkg || typeof pkg !== "object") return false;
|
|
1439
|
+
addDepsFromPkg(pkg, jsDeps);
|
|
1440
|
+
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
1441
|
+
const workspaceDirs = expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, pkg));
|
|
1442
|
+
for (const wsDir of workspaceDirs) {
|
|
1443
|
+
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
1444
|
+
if (!wsPkg) continue;
|
|
1445
|
+
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
1446
|
+
addDepsFromPkg(wsPkg, jsDeps);
|
|
1447
|
+
}
|
|
1448
|
+
collectNestedManifests(rootDir, jsDeps);
|
|
1449
|
+
return true;
|
|
1450
|
+
};
|
|
1451
|
+
const addPyDep = (pyDeps, name) => {
|
|
1452
|
+
const normalized = name.toLowerCase().replace(/_/g, "-");
|
|
1453
|
+
pyDeps.add(normalized);
|
|
1454
|
+
};
|
|
1455
|
+
const collectFromRequirementsTxt = (rootDir, pyDeps) => {
|
|
1456
|
+
const reqPath = path.join(rootDir, "requirements.txt");
|
|
1457
|
+
if (!fs.existsSync(reqPath)) return false;
|
|
1458
|
+
try {
|
|
1459
|
+
const content = fs.readFileSync(reqPath, "utf-8");
|
|
1460
|
+
for (const line of content.split("\n")) {
|
|
1461
|
+
const trimmed = line.trim();
|
|
1462
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
1463
|
+
const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
|
|
1464
|
+
if (match) addPyDep(pyDeps, match[1]);
|
|
1465
|
+
}
|
|
1466
|
+
return true;
|
|
1467
|
+
} catch {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
const collectFromPyproject = (rootDir, pyDeps) => {
|
|
1472
|
+
const pyprojPath = path.join(rootDir, "pyproject.toml");
|
|
1473
|
+
if (!fs.existsSync(pyprojPath)) return false;
|
|
1474
|
+
try {
|
|
1475
|
+
const content = fs.readFileSync(pyprojPath, "utf-8");
|
|
1476
|
+
const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1477
|
+
if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
|
|
1478
|
+
const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1479
|
+
if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
|
|
1480
|
+
const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
1481
|
+
if (pep621) for (const line of pep621[1].split("\n")) {
|
|
1482
|
+
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
1483
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
1484
|
+
}
|
|
1485
|
+
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1486
|
+
let match = poetryRe.exec(content);
|
|
1487
|
+
while (match !== null) {
|
|
1488
|
+
for (const line of match[1].split("\n")) {
|
|
1489
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1490
|
+
if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
|
|
1491
|
+
}
|
|
1492
|
+
match = poetryRe.exec(content);
|
|
1493
|
+
}
|
|
1494
|
+
return true;
|
|
1495
|
+
} catch {
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
const collectFromPipfile = (rootDir, pyDeps) => {
|
|
1500
|
+
const pipfilePath = path.join(rootDir, "Pipfile");
|
|
1501
|
+
if (!fs.existsSync(pipfilePath)) return false;
|
|
1502
|
+
try {
|
|
1503
|
+
const content = fs.readFileSync(pipfilePath, "utf-8");
|
|
1504
|
+
const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1505
|
+
let match = sectionRe.exec(content);
|
|
1506
|
+
while (match !== null) {
|
|
1507
|
+
for (const line of match[2].split("\n")) {
|
|
1508
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1509
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
1510
|
+
}
|
|
1511
|
+
match = sectionRe.exec(content);
|
|
1512
|
+
}
|
|
1513
|
+
return true;
|
|
1514
|
+
} catch {
|
|
1515
|
+
return false;
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
const loadManifest = (rootDir) => {
|
|
1519
|
+
const jsDeps = /* @__PURE__ */ new Set();
|
|
1520
|
+
const pyDeps = /* @__PURE__ */ new Set();
|
|
1521
|
+
const hasJsManifest = collectJsDeps(rootDir, jsDeps);
|
|
1522
|
+
const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
|
|
1523
|
+
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
1524
|
+
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
1525
|
+
return {
|
|
1526
|
+
jsDeps,
|
|
1527
|
+
pyDeps,
|
|
1528
|
+
hasJsManifest,
|
|
1529
|
+
hasPyManifest: hasReq || hasPyproject || hasPipfile
|
|
1530
|
+
};
|
|
1531
|
+
};
|
|
1532
|
+
const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
|
|
1533
|
+
const isJsBuiltin = (spec) => {
|
|
1534
|
+
return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
|
|
1535
|
+
};
|
|
1536
|
+
const VIRTUAL_MODULE_PREFIXES = [
|
|
1537
|
+
"astro:",
|
|
1538
|
+
"virtual:",
|
|
1539
|
+
"bun:"
|
|
1540
|
+
];
|
|
1541
|
+
const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
|
|
1542
|
+
const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
|
|
1543
|
+
const isLikelyRealImportSpec = (spec) => {
|
|
1544
|
+
if (spec.length === 0) return false;
|
|
1545
|
+
if (TEMPLATE_PLACEHOLDER_RE.test(spec)) return false;
|
|
1546
|
+
if (spec.includes("\\")) return false;
|
|
1547
|
+
if (/\s/.test(spec)) return false;
|
|
1548
|
+
return true;
|
|
1549
|
+
};
|
|
1550
|
+
const packageNameFromImport = (spec) => {
|
|
1551
|
+
if (spec.startsWith("@")) {
|
|
1552
|
+
const parts = spec.split("/");
|
|
1553
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec;
|
|
1554
|
+
}
|
|
1555
|
+
return spec.split("/")[0];
|
|
1556
|
+
};
|
|
1557
|
+
const STATIC_IMPORT_RE = /^\s*import\s+(?:[\w*{},\s]+\s+from\s+)?["']([^"']+)["']/;
|
|
1558
|
+
const DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*["']([^"']+)["']/g;
|
|
1559
|
+
const extractJsImports = (content) => {
|
|
1560
|
+
const lines = content.split("\n");
|
|
1561
|
+
const results = [];
|
|
1562
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1563
|
+
const line = lines[i];
|
|
1564
|
+
const trimmed = line.trim();
|
|
1565
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1566
|
+
const staticMatch = STATIC_IMPORT_RE.exec(line);
|
|
1567
|
+
if (staticMatch && isLikelyRealImportSpec(staticMatch[1])) results.push({
|
|
1568
|
+
spec: staticMatch[1],
|
|
1569
|
+
line: i + 1
|
|
1570
|
+
});
|
|
1571
|
+
DYNAMIC_IMPORT_RE.lastIndex = 0;
|
|
1572
|
+
let dyn = DYNAMIC_IMPORT_RE.exec(line);
|
|
1573
|
+
while (dyn !== null) {
|
|
1574
|
+
if (isLikelyRealImportSpec(dyn[1])) results.push({
|
|
1575
|
+
spec: dyn[1],
|
|
1576
|
+
line: i + 1
|
|
1577
|
+
});
|
|
1578
|
+
dyn = DYNAMIC_IMPORT_RE.exec(line);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return results;
|
|
1582
|
+
};
|
|
1583
|
+
const extractPyImports = (content) => {
|
|
1584
|
+
const lines = content.split("\n");
|
|
1585
|
+
const results = [];
|
|
1586
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1587
|
+
const line = lines[i].trim();
|
|
1588
|
+
if (line.startsWith("#")) continue;
|
|
1589
|
+
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
1590
|
+
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
1591
|
+
results.push({
|
|
1592
|
+
spec: fromMatch[1],
|
|
1593
|
+
line: i + 1
|
|
1594
|
+
});
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
1598
|
+
if (importMatch) for (const raw of importMatch[1].split(",")) {
|
|
1599
|
+
const cleaned = raw.trim().split(/\s+as\s+/)[0];
|
|
1600
|
+
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
1601
|
+
spec: cleaned,
|
|
1602
|
+
line: i + 1
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return results;
|
|
1607
|
+
};
|
|
1608
|
+
const checkJsImport = (spec, manifest) => {
|
|
1609
|
+
if (isJsRelativeOrAbsolute(spec)) return null;
|
|
1610
|
+
if (isJsBuiltin(spec)) return null;
|
|
1611
|
+
if (isJsVirtualModule(spec)) return null;
|
|
1612
|
+
const pkg = packageNameFromImport(spec);
|
|
1613
|
+
if (manifest.jsDeps.has(pkg)) return null;
|
|
1614
|
+
if (pkg.startsWith("@types/")) {
|
|
1615
|
+
const realPkg = pkg.slice(7);
|
|
1616
|
+
if (manifest.jsDeps.has(realPkg)) return null;
|
|
1617
|
+
}
|
|
1618
|
+
return pkg;
|
|
1619
|
+
};
|
|
1620
|
+
const checkPyImport = (spec, manifest) => {
|
|
1621
|
+
const root = spec.split(".")[0];
|
|
1622
|
+
if (PYTHON_STDLIB.has(root)) return null;
|
|
1623
|
+
const normalized = root.toLowerCase().replace(/_/g, "-");
|
|
1624
|
+
if (manifest.pyDeps.has(normalized)) return null;
|
|
1625
|
+
const pipName = PYTHON_IMPORT_TO_PIP[root];
|
|
1626
|
+
if (pipName && manifest.pyDeps.has(pipName)) return null;
|
|
1627
|
+
return root;
|
|
1628
|
+
};
|
|
1629
|
+
const detectHallucinatedImports = async (context) => {
|
|
1630
|
+
const manifest = loadManifest(context.rootDirectory);
|
|
1631
|
+
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
1632
|
+
const diagnostics = [];
|
|
1633
|
+
const files = getSourceFiles(context);
|
|
1634
|
+
for (const filePath of files) {
|
|
1635
|
+
const ext = path.extname(filePath);
|
|
1636
|
+
const isJs = JS_EXTENSIONS$2.has(ext);
|
|
1637
|
+
const isPy = PY_EXTENSIONS$2.has(ext);
|
|
1638
|
+
if (!isJs && !isPy) continue;
|
|
1639
|
+
if (isJs && !manifest.hasJsManifest) continue;
|
|
1640
|
+
if (isPy && !manifest.hasPyManifest) continue;
|
|
1641
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1642
|
+
let content;
|
|
1643
|
+
try {
|
|
1644
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1645
|
+
} catch {
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
1649
|
+
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
1650
|
+
for (const { spec, line } of imports) {
|
|
1651
|
+
const hallucinated = isJs ? checkJsImport(spec, manifest) : checkPyImport(spec, manifest);
|
|
1652
|
+
if (!hallucinated) continue;
|
|
1653
|
+
const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
|
|
1654
|
+
diagnostics.push({
|
|
1655
|
+
filePath: relPath,
|
|
1656
|
+
engine: "ai-slop",
|
|
1657
|
+
rule: "ai-slop/hallucinated-import",
|
|
1658
|
+
severity: "error",
|
|
1659
|
+
message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
|
|
1660
|
+
help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
|
|
1661
|
+
line,
|
|
1662
|
+
column: 1,
|
|
1663
|
+
category: "AI Slop",
|
|
1664
|
+
fixable: false
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return diagnostics;
|
|
1669
|
+
};
|
|
1670
|
+
|
|
907
1671
|
//#endregion
|
|
908
1672
|
//#region src/engines/ai-slop/narrative-comments-patterns.ts
|
|
909
1673
|
const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
|
|
@@ -1020,6 +1784,7 @@ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|re
|
|
|
1020
1784
|
|
|
1021
1785
|
//#endregion
|
|
1022
1786
|
//#region src/engines/ai-slop/narrative-comments.ts
|
|
1787
|
+
const NON_PRODUCTION_DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
|
|
1023
1788
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
1024
1789
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
1025
1790
|
const getCommentSyntax = (ext) => {
|
|
@@ -1048,6 +1813,10 @@ const getMatchedLinePrefix = (line, syntax) => {
|
|
|
1048
1813
|
}
|
|
1049
1814
|
return null;
|
|
1050
1815
|
};
|
|
1816
|
+
const isRustDocCommentLine = (line) => {
|
|
1817
|
+
const trimmed = line.trimStart();
|
|
1818
|
+
return trimmed.startsWith("///") || trimmed.startsWith("//!");
|
|
1819
|
+
};
|
|
1051
1820
|
const collectBlocks = (sourceLines, syntax) => {
|
|
1052
1821
|
const blocks = [];
|
|
1053
1822
|
let i = 0;
|
|
@@ -1063,6 +1832,8 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1063
1832
|
}
|
|
1064
1833
|
let next = i;
|
|
1065
1834
|
while (next < sourceLines.length && sourceLines[next].trim() === "") next += 1;
|
|
1835
|
+
const docCandidates = raw.filter((l) => l.trim().length > 0);
|
|
1836
|
+
const isRustDoc = docCandidates.length > 0 && docCandidates.every((l) => isRustDocCommentLine(l));
|
|
1066
1837
|
blocks.push({
|
|
1067
1838
|
kind: "line",
|
|
1068
1839
|
startLine: start + 1,
|
|
@@ -1070,6 +1841,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1070
1841
|
rawLines: raw,
|
|
1071
1842
|
prose: raw.map(stripLineComment),
|
|
1072
1843
|
hasMeaningfulJsdocTag: false,
|
|
1844
|
+
isRustDoc,
|
|
1073
1845
|
nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
|
|
1074
1846
|
});
|
|
1075
1847
|
continue;
|
|
@@ -1103,6 +1875,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1103
1875
|
rawLines: raw,
|
|
1104
1876
|
prose,
|
|
1105
1877
|
hasMeaningfulJsdocTag: hasMeaningful,
|
|
1878
|
+
isRustDoc: false,
|
|
1106
1879
|
nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
|
|
1107
1880
|
});
|
|
1108
1881
|
continue;
|
|
@@ -1153,6 +1926,22 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
|
1153
1926
|
return false;
|
|
1154
1927
|
};
|
|
1155
1928
|
const looksLikeSuppressDirective = (block) => block.rawLines.some((l) => /\b(biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(l));
|
|
1929
|
+
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
1930
|
+
const looksLikeGoDocComment = (block, ext) => {
|
|
1931
|
+
if (ext !== ".go" || block.kind !== "line") return false;
|
|
1932
|
+
const next = block.nextNonBlankLine;
|
|
1933
|
+
if (!next) return false;
|
|
1934
|
+
const declMatch = GO_DECL_NAME_RE.exec(next.trim());
|
|
1935
|
+
if (!declMatch) return false;
|
|
1936
|
+
return ((block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "") === declMatch[1];
|
|
1937
|
+
};
|
|
1938
|
+
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):/i;
|
|
1939
|
+
const hasDocIndicator = (block) => {
|
|
1940
|
+
const joined = block.prose.join(" ");
|
|
1941
|
+
if (DOC_INDICATOR_RE.test(joined)) return true;
|
|
1942
|
+
for (const l of block.prose) if (/^[-]\s/.test(l)) return true;
|
|
1943
|
+
return false;
|
|
1944
|
+
};
|
|
1156
1945
|
const detectNarrativeInBlock = (block, ext) => {
|
|
1157
1946
|
if (looksLikeLicenseHeader(block)) return {
|
|
1158
1947
|
matched: false,
|
|
@@ -1166,6 +1955,14 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1166
1955
|
matched: false,
|
|
1167
1956
|
reason: ""
|
|
1168
1957
|
};
|
|
1958
|
+
if (block.isRustDoc) return {
|
|
1959
|
+
matched: false,
|
|
1960
|
+
reason: ""
|
|
1961
|
+
};
|
|
1962
|
+
if (looksLikeGoDocComment(block, ext)) return {
|
|
1963
|
+
matched: false,
|
|
1964
|
+
reason: ""
|
|
1965
|
+
};
|
|
1169
1966
|
if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
|
|
1170
1967
|
matched: true,
|
|
1171
1968
|
reason: "decorative separator"
|
|
@@ -1178,11 +1975,16 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1178
1975
|
matched: true,
|
|
1179
1976
|
reason: "bare section label"
|
|
1180
1977
|
};
|
|
1978
|
+
const joined = block.prose.join(" ");
|
|
1979
|
+
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
1980
|
+
if ((hasWhyMarker || hasDocIndicator(block)) && block.kind === "jsdoc") return {
|
|
1981
|
+
matched: false,
|
|
1982
|
+
reason: ""
|
|
1983
|
+
};
|
|
1181
1984
|
if (block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
1182
1985
|
matched: true,
|
|
1183
1986
|
reason: block.kind === "jsdoc" ? "JSDoc preamble before declaration" : "multi-line preamble before declaration"
|
|
1184
1987
|
};
|
|
1185
|
-
const joined = block.prose.join(" ");
|
|
1186
1988
|
if (CROSS_REFERENCE_PHRASES.some((re) => re.test(joined))) return {
|
|
1187
1989
|
matched: true,
|
|
1188
1990
|
reason: "cross-reference commentary"
|
|
@@ -1198,8 +2000,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1198
2000
|
reason: "explanatory preamble"
|
|
1199
2001
|
};
|
|
1200
2002
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
1201
|
-
const joinedProse = block.prose.join(" ");
|
|
1202
|
-
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joinedProse);
|
|
1203
2003
|
if (nonEmptyProseCount >= 5) return {
|
|
1204
2004
|
matched: true,
|
|
1205
2005
|
reason: "long narrative block"
|
|
@@ -1213,84 +2013,330 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1213
2013
|
reason: ""
|
|
1214
2014
|
};
|
|
1215
2015
|
};
|
|
1216
|
-
const detectNarrativeComments = async (context) => {
|
|
1217
|
-
const files = getSourceFiles(context);
|
|
2016
|
+
const detectNarrativeComments = async (context) => {
|
|
2017
|
+
const files = getSourceFiles(context);
|
|
2018
|
+
const diagnostics = [];
|
|
2019
|
+
for (const filePath of files) {
|
|
2020
|
+
const ext = path.extname(filePath);
|
|
2021
|
+
if (!SUPPORTED_EXTS.has(ext)) continue;
|
|
2022
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2023
|
+
const syntax = getCommentSyntax(ext);
|
|
2024
|
+
if (!syntax) continue;
|
|
2025
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2026
|
+
if (NON_PRODUCTION_DIR_PATTERN.test(relativePath)) continue;
|
|
2027
|
+
let content;
|
|
2028
|
+
try {
|
|
2029
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2030
|
+
} catch {
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
const blocks = collectBlocks(content.split("\n"), syntax);
|
|
2034
|
+
for (const block of blocks) {
|
|
2035
|
+
const { matched, reason } = detectNarrativeInBlock(block, ext);
|
|
2036
|
+
if (!matched) continue;
|
|
2037
|
+
diagnostics.push({
|
|
2038
|
+
filePath: relativePath,
|
|
2039
|
+
engine: "ai-slop",
|
|
2040
|
+
rule: "ai-slop/narrative-comment",
|
|
2041
|
+
severity: "warning",
|
|
2042
|
+
message: `Narrative comment block (${reason})`,
|
|
2043
|
+
help: "Remove — narrative/decorative comments belong in PR descriptions, not source. Code should be self-explanatory.",
|
|
2044
|
+
line: block.startLine,
|
|
2045
|
+
column: 0,
|
|
2046
|
+
category: "Comments",
|
|
2047
|
+
fixable: true
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
return diagnostics;
|
|
2052
|
+
};
|
|
2053
|
+
|
|
2054
|
+
//#endregion
|
|
2055
|
+
//#region src/engines/ai-slop/python-patterns.ts
|
|
2056
|
+
const PY_EXTENSIONS$1 = new Set([".py"]);
|
|
2057
|
+
const BARE_EXCEPT_RE = /^\s*except\s*:\s*(?:#.*)?$/;
|
|
2058
|
+
const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\s*:\s*(?:#.*)?$/;
|
|
2059
|
+
const PRINT_RE = /^\s*print\s*\(/;
|
|
2060
|
+
const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
|
|
2061
|
+
const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
|
|
2062
|
+
const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
|
|
2063
|
+
const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
|
|
2064
|
+
const SCRIPT_DIR_NAMES = new Set([
|
|
2065
|
+
"scripts",
|
|
2066
|
+
"bin",
|
|
2067
|
+
".github",
|
|
2068
|
+
"action",
|
|
2069
|
+
"docs",
|
|
2070
|
+
"docs_src",
|
|
2071
|
+
"examples",
|
|
2072
|
+
"example"
|
|
2073
|
+
]);
|
|
2074
|
+
const isInScriptDir = (relPath) => relPath.split(path.sep).some((seg) => SCRIPT_DIR_NAMES.has(seg));
|
|
2075
|
+
const isTutorialFile = (basename) => basename.startsWith("tutorial") && basename.endsWith(".py");
|
|
2076
|
+
const MAIN_GUARD_RE = /^\s*if\s+__name__\s*==\s*["']__main__["']\s*:/;
|
|
2077
|
+
const hasMainGuard = (lines) => lines.some((l) => MAIN_GUARD_RE.test(l));
|
|
2078
|
+
const buildDocstringRanges = (lines) => {
|
|
2079
|
+
const inside = /* @__PURE__ */ new Set();
|
|
2080
|
+
let openDelim = null;
|
|
2081
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2082
|
+
const line = lines[i];
|
|
2083
|
+
if (openDelim) {
|
|
2084
|
+
inside.add(i);
|
|
2085
|
+
if (line.includes(openDelim)) openDelim = null;
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
for (const delim of ["\"\"\"", "'''"]) {
|
|
2089
|
+
const first = line.indexOf(delim);
|
|
2090
|
+
if (first === -1) continue;
|
|
2091
|
+
if (line.indexOf(delim, first + 3) === -1) {
|
|
2092
|
+
openDelim = delim;
|
|
2093
|
+
inside.add(i);
|
|
2094
|
+
break;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
return inside;
|
|
2099
|
+
};
|
|
2100
|
+
const pushFinding = (out, a) => {
|
|
2101
|
+
out.push({
|
|
2102
|
+
filePath: a.relPath,
|
|
2103
|
+
engine: "ai-slop",
|
|
2104
|
+
rule: a.rule,
|
|
2105
|
+
severity: a.severity,
|
|
2106
|
+
message: a.message,
|
|
2107
|
+
help: a.help,
|
|
2108
|
+
line: a.line,
|
|
2109
|
+
column: 1,
|
|
2110
|
+
category: "AI Slop",
|
|
2111
|
+
fixable: false
|
|
2112
|
+
});
|
|
2113
|
+
};
|
|
2114
|
+
const flagBareExcept = (lines, relPath, out) => {
|
|
2115
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2116
|
+
if (!BARE_EXCEPT_RE.test(lines[i])) continue;
|
|
2117
|
+
pushFinding(out, {
|
|
2118
|
+
relPath,
|
|
2119
|
+
rule: "ai-slop/python-bare-except",
|
|
2120
|
+
severity: "warning",
|
|
2121
|
+
message: "Bare `except:` swallows every exception including KeyboardInterrupt and SystemExit.",
|
|
2122
|
+
help: "Catch the specific exception type you actually expect (`except ValueError:`, `except (KeyError, IndexError):`). If you genuinely want everything, `except BaseException:` plus a re-raise or log makes the intent explicit.",
|
|
2123
|
+
line: i + 1
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
const flagBroadExceptWithSilentBody = (lines, relPath, out) => {
|
|
2128
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2129
|
+
const match = BROAD_EXCEPT_RE.exec(lines[i]);
|
|
2130
|
+
if (!match) continue;
|
|
2131
|
+
const trimmedNext = (lines[i + 1] ?? "").trim();
|
|
2132
|
+
if (!(trimmedNext === "pass" || trimmedNext.startsWith("#") && (lines[i + 2] ?? "").trim() === "pass")) continue;
|
|
2133
|
+
pushFinding(out, {
|
|
2134
|
+
relPath,
|
|
2135
|
+
rule: "ai-slop/python-broad-except",
|
|
2136
|
+
severity: "warning",
|
|
2137
|
+
message: `\`except ${match[1]}: pass\` silently drops every exception. Failures vanish without a trace.`,
|
|
2138
|
+
help: "Either narrow the exception class (`except ValueError:`), log the error, or re-raise. If you genuinely intend to swallow, add a comment naming the specific failure mode you're handling — auditors will thank you.",
|
|
2139
|
+
line: i + 1
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
const flagMutableDefaults = (lines, relPath, out) => {
|
|
2144
|
+
let i = 0;
|
|
2145
|
+
while (i < lines.length) {
|
|
2146
|
+
if (!DEF_RE.test(lines[i])) {
|
|
2147
|
+
i++;
|
|
2148
|
+
continue;
|
|
2149
|
+
}
|
|
2150
|
+
const startLine = i;
|
|
2151
|
+
let signature = lines[i];
|
|
2152
|
+
let parenDepth = 0;
|
|
2153
|
+
for (const ch of signature) if (ch === "(") parenDepth++;
|
|
2154
|
+
else if (ch === ")") parenDepth--;
|
|
2155
|
+
while (parenDepth > 0 && i + 1 < lines.length) {
|
|
2156
|
+
i++;
|
|
2157
|
+
signature += `\n${lines[i]}`;
|
|
2158
|
+
for (const ch of lines[i]) if (ch === "(") parenDepth++;
|
|
2159
|
+
else if (ch === ")") parenDepth--;
|
|
2160
|
+
}
|
|
2161
|
+
MUTABLE_DEFAULT_RE.lastIndex = 0;
|
|
2162
|
+
const found = MUTABLE_DEFAULT_RE.exec(signature);
|
|
2163
|
+
if (found) pushFinding(out, {
|
|
2164
|
+
relPath,
|
|
2165
|
+
rule: "ai-slop/python-mutable-default",
|
|
2166
|
+
severity: "warning",
|
|
2167
|
+
message: `Mutable default argument \`${found[1]}=${found[2]}\`. The default is shared across all calls — bugs that look like state-leakage.`,
|
|
2168
|
+
help: "Use `None` as the default and create the mutable value inside the body: `def f(items=None): items = items if items is not None else []`. Standard Python idiom; anything else is the AI agent shortcutting.",
|
|
2169
|
+
line: startLine + 1
|
|
2170
|
+
});
|
|
2171
|
+
i++;
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
const flagPrintInProduction = (lines, relPath, basename, out) => {
|
|
2175
|
+
if (isTestFile$1(relPath, basename) || isScriptOrEntrypoint(basename)) return;
|
|
2176
|
+
if (isInScriptDir(relPath)) return;
|
|
2177
|
+
if (isTutorialFile(basename)) return;
|
|
2178
|
+
if (hasMainGuard(lines)) return;
|
|
2179
|
+
const docstringLines = buildDocstringRanges(lines);
|
|
2180
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2181
|
+
const line = lines[i];
|
|
2182
|
+
if (!PRINT_RE.test(line)) continue;
|
|
2183
|
+
if (line.trim().startsWith("#")) continue;
|
|
2184
|
+
if (docstringLines.has(i)) continue;
|
|
2185
|
+
pushFinding(out, {
|
|
2186
|
+
relPath,
|
|
2187
|
+
rule: "ai-slop/python-print-debug",
|
|
2188
|
+
severity: "warning",
|
|
2189
|
+
message: "`print()` in production code — usually a leftover debug statement.",
|
|
2190
|
+
help: "Use the project's logger (`logging.getLogger(__name__).info(...)`). If this file is genuinely a CLI entry point (typer/click/argparse), it's safe to ignore — but rename to `__main__.py` or move under `scripts/` so the rule skips it next time.",
|
|
2191
|
+
line: i + 1
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
const detectPythonPatterns = async (context) => {
|
|
1218
2196
|
const diagnostics = [];
|
|
2197
|
+
const files = getSourceFiles(context);
|
|
1219
2198
|
for (const filePath of files) {
|
|
1220
|
-
|
|
1221
|
-
if (!SUPPORTED_EXTS.has(ext)) continue;
|
|
2199
|
+
if (!PY_EXTENSIONS$1.has(path.extname(filePath))) continue;
|
|
1222
2200
|
if (isAutoGenerated(filePath)) continue;
|
|
1223
|
-
const syntax = getCommentSyntax(ext);
|
|
1224
|
-
if (!syntax) continue;
|
|
1225
2201
|
let content;
|
|
1226
2202
|
try {
|
|
1227
2203
|
content = fs.readFileSync(filePath, "utf-8");
|
|
1228
2204
|
} catch {
|
|
1229
2205
|
continue;
|
|
1230
2206
|
}
|
|
1231
|
-
const
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
engine: "ai-slop",
|
|
1239
|
-
rule: "ai-slop/narrative-comment",
|
|
1240
|
-
severity: "warning",
|
|
1241
|
-
message: `Narrative comment block (${reason})`,
|
|
1242
|
-
help: "Remove — narrative/decorative comments belong in PR descriptions, not source. Code should be self-explanatory.",
|
|
1243
|
-
line: block.startLine,
|
|
1244
|
-
column: 0,
|
|
1245
|
-
category: "Comments",
|
|
1246
|
-
fixable: true
|
|
1247
|
-
});
|
|
1248
|
-
}
|
|
2207
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2208
|
+
const basename = path.basename(filePath);
|
|
2209
|
+
const lines = content.split("\n");
|
|
2210
|
+
flagBareExcept(lines, relPath, diagnostics);
|
|
2211
|
+
flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
|
|
2212
|
+
flagMutableDefaults(lines, relPath, diagnostics);
|
|
2213
|
+
flagPrintInProduction(lines, relPath, basename, diagnostics);
|
|
1249
2214
|
}
|
|
1250
2215
|
return diagnostics;
|
|
1251
2216
|
};
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
2217
|
+
|
|
2218
|
+
//#endregion
|
|
2219
|
+
//#region src/engines/ai-slop/rust-patterns.ts
|
|
2220
|
+
const RUST_EXTENSIONS = new Set([".rs"]);
|
|
2221
|
+
const UNWRAP_CALL_RE = /\.unwrap\s*\(\s*\)/;
|
|
2222
|
+
const TODO_MACRO_RE = /\b(todo|unimplemented)\s*!\s*\(/;
|
|
2223
|
+
const COMMENT_LINE_RE = /^\s*\/\//;
|
|
2224
|
+
const TEST_ATTR_RE = /^\s*#\s*\[\s*(?:cfg\s*\(\s*test\s*\)|test|tokio::test)/;
|
|
2225
|
+
const WRITELN_UNWRAP_RE = /\b(?:writeln|write)\s*!\s*\([^)]*\)\s*\.unwrap\s*\(\s*\)/;
|
|
2226
|
+
const TEST_BASENAMES = new Set([
|
|
2227
|
+
"tests.rs",
|
|
2228
|
+
"testutil.rs",
|
|
2229
|
+
"test_util.rs",
|
|
2230
|
+
"test_utils.rs",
|
|
2231
|
+
"build.rs"
|
|
2232
|
+
]);
|
|
2233
|
+
const TEST_CRATE_SEGMENT_RE = /(?:^|[-_])tests?(?:$|[-_])/;
|
|
2234
|
+
const isTestFile = (relPath) => {
|
|
2235
|
+
const segments = relPath.split(path.sep);
|
|
2236
|
+
if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
|
|
2237
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
2238
|
+
if (TEST_BASENAMES.has(basename)) return true;
|
|
2239
|
+
return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
|
|
2240
|
+
};
|
|
2241
|
+
const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
|
|
2242
|
+
const UNWRAP_INTENT_LOOKBACK = 2;
|
|
2243
|
+
const hasIntentComment = (lines, lineIdx) => {
|
|
2244
|
+
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - UNWRAP_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE.test(lines[j])) return true;
|
|
2245
|
+
return false;
|
|
2246
|
+
};
|
|
2247
|
+
const buildTestRanges = (lines) => {
|
|
2248
|
+
const ranges = [];
|
|
2249
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2250
|
+
if (!TEST_ATTR_RE.test(lines[i])) continue;
|
|
2251
|
+
const openLine = i;
|
|
2252
|
+
let depth = 0;
|
|
2253
|
+
let started = false;
|
|
2254
|
+
for (let j = i; j < lines.length; j++) {
|
|
2255
|
+
const line = lines[j];
|
|
2256
|
+
for (const ch of line) if (ch === "{") {
|
|
2257
|
+
depth++;
|
|
2258
|
+
started = true;
|
|
2259
|
+
} else if (ch === "}") depth--;
|
|
2260
|
+
if (started && depth === 0) {
|
|
2261
|
+
ranges.push([openLine, j]);
|
|
2262
|
+
i = j;
|
|
2263
|
+
break;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
1261
2266
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
2267
|
+
return ranges;
|
|
2268
|
+
};
|
|
2269
|
+
const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
|
|
2270
|
+
const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
|
|
2271
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2272
|
+
const line = lines[i];
|
|
2273
|
+
if (COMMENT_LINE_RE.test(line)) continue;
|
|
2274
|
+
if (isInRange(testRanges, i)) continue;
|
|
2275
|
+
if (!UNWRAP_CALL_RE.test(line)) continue;
|
|
2276
|
+
if (WRITELN_UNWRAP_RE.test(line)) continue;
|
|
2277
|
+
if (hasIntentComment(lines, i)) continue;
|
|
2278
|
+
out.push({
|
|
2279
|
+
filePath: relPath,
|
|
2280
|
+
engine: "ai-slop",
|
|
2281
|
+
rule: "ai-slop/rust-non-test-unwrap",
|
|
2282
|
+
severity: "warning",
|
|
2283
|
+
message: "`.unwrap()` in non-test code panics on None/Err. Surfaces as a hard crash for the caller.",
|
|
2284
|
+
help: "Use `?` to propagate, `.expect(\"context\")` if you really mean it (and the message names the invariant), or pattern-match the variant you care about. Reserve raw `.unwrap()` for tests and prototypes.",
|
|
2285
|
+
line: i + 1,
|
|
2286
|
+
column: 1,
|
|
2287
|
+
category: "AI Slop",
|
|
2288
|
+
fixable: false
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
const flagTodoMacro = (lines, relPath, out) => {
|
|
2293
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2294
|
+
const line = lines[i];
|
|
2295
|
+
if (COMMENT_LINE_RE.test(line)) continue;
|
|
2296
|
+
const match = TODO_MACRO_RE.exec(line);
|
|
2297
|
+
if (!match) continue;
|
|
2298
|
+
out.push({
|
|
2299
|
+
filePath: relPath,
|
|
2300
|
+
engine: "ai-slop",
|
|
2301
|
+
rule: "ai-slop/rust-todo-stub",
|
|
2302
|
+
severity: "warning",
|
|
2303
|
+
message: `\`${match[1]}!()\` panics at runtime — almost certainly a stub the agent forgot to fill in.`,
|
|
2304
|
+
help: "Implement the missing path or remove it. If the work is genuinely deferred, file a ticket and put the number in a comment next to the macro so it doesn't ship invisibly.",
|
|
2305
|
+
line: i + 1,
|
|
2306
|
+
column: 1,
|
|
2307
|
+
category: "AI Slop",
|
|
2308
|
+
fixable: false
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
const detectRustPatterns = async (context) => {
|
|
2313
|
+
const diagnostics = [];
|
|
2314
|
+
const files = getSourceFiles(context);
|
|
2315
|
+
for (const filePath of files) {
|
|
2316
|
+
if (!RUST_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
2317
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1265
2318
|
let content;
|
|
1266
2319
|
try {
|
|
1267
2320
|
content = fs.readFileSync(filePath, "utf-8");
|
|
1268
2321
|
} catch {
|
|
1269
2322
|
continue;
|
|
1270
2323
|
}
|
|
2324
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
1271
2325
|
const lines = content.split("\n");
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
if (!block) continue;
|
|
1277
|
-
for (let ln = block.startLine; ln <= block.endLine; ln += 1) toRemove.add(ln);
|
|
1278
|
-
const prev = block.startLine - 1;
|
|
1279
|
-
const next = block.endLine + 1;
|
|
1280
|
-
const prevIsBlank = prev >= 1 && lines[prev - 1]?.trim() === "";
|
|
1281
|
-
const nextIsBlank = next <= lines.length && lines[next - 1]?.trim() === "";
|
|
1282
|
-
if (prevIsBlank && nextIsBlank) toRemove.add(prev);
|
|
2326
|
+
if (isExampleFile(relPath)) continue;
|
|
2327
|
+
if (isTestFile(relPath)) {
|
|
2328
|
+
flagTodoMacro(lines, relPath, diagnostics);
|
|
2329
|
+
continue;
|
|
1283
2330
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const newContent = kept.join("\n");
|
|
1287
|
-
if (newContent !== content) fs.writeFileSync(filePath, newContent);
|
|
2331
|
+
flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
|
|
2332
|
+
flagTodoMacro(lines, relPath, diagnostics);
|
|
1288
2333
|
}
|
|
2334
|
+
return diagnostics;
|
|
1289
2335
|
};
|
|
1290
2336
|
|
|
1291
2337
|
//#endregion
|
|
1292
2338
|
//#region src/engines/ai-slop/unused-imports.ts
|
|
1293
|
-
const JS_EXTENSIONS = new Set([
|
|
2339
|
+
const JS_EXTENSIONS$1 = new Set([
|
|
1294
2340
|
".ts",
|
|
1295
2341
|
".tsx",
|
|
1296
2342
|
".js",
|
|
@@ -1420,7 +2466,7 @@ const analyzeFile = (filePath) => {
|
|
|
1420
2466
|
const lines = content.split("\n");
|
|
1421
2467
|
let symbols;
|
|
1422
2468
|
let importLines;
|
|
1423
|
-
if (JS_EXTENSIONS.has(ext)) {
|
|
2469
|
+
if (JS_EXTENSIONS$1.has(ext)) {
|
|
1424
2470
|
const result = extractJsImportedSymbols(lines);
|
|
1425
2471
|
symbols = result.symbols;
|
|
1426
2472
|
importLines = result.importLines;
|
|
@@ -1476,7 +2522,12 @@ const aiSlopEngine = {
|
|
|
1476
2522
|
detectOverAbstraction(context),
|
|
1477
2523
|
detectDeadPatterns(context),
|
|
1478
2524
|
detectUnusedImports(context),
|
|
1479
|
-
detectNarrativeComments(context)
|
|
2525
|
+
detectNarrativeComments(context),
|
|
2526
|
+
detectDuplicateImports(context),
|
|
2527
|
+
detectPythonPatterns(context),
|
|
2528
|
+
detectGoPatterns(context),
|
|
2529
|
+
detectRustPatterns(context),
|
|
2530
|
+
detectHallucinatedImports(context)
|
|
1480
2531
|
]);
|
|
1481
2532
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
1482
2533
|
return {
|
|
@@ -1872,6 +2923,12 @@ const isDataFile = (content) => {
|
|
|
1872
2923
|
const dataLinePattern = /^\s*[{}[\]"']/;
|
|
1873
2924
|
return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
|
|
1874
2925
|
};
|
|
2926
|
+
const TEST_PATH_RE = /(?:^|\/)(?:tests?|spec|specs|__tests__|__spec__|src\/test)\//i;
|
|
2927
|
+
const TEST_BASENAME_RE = /(?:^|[/.])(?:test_[\w-]+\.(?:py|rb)|[\w-]+_(?:test|spec)\.(?:py|rb|go|rs)|[\w-]+\.(?:test|spec)\.(?:[jt]sx?|mjs|cjs)|conftest\.py|[A-Z]\w*Tests?\.(?:java|cs|php))$/;
|
|
2928
|
+
const MIGRATION_PATH_RE = /(?:^|\/)(?:migrations?|migrate|prisma\/migrations|db\/migrate)\//i;
|
|
2929
|
+
const FIXTURE_PATH_RE = /(?:^|\/)(?:__fixtures__|__snapshots__|__mocks__|fixtures?|snapshots?|seeds?|stubs?)\//i;
|
|
2930
|
+
const GENERATED_PATH_RE = /(?:^|\/)(?:generated|gen|build|dist|out|target|coverage|node_modules|vendor|\.next|\.nuxt|\.svelte-kit)\//i;
|
|
2931
|
+
const isExemptFromComplexity = (relativePath) => TEST_PATH_RE.test(relativePath) || TEST_BASENAME_RE.test(relativePath) || MIGRATION_PATH_RE.test(relativePath) || FIXTURE_PATH_RE.test(relativePath) || GENERATED_PATH_RE.test(relativePath);
|
|
1875
2932
|
const analyzeFunctions = (content, ext) => {
|
|
1876
2933
|
const lines = content.split("\n");
|
|
1877
2934
|
const functions = [];
|
|
@@ -1900,13 +2957,13 @@ const checkFileDiagnostics = (relativePath, content, limits) => {
|
|
|
1900
2957
|
const lineCount = content.split("\n").length;
|
|
1901
2958
|
const ext = path.extname(relativePath).toLowerCase();
|
|
1902
2959
|
if (isDataFile(content)) return results;
|
|
1903
|
-
const
|
|
1904
|
-
if (lineCount >
|
|
2960
|
+
const configuredMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
|
|
2961
|
+
if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
|
|
1905
2962
|
filePath: relativePath,
|
|
1906
2963
|
engine: "code-quality",
|
|
1907
2964
|
rule: "complexity/file-too-large",
|
|
1908
2965
|
severity: "warning",
|
|
1909
|
-
message: `File has ${lineCount} lines (max: ${
|
|
2966
|
+
message: `File has ${lineCount} lines (max: ${configuredMax})`,
|
|
1910
2967
|
help: "Consider splitting this file into smaller modules",
|
|
1911
2968
|
line: 0,
|
|
1912
2969
|
column: 0,
|
|
@@ -1956,13 +3013,14 @@ const checkFunctionDiagnostics = (relativePath, fn, limits) => {
|
|
|
1956
3013
|
return results;
|
|
1957
3014
|
};
|
|
1958
3015
|
const checkFileComplexity = (filePath, rootDirectory, limits) => {
|
|
3016
|
+
const relativePath = path.relative(rootDirectory, filePath);
|
|
3017
|
+
if (isExemptFromComplexity(relativePath)) return [];
|
|
1959
3018
|
let content;
|
|
1960
3019
|
try {
|
|
1961
3020
|
content = fs.readFileSync(filePath, "utf-8");
|
|
1962
3021
|
} catch {
|
|
1963
3022
|
return [];
|
|
1964
3023
|
}
|
|
1965
|
-
const relativePath = path.relative(rootDirectory, filePath);
|
|
1966
3024
|
const ext = path.extname(filePath).toLowerCase();
|
|
1967
3025
|
const diagnostics = checkFileDiagnostics(relativePath, content, limits);
|
|
1968
3026
|
for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
|
|
@@ -3538,7 +4596,10 @@ const lintEngine = {
|
|
|
3538
4596
|
const diagnostics = [];
|
|
3539
4597
|
const { languages, installedTools } = context;
|
|
3540
4598
|
const promises = [];
|
|
3541
|
-
if (languages.includes("typescript") || languages.includes("javascript"))
|
|
4599
|
+
if (languages.includes("typescript") || languages.includes("javascript")) {
|
|
4600
|
+
promises.push(runOxlint(context));
|
|
4601
|
+
if (context.config.lint.typecheck) promises.push(import("./typecheck-B1MXNAy-.js").then((mod) => mod.runTypecheck(context)));
|
|
4602
|
+
}
|
|
3542
4603
|
if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
|
|
3543
4604
|
if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
|
|
3544
4605
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
@@ -4556,6 +5617,7 @@ const runScopedScan = async (cwd, filePaths) => {
|
|
|
4556
5617
|
audit: false,
|
|
4557
5618
|
auditTimeout: 0
|
|
4558
5619
|
},
|
|
5620
|
+
lint: { typecheck: false },
|
|
4559
5621
|
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
4560
5622
|
}
|
|
4561
5623
|
}, {
|
|
@@ -4594,6 +5656,9 @@ const readIfExists = (targetPath) => {
|
|
|
4594
5656
|
|
|
4595
5657
|
//#endregion
|
|
4596
5658
|
//#region src/hooks/quality-gate/baseline.ts
|
|
5659
|
+
const fingerprintDiagnostic = (d, rootDirectory) => {
|
|
5660
|
+
return `${path.isAbsolute(d.filePath) ? path.relative(rootDirectory, d.filePath) : d.filePath}:${d.line}:${d.rule}`;
|
|
5661
|
+
};
|
|
4597
5662
|
const BASELINE_REL = path.join(".aislop", "baseline.json");
|
|
4598
5663
|
const baselinePath = (cwd) => path.join(cwd, BASELINE_REL);
|
|
4599
5664
|
const readBaseline = (cwd) => {
|
|
@@ -4601,8 +5666,16 @@ const readBaseline = (cwd) => {
|
|
|
4601
5666
|
if (!raw) return null;
|
|
4602
5667
|
try {
|
|
4603
5668
|
const parsed = JSON.parse(raw);
|
|
4604
|
-
if (parsed.schema !== "aislop.baseline.v1") return null;
|
|
4605
|
-
return
|
|
5669
|
+
if (parsed.schema !== "aislop.baseline.v2" && parsed.schema !== "aislop.baseline.v1") return null;
|
|
5670
|
+
return {
|
|
5671
|
+
schema: "aislop.baseline.v2",
|
|
5672
|
+
updatedAt: parsed.updatedAt ?? "",
|
|
5673
|
+
score: parsed.score ?? 0,
|
|
5674
|
+
byEngine: parsed.byEngine ?? {},
|
|
5675
|
+
fileCount: parsed.fileCount ?? 0,
|
|
5676
|
+
commit: parsed.commit,
|
|
5677
|
+
findingFingerprints: parsed.findingFingerprints ?? []
|
|
5678
|
+
};
|
|
4606
5679
|
} catch {
|
|
4607
5680
|
return null;
|
|
4608
5681
|
}
|
|
@@ -4626,7 +5699,8 @@ const captureBaseline = async (cwd) => {
|
|
|
4626
5699
|
security: {
|
|
4627
5700
|
audit: false,
|
|
4628
5701
|
auditTimeout: 0
|
|
4629
|
-
}
|
|
5702
|
+
},
|
|
5703
|
+
lint: { typecheck: false }
|
|
4630
5704
|
}
|
|
4631
5705
|
}, {
|
|
4632
5706
|
format: config.engines.format,
|
|
@@ -4643,12 +5717,14 @@ const captureBaseline = async (cwd) => {
|
|
|
4643
5717
|
const { score: engineScore } = calculateScore(diagnostics.filter((d) => r.diagnostics.includes(d)), config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
4644
5718
|
byEngine[r.engine] = engineScore;
|
|
4645
5719
|
}
|
|
5720
|
+
const findingFingerprints = diagnostics.filter((d) => d.severity === "error" || d.severity === "warning").map((d) => fingerprintDiagnostic(d, project.rootDirectory));
|
|
4646
5721
|
const target = writeBaseline(cwd, {
|
|
4647
|
-
schema: "aislop.baseline.
|
|
5722
|
+
schema: "aislop.baseline.v2",
|
|
4648
5723
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4649
5724
|
score,
|
|
4650
5725
|
byEngine,
|
|
4651
|
-
fileCount: project.sourceFileCount
|
|
5726
|
+
fileCount: project.sourceFileCount,
|
|
5727
|
+
findingFingerprints
|
|
4652
5728
|
});
|
|
4653
5729
|
return {
|
|
4654
5730
|
score,
|
|
@@ -4737,7 +5813,10 @@ const runClaudeHook = async (deps = {}) => {
|
|
|
4737
5813
|
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
4738
5814
|
const baseline = readBaseline(cwd);
|
|
4739
5815
|
appendSessionFiles(cwd, files);
|
|
4740
|
-
const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline
|
|
5816
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline ? {
|
|
5817
|
+
score: baseline.score,
|
|
5818
|
+
findingFingerprints: baseline.findingFingerprints
|
|
5819
|
+
} : void 0);
|
|
4741
5820
|
const envelope = renderClaudeOutput(JSON.stringify(feedback));
|
|
4742
5821
|
write(JSON.stringify(envelope));
|
|
4743
5822
|
return 0;
|
|
@@ -4747,6 +5826,42 @@ const runClaudeHook = async (deps = {}) => {
|
|
|
4747
5826
|
release();
|
|
4748
5827
|
}
|
|
4749
5828
|
};
|
|
5829
|
+
const parseClaudeFileChangedStdin = (raw) => {
|
|
5830
|
+
if (!raw.trim()) return {};
|
|
5831
|
+
try {
|
|
5832
|
+
return JSON.parse(raw);
|
|
5833
|
+
} catch {
|
|
5834
|
+
return {};
|
|
5835
|
+
}
|
|
5836
|
+
};
|
|
5837
|
+
const runClaudeFileChangedHook = async (deps = {}) => {
|
|
5838
|
+
const getStdin = deps.stdin ?? readStdin$2;
|
|
5839
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
5840
|
+
const input = parseClaudeFileChangedStdin(await getStdin());
|
|
5841
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
5842
|
+
const release = acquireHookLock(cwd);
|
|
5843
|
+
if (!release) return 0;
|
|
5844
|
+
try {
|
|
5845
|
+
const result = await captureBaseline(cwd);
|
|
5846
|
+
const changed = input.file_path ? path.relative(cwd, input.file_path) || input.file_path : "<unknown>";
|
|
5847
|
+
const envelope = renderClaudeOutput(JSON.stringify({
|
|
5848
|
+
schema: "aislop.hook.v2",
|
|
5849
|
+
event: "file_changed",
|
|
5850
|
+
file: changed,
|
|
5851
|
+
message: `Watched file changed (${changed}). aislop refreshed the baseline — score: ${result.score}.`,
|
|
5852
|
+
baseline: {
|
|
5853
|
+
score: result.score,
|
|
5854
|
+
fileCount: result.fileCount
|
|
5855
|
+
}
|
|
5856
|
+
}));
|
|
5857
|
+
write(JSON.stringify(envelope));
|
|
5858
|
+
return 0;
|
|
5859
|
+
} catch {
|
|
5860
|
+
return 0;
|
|
5861
|
+
} finally {
|
|
5862
|
+
release();
|
|
5863
|
+
}
|
|
5864
|
+
};
|
|
4750
5865
|
const parseClaudeStopStdin = (raw) => {
|
|
4751
5866
|
if (!raw.trim()) return {};
|
|
4752
5867
|
try {
|
|
@@ -4769,7 +5884,10 @@ const runClaudeStopHook = async (deps = {}) => {
|
|
|
4769
5884
|
if (!release) return 0;
|
|
4770
5885
|
try {
|
|
4771
5886
|
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, sessionFiles);
|
|
4772
|
-
const feedback = buildFeedback(diagnostics, score, rootDirectory,
|
|
5887
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, {
|
|
5888
|
+
score: baseline.score,
|
|
5889
|
+
findingFingerprints: baseline.findingFingerprints
|
|
5890
|
+
});
|
|
4773
5891
|
if (!feedback.regressed) {
|
|
4774
5892
|
clearSessionFiles(cwd);
|
|
4775
5893
|
return 0;
|
|
@@ -4897,7 +6015,7 @@ const AISLOP_MD_BODY = `# aislop — agent instructions
|
|
|
4897
6015
|
|
|
4898
6016
|
## On every edit
|
|
4899
6017
|
|
|
4900
|
-
A PostToolUse hook runs \`aislop hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`AislopFeedback\` (schema \`aislop.hook.
|
|
6018
|
+
A PostToolUse hook runs \`aislop hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`AislopFeedback\` (schema \`aislop.hook.v2\` — score, baseline, delta, regressed, counts, findings, newSinceBaseline, suggestedActions). Act on them the same turn; the \`suggestedActions\` field tells you exactly what to run next.
|
|
4901
6019
|
|
|
4902
6020
|
## Severity ladder
|
|
4903
6021
|
|
|
@@ -5134,6 +6252,25 @@ const buildStopHookGroup = () => {
|
|
|
5134
6252
|
}]
|
|
5135
6253
|
};
|
|
5136
6254
|
};
|
|
6255
|
+
const FILE_CHANGED_MATCHER = ".aislop/config.yml|.aislop/rules.yml|package.json";
|
|
6256
|
+
const buildFileChangedHookGroup = () => {
|
|
6257
|
+
const hashBody = JSON.stringify({
|
|
6258
|
+
command: "aislop hook claude --on-file-changed",
|
|
6259
|
+
matcher: FILE_CHANGED_MATCHER
|
|
6260
|
+
});
|
|
6261
|
+
return {
|
|
6262
|
+
matcher: FILE_CHANGED_MATCHER,
|
|
6263
|
+
hooks: [{
|
|
6264
|
+
type: "command",
|
|
6265
|
+
command: "aislop hook claude --on-file-changed",
|
|
6266
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
6267
|
+
v: 1,
|
|
6268
|
+
managed: true,
|
|
6269
|
+
hash: sentinelHash(hashBody)
|
|
6270
|
+
}
|
|
6271
|
+
}]
|
|
6272
|
+
};
|
|
6273
|
+
};
|
|
5137
6274
|
const renderSettings$1 = (existingRaw, qualityGate) => {
|
|
5138
6275
|
let obj = {};
|
|
5139
6276
|
if (existingRaw) try {
|
|
@@ -5142,6 +6279,7 @@ const renderSettings$1 = (existingRaw, qualityGate) => {
|
|
|
5142
6279
|
obj = {};
|
|
5143
6280
|
}
|
|
5144
6281
|
let next = upsertHookGroup(obj, "PostToolUse", buildHookGroup$1());
|
|
6282
|
+
next = upsertHookGroup(next, "FileChanged", buildFileChangedHookGroup());
|
|
5145
6283
|
if (qualityGate) next = upsertHookGroup(next, "Stop", buildStopHookGroup());
|
|
5146
6284
|
else next = removeAislopEntries(next, "Stop").next;
|
|
5147
6285
|
return `${JSON.stringify(next, null, 2)}\n`;
|
|
@@ -5150,7 +6288,7 @@ const installClaude = (opts) => {
|
|
|
5150
6288
|
const paths = resolveClaudePaths(opts);
|
|
5151
6289
|
const result = emptyResult();
|
|
5152
6290
|
const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
|
|
5153
|
-
applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse
|
|
6291
|
+
applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse + FileChanged hooks");
|
|
5154
6292
|
const mdHash = sentinelHash(AISLOP_MD_BODY);
|
|
5155
6293
|
const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, mdHash);
|
|
5156
6294
|
applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
|
|
@@ -5180,7 +6318,9 @@ const uninstallClaude = (opts) => {
|
|
|
5180
6318
|
} catch {
|
|
5181
6319
|
obj = {};
|
|
5182
6320
|
}
|
|
5183
|
-
const
|
|
6321
|
+
const afterPostToolUse = removeAislopEntries(obj, "PostToolUse").next;
|
|
6322
|
+
const afterFileChanged = removeAislopEntries(afterPostToolUse, "FileChanged").next;
|
|
6323
|
+
const stripped = removeAislopEntries(afterFileChanged, "Stop").next;
|
|
5184
6324
|
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
5185
6325
|
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
|
|
5186
6326
|
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
@@ -5189,7 +6329,7 @@ const uninstallClaude = (opts) => {
|
|
|
5189
6329
|
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
5190
6330
|
else result.skipped.push(paths.aislopMd);
|
|
5191
6331
|
const claudeMd = readIfExists(paths.claudeMd);
|
|
5192
|
-
if (claudeMd
|
|
6332
|
+
if (claudeMd?.includes("@AISLOP.md")) {
|
|
5193
6333
|
const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
5194
6334
|
applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
5195
6335
|
} else result.skipped.push(paths.claudeMd);
|
|
@@ -5750,7 +6890,9 @@ const hookRun = async (agent, flags) => {
|
|
|
5750
6890
|
process.exit(0);
|
|
5751
6891
|
}
|
|
5752
6892
|
let exitCode = 0;
|
|
5753
|
-
if (agent === "claude")
|
|
6893
|
+
if (agent === "claude") if (flags?.onFileChanged) exitCode = await runClaudeFileChangedHook();
|
|
6894
|
+
else if (flags?.stop) exitCode = await runClaudeStopHook();
|
|
6895
|
+
else exitCode = await runClaudeHook();
|
|
5754
6896
|
else if (agent === "cursor") exitCode = await runCursorHook();
|
|
5755
6897
|
else if (agent === "gemini") exitCode = await runGeminiHook();
|
|
5756
6898
|
else {
|
|
@@ -5901,8 +7043,11 @@ const registerCallbacks = (hook) => {
|
|
|
5901
7043
|
hook.command("baseline").description("Capture the current project score as the quality-gate baseline").action(async () => {
|
|
5902
7044
|
await hookBaseline();
|
|
5903
7045
|
});
|
|
5904
|
-
hook.command("claude").description("Internal: Claude Code PostToolUse / Stop callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").action(async (opts) => {
|
|
5905
|
-
await hookRun("claude", {
|
|
7046
|
+
hook.command("claude").description("Internal: Claude Code PostToolUse / Stop / FileChanged callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").option("--on-file-changed", "run in FileChanged mode (refresh baseline on watched file change)").action(async (opts) => {
|
|
7047
|
+
await hookRun("claude", {
|
|
7048
|
+
stop: Boolean(opts.stop),
|
|
7049
|
+
onFileChanged: Boolean(opts.onFileChanged)
|
|
7050
|
+
});
|
|
5906
7051
|
});
|
|
5907
7052
|
hook.command("cursor").description("Internal: Cursor afterFileEdit callback (reads stdin)").action(async () => {
|
|
5908
7053
|
await hookRun("cursor");
|
|
@@ -5918,6 +7063,83 @@ const registerHookCommand = (program) => {
|
|
|
5918
7063
|
registerCallbacks(hook);
|
|
5919
7064
|
};
|
|
5920
7065
|
|
|
7066
|
+
//#endregion
|
|
7067
|
+
//#region src/commands/badge.ts
|
|
7068
|
+
const GITHUB_REMOTE_RE = /^(?:git@github\.com:|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/.\s]+?)(?:\.git)?\s*$/;
|
|
7069
|
+
const renderBadgeOutput = ({ owner, repo, svgUrl, pageUrl }) => {
|
|
7070
|
+
const slug = `${owner}/${repo}`;
|
|
7071
|
+
const markdown = `[](${pageUrl})`;
|
|
7072
|
+
return [
|
|
7073
|
+
``,
|
|
7074
|
+
` Repository: ${slug}`,
|
|
7075
|
+
` Badge URL: ${svgUrl}`,
|
|
7076
|
+
``,
|
|
7077
|
+
` Markdown:`,
|
|
7078
|
+
``,
|
|
7079
|
+
` ${markdown}`,
|
|
7080
|
+
``,
|
|
7081
|
+
` Drop the line above into your README. The badge auto-updates after every public scan.`,
|
|
7082
|
+
``
|
|
7083
|
+
].join("\n");
|
|
7084
|
+
};
|
|
7085
|
+
const detectGithubSlugFromGit = (directory) => {
|
|
7086
|
+
let raw;
|
|
7087
|
+
try {
|
|
7088
|
+
raw = execSync("git remote get-url origin", {
|
|
7089
|
+
cwd: path.resolve(directory),
|
|
7090
|
+
encoding: "utf-8",
|
|
7091
|
+
stdio: [
|
|
7092
|
+
"ignore",
|
|
7093
|
+
"pipe",
|
|
7094
|
+
"ignore"
|
|
7095
|
+
]
|
|
7096
|
+
});
|
|
7097
|
+
} catch {
|
|
7098
|
+
return null;
|
|
7099
|
+
}
|
|
7100
|
+
const match = raw.trim().match(GITHUB_REMOTE_RE);
|
|
7101
|
+
if (!match) return null;
|
|
7102
|
+
const owner = match[1];
|
|
7103
|
+
const repo = match[2];
|
|
7104
|
+
if (!owner || !repo) return null;
|
|
7105
|
+
return {
|
|
7106
|
+
owner,
|
|
7107
|
+
repo
|
|
7108
|
+
};
|
|
7109
|
+
};
|
|
7110
|
+
const badgeCommand = async (options = {}) => {
|
|
7111
|
+
let owner = options.owner?.trim();
|
|
7112
|
+
let repo = options.repo?.trim();
|
|
7113
|
+
if (!owner || !repo) {
|
|
7114
|
+
const detected = detectGithubSlugFromGit(options.directory ?? ".");
|
|
7115
|
+
if (!detected) throw new Error("Could not detect a GitHub remote. Run from a repo with `git remote get-url origin` set, or pass --owner and --repo.");
|
|
7116
|
+
owner ??= detected.owner;
|
|
7117
|
+
repo ??= detected.repo;
|
|
7118
|
+
}
|
|
7119
|
+
const svgUrl = `https://badges.scanaislop.com/score/${owner}/${repo}.svg`;
|
|
7120
|
+
const pageUrl = `https://scanaislop.com/${owner}/${repo}`;
|
|
7121
|
+
const output = renderBadgeOutput({
|
|
7122
|
+
owner,
|
|
7123
|
+
repo,
|
|
7124
|
+
svgUrl,
|
|
7125
|
+
pageUrl
|
|
7126
|
+
});
|
|
7127
|
+
if (options.json) process.stdout.write(JSON.stringify({
|
|
7128
|
+
owner,
|
|
7129
|
+
repo,
|
|
7130
|
+
svgUrl,
|
|
7131
|
+
pageUrl
|
|
7132
|
+
}) + "\n");
|
|
7133
|
+
else process.stdout.write(output);
|
|
7134
|
+
return {
|
|
7135
|
+
owner,
|
|
7136
|
+
repo,
|
|
7137
|
+
svgUrl,
|
|
7138
|
+
pageUrl,
|
|
7139
|
+
output
|
|
7140
|
+
};
|
|
7141
|
+
};
|
|
7142
|
+
|
|
5921
7143
|
//#endregion
|
|
5922
7144
|
//#region src/ui/symbols.ts
|
|
5923
7145
|
const TTY = {
|
|
@@ -6334,7 +7556,7 @@ const renderCleanRun = (input, deps = {}) => {
|
|
|
6334
7556
|
|
|
6335
7557
|
//#endregion
|
|
6336
7558
|
//#region src/version.ts
|
|
6337
|
-
const APP_VERSION = "0.
|
|
7559
|
+
const APP_VERSION = "0.8.0";
|
|
6338
7560
|
|
|
6339
7561
|
//#endregion
|
|
6340
7562
|
//#region src/utils/telemetry.ts
|
|
@@ -6489,6 +7711,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
6489
7711
|
const engineConfig = {
|
|
6490
7712
|
quality: config.quality,
|
|
6491
7713
|
security: config.security,
|
|
7714
|
+
lint: config.lint,
|
|
6492
7715
|
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
6493
7716
|
};
|
|
6494
7717
|
const gridRows = ALL_ENGINE_NAMES.filter((engine) => config.engines[engine] !== false).map((engine) => ({
|
|
@@ -6556,7 +7779,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
6556
7779
|
});
|
|
6557
7780
|
}
|
|
6558
7781
|
if (options.json) {
|
|
6559
|
-
const { buildJsonOutput } = await import("./json-
|
|
7782
|
+
const { buildJsonOutput } = await import("./json-BbMwrgyd.js");
|
|
6560
7783
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
6561
7784
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
6562
7785
|
return { exitCode };
|
|
@@ -6767,16 +7990,29 @@ const planFormat = (ctx) => {
|
|
|
6767
7990
|
skipReason: "no supported language"
|
|
6768
7991
|
};
|
|
6769
7992
|
};
|
|
6770
|
-
const
|
|
6771
|
-
const
|
|
6772
|
-
|
|
6773
|
-
|
|
7993
|
+
const findLocalTsc = (root) => {
|
|
7994
|
+
const candidate = path.join(root, "node_modules", ".bin", "tsc");
|
|
7995
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
7996
|
+
};
|
|
7997
|
+
const withTypecheckSuffix = (baseTool, ctx) => {
|
|
7998
|
+
if (!ctx.config.lint?.typecheck) return {
|
|
7999
|
+
tool: baseTool,
|
|
6774
8000
|
status: "ok"
|
|
6775
8001
|
};
|
|
6776
|
-
if (
|
|
6777
|
-
tool:
|
|
8002
|
+
if (findLocalTsc(ctx.rootDirectory)) return {
|
|
8003
|
+
tool: `${baseTool} + tsc`,
|
|
6778
8004
|
status: "ok"
|
|
6779
8005
|
};
|
|
8006
|
+
return {
|
|
8007
|
+
tool: `${baseTool} + tsc not found`,
|
|
8008
|
+
status: "missing",
|
|
8009
|
+
remediation: "Install TypeScript locally (pnpm add -D typescript), or set lint.typecheck: false in .aislop/config.yml."
|
|
8010
|
+
};
|
|
8011
|
+
};
|
|
8012
|
+
const planLint = (ctx) => {
|
|
8013
|
+
const { languages, frameworks, installedTools } = ctx.projectInfo;
|
|
8014
|
+
if (frameworks.includes("expo")) return withTypecheckSuffix("expo-doctor + oxlint (bundled)", ctx);
|
|
8015
|
+
if (hasJsLike(languages)) return withTypecheckSuffix("oxlint (bundled)", ctx);
|
|
6780
8016
|
return firstMatching(languages, installedTools, LINT_SPECS) ?? {
|
|
6781
8017
|
tool: "no linter",
|
|
6782
8018
|
status: "skipped",
|
|
@@ -7371,6 +8607,212 @@ const applyEditsAndCollapse = (lines, linesToRemove, lineReplacements) => {
|
|
|
7371
8607
|
return collapsed.join("\n");
|
|
7372
8608
|
};
|
|
7373
8609
|
|
|
8610
|
+
//#endregion
|
|
8611
|
+
//#region src/engines/ai-slop/duplicate-imports-fix.ts
|
|
8612
|
+
const JS_EXTENSIONS = new Set([
|
|
8613
|
+
".ts",
|
|
8614
|
+
".tsx",
|
|
8615
|
+
".js",
|
|
8616
|
+
".jsx",
|
|
8617
|
+
".mjs",
|
|
8618
|
+
".cjs"
|
|
8619
|
+
]);
|
|
8620
|
+
const IMPORT_FROM_RE = /^\s*import\s+(.*?)\s+from\s+["']([^"']+)["']\s*;?\s*$/;
|
|
8621
|
+
const SIDE_EFFECT_RE = /^\s*import\s+["']([^"']+)["']\s*;?\s*$/;
|
|
8622
|
+
const parseNamedClause = (clause) => {
|
|
8623
|
+
const inner = clause.trim().slice(1, -1).trim();
|
|
8624
|
+
if (inner.length === 0) return [];
|
|
8625
|
+
const items = [];
|
|
8626
|
+
for (const part of inner.split(",")) {
|
|
8627
|
+
const trimmed = part.trim();
|
|
8628
|
+
if (!trimmed) continue;
|
|
8629
|
+
let isType = false;
|
|
8630
|
+
let working = trimmed;
|
|
8631
|
+
if (/^type\s+/.test(working)) {
|
|
8632
|
+
isType = true;
|
|
8633
|
+
working = working.replace(/^type\s+/, "");
|
|
8634
|
+
}
|
|
8635
|
+
const aliasMatch = working.match(/^(\w+)\s+as\s+(\w+)$/);
|
|
8636
|
+
if (aliasMatch) {
|
|
8637
|
+
items.push({
|
|
8638
|
+
name: aliasMatch[1],
|
|
8639
|
+
alias: aliasMatch[2],
|
|
8640
|
+
isType
|
|
8641
|
+
});
|
|
8642
|
+
continue;
|
|
8643
|
+
}
|
|
8644
|
+
if (/^\w+$/.test(working)) items.push({
|
|
8645
|
+
name: working,
|
|
8646
|
+
isType
|
|
8647
|
+
});
|
|
8648
|
+
}
|
|
8649
|
+
return items;
|
|
8650
|
+
};
|
|
8651
|
+
const parseImportClause = (clause) => {
|
|
8652
|
+
let rest = clause.trim();
|
|
8653
|
+
let isTypeOnly = false;
|
|
8654
|
+
if (/^type\s+/.test(rest)) {
|
|
8655
|
+
isTypeOnly = true;
|
|
8656
|
+
rest = rest.replace(/^type\s+/, "");
|
|
8657
|
+
}
|
|
8658
|
+
const out = {
|
|
8659
|
+
named: [],
|
|
8660
|
+
isTypeOnly
|
|
8661
|
+
};
|
|
8662
|
+
const defMatch = rest.match(/^([A-Za-z_$][\w$]*)\s*(?:,\s*(.+))?$/);
|
|
8663
|
+
if (defMatch && !rest.startsWith("{") && !rest.startsWith("*")) {
|
|
8664
|
+
out.default = defMatch[1];
|
|
8665
|
+
rest = defMatch[2]?.trim() ?? "";
|
|
8666
|
+
}
|
|
8667
|
+
if (rest.startsWith("*")) {
|
|
8668
|
+
const nsMatch = rest.match(/^\*\s+as\s+(\w+)/);
|
|
8669
|
+
if (nsMatch) out.namespace = nsMatch[1];
|
|
8670
|
+
return out;
|
|
8671
|
+
}
|
|
8672
|
+
if (rest.startsWith("{")) out.named = parseNamedClause(rest);
|
|
8673
|
+
return out;
|
|
8674
|
+
};
|
|
8675
|
+
const parseImportLine = (line, lineIndex) => {
|
|
8676
|
+
const sideEffect = line.match(SIDE_EFFECT_RE);
|
|
8677
|
+
if (sideEffect) return {
|
|
8678
|
+
lineIndex,
|
|
8679
|
+
module: sideEffect[1],
|
|
8680
|
+
named: [],
|
|
8681
|
+
isTypeOnly: false,
|
|
8682
|
+
isSideEffect: true
|
|
8683
|
+
};
|
|
8684
|
+
const m = line.match(IMPORT_FROM_RE);
|
|
8685
|
+
if (!m) return null;
|
|
8686
|
+
return {
|
|
8687
|
+
lineIndex,
|
|
8688
|
+
module: m[2],
|
|
8689
|
+
isSideEffect: false,
|
|
8690
|
+
...parseImportClause(m[1])
|
|
8691
|
+
};
|
|
8692
|
+
};
|
|
8693
|
+
const formatNamed = (n, stripType) => {
|
|
8694
|
+
const prefix = n.isType && !stripType ? "type " : "";
|
|
8695
|
+
const suffix = n.alias ? ` as ${n.alias}` : "";
|
|
8696
|
+
return `${prefix}${n.name}${suffix}`;
|
|
8697
|
+
};
|
|
8698
|
+
const mergeImports = (group) => {
|
|
8699
|
+
if (group.some((s) => s.isSideEffect)) return null;
|
|
8700
|
+
if (group.some((s) => s.namespace !== void 0)) return null;
|
|
8701
|
+
if (group.some((s) => s.isTypeOnly && s.default !== void 0)) return null;
|
|
8702
|
+
const defaults = group.map((s) => s.default).filter((d) => d !== void 0);
|
|
8703
|
+
const uniqueDefaults = Array.from(new Set(defaults));
|
|
8704
|
+
if (uniqueDefaults.length > 1) return null;
|
|
8705
|
+
const defaultName = uniqueDefaults[0];
|
|
8706
|
+
const merged = /* @__PURE__ */ new Map();
|
|
8707
|
+
for (const stmt of group) for (const nm of stmt.named) {
|
|
8708
|
+
const key = nm.alias ?? nm.name;
|
|
8709
|
+
const isType = nm.isType || stmt.isTypeOnly;
|
|
8710
|
+
const existing = merged.get(key);
|
|
8711
|
+
if (!existing) merged.set(key, {
|
|
8712
|
+
...nm,
|
|
8713
|
+
isType
|
|
8714
|
+
});
|
|
8715
|
+
else existing.isType = existing.isType && isType;
|
|
8716
|
+
}
|
|
8717
|
+
const insertionOrder = Array.from(merged.values());
|
|
8718
|
+
const namedList = [...insertionOrder.filter((n) => !n.isType), ...insertionOrder.filter((n) => n.isType)];
|
|
8719
|
+
const allTypeOnly = namedList.length > 0 && namedList.every((n) => n.isType);
|
|
8720
|
+
const module = group[0].module;
|
|
8721
|
+
if (!defaultName && namedList.length === 0) return null;
|
|
8722
|
+
if (!defaultName && allTypeOnly) return `import type { ${namedList.map((n) => formatNamed(n, true)).join(", ")} } from "${module}";`;
|
|
8723
|
+
const parts = [];
|
|
8724
|
+
if (defaultName) parts.push(defaultName);
|
|
8725
|
+
if (namedList.length > 0) {
|
|
8726
|
+
const items = namedList.map((n) => formatNamed(n, false)).join(", ");
|
|
8727
|
+
parts.push(`{ ${items} }`);
|
|
8728
|
+
}
|
|
8729
|
+
return `import ${parts.join(", ")} from "${module}";`;
|
|
8730
|
+
};
|
|
8731
|
+
const fixDuplicateImports = async (context) => {
|
|
8732
|
+
const files = getSourceFiles(context);
|
|
8733
|
+
for (const filePath of files) {
|
|
8734
|
+
if (!JS_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
8735
|
+
if (isAutoGenerated(filePath)) continue;
|
|
8736
|
+
let content;
|
|
8737
|
+
try {
|
|
8738
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
8739
|
+
} catch {
|
|
8740
|
+
continue;
|
|
8741
|
+
}
|
|
8742
|
+
const lines = content.split("\n");
|
|
8743
|
+
const imports = [];
|
|
8744
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8745
|
+
const stmt = parseImportLine(lines[i], i);
|
|
8746
|
+
if (stmt) imports.push(stmt);
|
|
8747
|
+
}
|
|
8748
|
+
if (imports.length < 2) continue;
|
|
8749
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8750
|
+
for (const stmt of imports) {
|
|
8751
|
+
const list = groups.get(stmt.module) ?? [];
|
|
8752
|
+
list.push(stmt);
|
|
8753
|
+
groups.set(stmt.module, list);
|
|
8754
|
+
}
|
|
8755
|
+
const linesToRemove = /* @__PURE__ */ new Set();
|
|
8756
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
8757
|
+
let modified = false;
|
|
8758
|
+
for (const group of groups.values()) {
|
|
8759
|
+
if (group.length < 2) continue;
|
|
8760
|
+
const merged = mergeImports(group);
|
|
8761
|
+
if (!merged) continue;
|
|
8762
|
+
replacements.set(group[0].lineIndex, merged);
|
|
8763
|
+
for (const stmt of group.slice(1)) linesToRemove.add(stmt.lineIndex);
|
|
8764
|
+
modified = true;
|
|
8765
|
+
}
|
|
8766
|
+
if (!modified) continue;
|
|
8767
|
+
const next = [...lines];
|
|
8768
|
+
for (const [idx, replacement] of replacements) next[idx] = replacement;
|
|
8769
|
+
const sortedRemove = Array.from(linesToRemove).sort((a, b) => b - a);
|
|
8770
|
+
for (const idx of sortedRemove) next.splice(idx, 1);
|
|
8771
|
+
fs.writeFileSync(filePath, next.join("\n"));
|
|
8772
|
+
}
|
|
8773
|
+
};
|
|
8774
|
+
|
|
8775
|
+
//#endregion
|
|
8776
|
+
//#region src/engines/ai-slop/narrative-comments-fix.ts
|
|
8777
|
+
const fixNarrativeComments = async (context) => {
|
|
8778
|
+
const diagnostics = await detectNarrativeComments(context);
|
|
8779
|
+
if (diagnostics.length === 0) return;
|
|
8780
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
8781
|
+
for (const d of diagnostics) {
|
|
8782
|
+
const abs = d.filePath.startsWith("/") ? d.filePath : `${context.rootDirectory}/${d.filePath}`;
|
|
8783
|
+
const list = byFile.get(abs) ?? [];
|
|
8784
|
+
list.push(d);
|
|
8785
|
+
byFile.set(abs, list);
|
|
8786
|
+
}
|
|
8787
|
+
for (const [filePath, diags] of byFile) {
|
|
8788
|
+
const syntax = getCommentSyntax(path.extname(filePath));
|
|
8789
|
+
if (!syntax) continue;
|
|
8790
|
+
let content;
|
|
8791
|
+
try {
|
|
8792
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
8793
|
+
} catch {
|
|
8794
|
+
continue;
|
|
8795
|
+
}
|
|
8796
|
+
const lines = content.split("\n");
|
|
8797
|
+
const blocks = collectBlocks(lines, syntax);
|
|
8798
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
8799
|
+
for (const d of diags) {
|
|
8800
|
+
const block = blocks.find((b) => b.startLine === d.line);
|
|
8801
|
+
if (!block) continue;
|
|
8802
|
+
for (let ln = block.startLine; ln <= block.endLine; ln += 1) toRemove.add(ln);
|
|
8803
|
+
const prev = block.startLine - 1;
|
|
8804
|
+
const next = block.endLine + 1;
|
|
8805
|
+
const prevIsBlank = prev >= 1 && lines[prev - 1]?.trim() === "";
|
|
8806
|
+
const nextIsBlank = next <= lines.length && lines[next - 1]?.trim() === "";
|
|
8807
|
+
if (prevIsBlank && nextIsBlank) toRemove.add(prev);
|
|
8808
|
+
}
|
|
8809
|
+
const kept = [];
|
|
8810
|
+
for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
|
|
8811
|
+
const newContent = kept.join("\n");
|
|
8812
|
+
if (newContent !== content) fs.writeFileSync(filePath, newContent);
|
|
8813
|
+
}
|
|
8814
|
+
};
|
|
8815
|
+
|
|
7374
8816
|
//#endregion
|
|
7375
8817
|
//#region src/engines/ai-slop/unused-imports-fix.ts
|
|
7376
8818
|
const fixUnusedImports = async (context) => {
|
|
@@ -7392,9 +8834,9 @@ const fixUnusedImports = async (context) => {
|
|
|
7392
8834
|
for (const [lineNo, syms] of symbolsByLine) {
|
|
7393
8835
|
const lineIdx = lineNo - 1;
|
|
7394
8836
|
const allUnused = syms.every((s) => unusedNames.has(s.name));
|
|
7395
|
-
const importSpan = JS_EXTENSIONS.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
|
|
8837
|
+
const importSpan = JS_EXTENSIONS$1.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
|
|
7396
8838
|
if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
|
|
7397
|
-
else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
8839
|
+
else if (JS_EXTENSIONS$1.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
7398
8840
|
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
|
|
7399
8841
|
}
|
|
7400
8842
|
if (linesToRemove.size === 0 && unused.length === 0) continue;
|
|
@@ -8127,6 +9569,7 @@ const hasJsOrTs = (projectInfo) => projectInfo.languages.includes("typescript")
|
|
|
8127
9569
|
const runAiSlopSteps = async (deps) => {
|
|
8128
9570
|
if (!deps.config.engines["ai-slop"]) return;
|
|
8129
9571
|
await deps.runStep("Unused imports", () => detectUnusedImports(deps.context), () => fixUnusedImports(deps.context));
|
|
9572
|
+
await deps.runStep("Duplicate imports", () => detectDuplicateImports(deps.context), () => fixDuplicateImports(deps.context));
|
|
8130
9573
|
const detectFixableSlop = async () => {
|
|
8131
9574
|
const [comments, dead, narrative] = await Promise.all([
|
|
8132
9575
|
detectTrivialComments(deps.context),
|
|
@@ -8232,7 +9675,8 @@ const createEngineContext = (rootDirectory, projectInfo, config) => ({
|
|
|
8232
9675
|
installedTools: projectInfo.installedTools,
|
|
8233
9676
|
config: {
|
|
8234
9677
|
quality: config.quality,
|
|
8235
|
-
security: config.security
|
|
9678
|
+
security: config.security,
|
|
9679
|
+
lint: config.lint
|
|
8236
9680
|
}
|
|
8237
9681
|
});
|
|
8238
9682
|
const fixCommand = async (directory, config, options = {
|
|
@@ -8295,6 +9739,7 @@ const fixCommand = async (directory, config, options = {
|
|
|
8295
9739
|
const engineConfig = {
|
|
8296
9740
|
quality: config.quality,
|
|
8297
9741
|
security: config.security,
|
|
9742
|
+
lint: config.lint,
|
|
8298
9743
|
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
8299
9744
|
};
|
|
8300
9745
|
rail.start("Verifying results");
|
|
@@ -8599,8 +10044,10 @@ const buildRulesRender = (input) => {
|
|
|
8599
10044
|
const AI_SLOP_FIXABLE = new Set([
|
|
8600
10045
|
"ai-slop/trivial-comment",
|
|
8601
10046
|
"ai-slop/unused-import",
|
|
8602
|
-
"ai-slop/narrative-comment"
|
|
10047
|
+
"ai-slop/narrative-comment",
|
|
10048
|
+
"ai-slop/duplicate-import"
|
|
8603
10049
|
]);
|
|
10050
|
+
const AI_SLOP_ERRORS = new Set(["ai-slop/hallucinated-import"]);
|
|
8604
10051
|
const BUILTIN_RULES = [
|
|
8605
10052
|
{
|
|
8606
10053
|
engine: "format",
|
|
@@ -8621,7 +10068,8 @@ const BUILTIN_RULES = [
|
|
|
8621
10068
|
"ruff/*",
|
|
8622
10069
|
"go/*",
|
|
8623
10070
|
"clippy/*",
|
|
8624
|
-
"rubocop/*"
|
|
10071
|
+
"rubocop/*",
|
|
10072
|
+
"typescript/*"
|
|
8625
10073
|
]
|
|
8626
10074
|
},
|
|
8627
10075
|
{
|
|
@@ -8657,7 +10105,16 @@ const BUILTIN_RULES = [
|
|
|
8657
10105
|
"ai-slop/unsafe-type-assertion",
|
|
8658
10106
|
"ai-slop/double-type-assertion",
|
|
8659
10107
|
"ai-slop/ts-directive",
|
|
8660
|
-
"ai-slop/narrative-comment"
|
|
10108
|
+
"ai-slop/narrative-comment",
|
|
10109
|
+
"ai-slop/duplicate-import",
|
|
10110
|
+
"ai-slop/python-bare-except",
|
|
10111
|
+
"ai-slop/python-broad-except",
|
|
10112
|
+
"ai-slop/python-mutable-default",
|
|
10113
|
+
"ai-slop/python-print-debug",
|
|
10114
|
+
"ai-slop/go-library-panic",
|
|
10115
|
+
"ai-slop/rust-non-test-unwrap",
|
|
10116
|
+
"ai-slop/rust-todo-stub",
|
|
10117
|
+
"ai-slop/hallucinated-import"
|
|
8661
10118
|
]
|
|
8662
10119
|
},
|
|
8663
10120
|
{
|
|
@@ -8688,7 +10145,7 @@ const toRuleEntry = (engine, ruleId) => {
|
|
|
8688
10145
|
if (engine === "ai-slop") return {
|
|
8689
10146
|
id: ruleId,
|
|
8690
10147
|
engine,
|
|
8691
|
-
severity: "warning",
|
|
10148
|
+
severity: AI_SLOP_ERRORS.has(ruleId) ? "error" : "warning",
|
|
8692
10149
|
fixable: AI_SLOP_FIXABLE.has(ruleId)
|
|
8693
10150
|
};
|
|
8694
10151
|
return {
|
|
@@ -8980,6 +10437,20 @@ program.command("ci [directory]").description("CI-friendly JSON output with exit
|
|
|
8980
10437
|
program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
|
|
8981
10438
|
await rulesCommand(directory);
|
|
8982
10439
|
});
|
|
10440
|
+
program.command("badge [directory]").description("Print the public score badge URL + README markdown for this repo").option("--owner <owner>", "GitHub owner (auto-detected from git remote if omitted)").option("--repo <repo>", "GitHub repo name (auto-detected from git remote if omitted)").option("--json", "emit machine-readable JSON instead of the rendered output").action(async (directory = ".", _flags, command) => {
|
|
10441
|
+
const flags = command.optsWithGlobals();
|
|
10442
|
+
try {
|
|
10443
|
+
await badgeCommand({
|
|
10444
|
+
directory,
|
|
10445
|
+
owner: flags.owner,
|
|
10446
|
+
repo: flags.repo,
|
|
10447
|
+
json: Boolean(flags.json)
|
|
10448
|
+
});
|
|
10449
|
+
} catch (err) {
|
|
10450
|
+
process.stderr.write(`${err?.message ?? "Failed to print badge"}\n`);
|
|
10451
|
+
process.exit(1);
|
|
10452
|
+
}
|
|
10453
|
+
});
|
|
8983
10454
|
registerHookCommand(program);
|
|
8984
10455
|
const main = async () => {
|
|
8985
10456
|
await program.parseAsync();
|
|
@@ -8988,4 +10459,4 @@ const main = async () => {
|
|
|
8988
10459
|
main();
|
|
8989
10460
|
|
|
8990
10461
|
//#endregion
|
|
8991
|
-
export { ENGINE_INFO as n, APP_VERSION as t };
|
|
10462
|
+
export { ENGINE_INFO as n, runSubprocess as r, APP_VERSION as t };
|