aislop 0.9.4 → 0.9.5
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 +50 -3
- package/dist/cli.js +1034 -62
- package/dist/{version-C45P3Q1N.js → engine-info-DCvIfZ0f.js} +1 -5
- package/dist/index.d.ts +6 -0
- package/dist/index.js +758 -51
- package/dist/{json-CXiEvR_M.js → json-CZU3lEfE.js} +2 -1
- package/dist/mcp.js +653 -39
- package/dist/sarif-CZVuavf_.js +61 -0
- package/dist/sarif-Cneulb6L.js +60 -0
- package/dist/version-ls3wZmOU.js +5 -0
- package/package.json +3 -2
- package/scripts/gen-config-schema.mjs +35 -0
- /package/dist/{expo-doctor-Bz0LZhQ6.js → expo-doctor-BcIkOte5.js} +0 -0
- /package/dist/{generic-BrcWMW7E.js → generic-D_T4cUaC.js} +0 -0
- /package/dist/{typecheck-XJMuCczG.js → typecheck-DQSzG8fX.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -10,8 +10,8 @@ import YAML from "yaml";
|
|
|
10
10
|
import { z } from "zod/v4";
|
|
11
11
|
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
12
12
|
import micromatch from "micromatch";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
13
|
import ts from "typescript";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
16
16
|
import pc from "picocolors";
|
|
17
17
|
import wcwidth from "wcwidth";
|
|
@@ -34,7 +34,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/version.ts
|
|
37
|
-
const APP_VERSION = "0.9.
|
|
37
|
+
const APP_VERSION = "0.9.5";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/telemetry/env.ts
|
|
@@ -180,8 +180,8 @@ const redactProperties = (props) => {
|
|
|
180
180
|
|
|
181
181
|
//#endregion
|
|
182
182
|
//#region src/telemetry/client.ts
|
|
183
|
-
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
184
|
-
const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
183
|
+
const POSTHOG_HOST = process.env.AISLOP_POSTHOG_HOST ?? "https://eu.i.posthog.com";
|
|
184
|
+
const POSTHOG_KEY = process.env.AISLOP_POSTHOG_KEY ?? "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
185
185
|
const SCHEMA_VERSION = "v2";
|
|
186
186
|
const REQUEST_TIMEOUT_MS = 3e3;
|
|
187
187
|
const isTelemetryDisabled = (config) => {
|
|
@@ -602,7 +602,8 @@ const DEFAULT_CONFIG = {
|
|
|
602
602
|
failBelow: 70,
|
|
603
603
|
format: "json"
|
|
604
604
|
},
|
|
605
|
-
telemetry: { enabled: true }
|
|
605
|
+
telemetry: { enabled: true },
|
|
606
|
+
rules: {}
|
|
606
607
|
};
|
|
607
608
|
const GITHUB_WORKFLOW_DIR = ".github/workflows";
|
|
608
609
|
const GITHUB_WORKFLOW_FILE = "aislop.yml";
|
|
@@ -729,6 +730,12 @@ const CiSchema = z.object({
|
|
|
729
730
|
format: z.enum(["json"]).default("json")
|
|
730
731
|
});
|
|
731
732
|
const TelemetrySchema = z.object({ enabled: z.boolean().default(true) });
|
|
733
|
+
const RuleSeverityOverride = z.enum([
|
|
734
|
+
"error",
|
|
735
|
+
"warning",
|
|
736
|
+
"off"
|
|
737
|
+
]);
|
|
738
|
+
const RulesSchema = z.record(z.string(), RuleSeverityOverride).default(() => ({}));
|
|
732
739
|
const AislopConfigSchema = z.object({
|
|
733
740
|
version: z.number().default(1),
|
|
734
741
|
engines: EnginesSchema.default(() => ({
|
|
@@ -763,6 +770,7 @@ const AislopConfigSchema = z.object({
|
|
|
763
770
|
format: "json"
|
|
764
771
|
})),
|
|
765
772
|
telemetry: TelemetrySchema.default(() => ({ enabled: true })),
|
|
773
|
+
rules: RulesSchema,
|
|
766
774
|
exclude: z.array(z.string()).default(() => [
|
|
767
775
|
"node_modules",
|
|
768
776
|
".git",
|
|
@@ -831,7 +839,7 @@ const loadConfig = (directory) => {
|
|
|
831
839
|
//#endregion
|
|
832
840
|
//#region src/utils/source-files.ts
|
|
833
841
|
const MAX_BUFFER$1 = 50 * 1024 * 1024;
|
|
834
|
-
const SOURCE_EXTENSIONS = new Set([
|
|
842
|
+
const SOURCE_EXTENSIONS$1 = new Set([
|
|
835
843
|
".ts",
|
|
836
844
|
".tsx",
|
|
837
845
|
".js",
|
|
@@ -939,7 +947,7 @@ const toProjectPath = (rootDirectory, filePath) => {
|
|
|
939
947
|
const isWithinProject = (relativePath) => relativePath.length > 0 && !relativePath.startsWith("..");
|
|
940
948
|
const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
941
949
|
const extension = path.extname(filePath);
|
|
942
|
-
return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
|
|
950
|
+
return SOURCE_EXTENSIONS$1.has(extension) || extraExtensions.has(extension);
|
|
943
951
|
};
|
|
944
952
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
945
953
|
const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
|
|
@@ -1078,7 +1086,7 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
|
1078
1086
|
|
|
1079
1087
|
//#endregion
|
|
1080
1088
|
//#region src/engines/ai-slop/abstractions.ts
|
|
1081
|
-
const JS_EXTS$
|
|
1089
|
+
const JS_EXTS$2 = new Set([
|
|
1082
1090
|
".ts",
|
|
1083
1091
|
".tsx",
|
|
1084
1092
|
".js",
|
|
@@ -1089,11 +1097,11 @@ const JS_EXTS$1 = new Set([
|
|
|
1089
1097
|
const THIN_WRAPPER_PATTERNS = [
|
|
1090
1098
|
{
|
|
1091
1099
|
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
1092
|
-
extensions: JS_EXTS$
|
|
1100
|
+
extensions: JS_EXTS$2
|
|
1093
1101
|
},
|
|
1094
1102
|
{
|
|
1095
1103
|
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
1096
|
-
extensions: JS_EXTS$
|
|
1104
|
+
extensions: JS_EXTS$2
|
|
1097
1105
|
},
|
|
1098
1106
|
{
|
|
1099
1107
|
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
@@ -1326,6 +1334,12 @@ const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
|
1326
1334
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
1327
1335
|
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
1328
1336
|
const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
|
|
1337
|
+
const isLineComment = (trimmed) => isJsComment(trimmed) || isPythonComment(trimmed);
|
|
1338
|
+
const isInMultiLineCommentRun = (lines, index) => {
|
|
1339
|
+
const prev = index > 0 ? lines[index - 1].trim() : "";
|
|
1340
|
+
const next = index + 1 < lines.length ? lines[index + 1].trim() : "";
|
|
1341
|
+
return isLineComment(prev) || isLineComment(next);
|
|
1342
|
+
};
|
|
1329
1343
|
/**
|
|
1330
1344
|
* Extract just the comment text after the comment marker.
|
|
1331
1345
|
*/
|
|
@@ -1378,6 +1392,7 @@ const scanFileForTrivialComments = (content, relativePath, ext) => {
|
|
|
1378
1392
|
const lines = content.split("\n");
|
|
1379
1393
|
for (let i = 0; i < lines.length; i++) {
|
|
1380
1394
|
if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
|
|
1395
|
+
if (isInMultiLineCommentRun(lines, i)) continue;
|
|
1381
1396
|
if (isDocCommentForDeclaration(lines, i, ext)) continue;
|
|
1382
1397
|
diagnostics.push({
|
|
1383
1398
|
filePath: relativePath,
|
|
@@ -1539,6 +1554,177 @@ const detectDeadPatterns = async (context) => {
|
|
|
1539
1554
|
return diagnostics;
|
|
1540
1555
|
};
|
|
1541
1556
|
|
|
1557
|
+
//#endregion
|
|
1558
|
+
//#region src/engines/ai-slop/defensive-patterns.ts
|
|
1559
|
+
const JS_TS_EXTENSIONS = new Set([
|
|
1560
|
+
".ts",
|
|
1561
|
+
".tsx",
|
|
1562
|
+
".js",
|
|
1563
|
+
".jsx",
|
|
1564
|
+
".mjs",
|
|
1565
|
+
".cjs"
|
|
1566
|
+
]);
|
|
1567
|
+
const TS_EXTENSIONS = new Set([".ts", ".tsx"]);
|
|
1568
|
+
const COERCION_CTORS = {
|
|
1569
|
+
string: "String",
|
|
1570
|
+
number: "Number",
|
|
1571
|
+
boolean: "Boolean"
|
|
1572
|
+
};
|
|
1573
|
+
const scriptKindFor = (ext) => {
|
|
1574
|
+
switch (ext) {
|
|
1575
|
+
case ".tsx": return ts.ScriptKind.TSX;
|
|
1576
|
+
case ".jsx": return ts.ScriptKind.JSX;
|
|
1577
|
+
case ".js":
|
|
1578
|
+
case ".mjs":
|
|
1579
|
+
case ".cjs": return ts.ScriptKind.JS;
|
|
1580
|
+
default: return ts.ScriptKind.TS;
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
const lineFor = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
1584
|
+
const makeDiagnostic = (filePath, line, rule, message, help) => ({
|
|
1585
|
+
filePath,
|
|
1586
|
+
engine: "ai-slop",
|
|
1587
|
+
rule,
|
|
1588
|
+
severity: "warning",
|
|
1589
|
+
message,
|
|
1590
|
+
help,
|
|
1591
|
+
line,
|
|
1592
|
+
column: 0,
|
|
1593
|
+
category: "AI Slop",
|
|
1594
|
+
fixable: false
|
|
1595
|
+
});
|
|
1596
|
+
const isFunctionNode = (node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
|
|
1597
|
+
const primitiveKindOf = (node) => {
|
|
1598
|
+
if (!node) return null;
|
|
1599
|
+
switch (node.kind) {
|
|
1600
|
+
case ts.SyntaxKind.StringKeyword: return "string";
|
|
1601
|
+
case ts.SyntaxKind.NumberKeyword: return "number";
|
|
1602
|
+
case ts.SyntaxKind.BooleanKeyword: return "boolean";
|
|
1603
|
+
default: return null;
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
const hasExportModifier = (node) => node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
1607
|
+
const primitiveParamsOf = (node) => {
|
|
1608
|
+
const params = /* @__PURE__ */ new Map();
|
|
1609
|
+
for (const param of node.parameters) {
|
|
1610
|
+
if (!ts.isIdentifier(param.name)) continue;
|
|
1611
|
+
const kind = primitiveKindOf(param.type);
|
|
1612
|
+
if (!kind) continue;
|
|
1613
|
+
params.set(param.name.text, kind);
|
|
1614
|
+
}
|
|
1615
|
+
return params;
|
|
1616
|
+
};
|
|
1617
|
+
const isRethrowStatement = (statement, errorName) => ts.isThrowStatement(statement) && statement.expression !== void 0 && ts.isIdentifier(statement.expression) && statement.expression.text === errorName;
|
|
1618
|
+
const isPromiseRejectRethrow = (statement, errorName) => {
|
|
1619
|
+
if (!ts.isReturnStatement(statement) || !statement.expression) return false;
|
|
1620
|
+
const expression = statement.expression;
|
|
1621
|
+
if (!ts.isCallExpression(expression) || expression.arguments.length !== 1) return false;
|
|
1622
|
+
const [arg] = expression.arguments;
|
|
1623
|
+
if (!ts.isIdentifier(arg) || arg.text !== errorName) return false;
|
|
1624
|
+
if (!ts.isPropertyAccessExpression(expression.expression)) return false;
|
|
1625
|
+
const target = expression.expression;
|
|
1626
|
+
return ts.isIdentifier(target.expression) && target.expression.text === "Promise" && target.name.text === "reject";
|
|
1627
|
+
};
|
|
1628
|
+
const detectRedundantTryCatch = (sourceFile, relativePath) => {
|
|
1629
|
+
const diagnostics = [];
|
|
1630
|
+
const visit = (node) => {
|
|
1631
|
+
if (ts.isTryStatement(node) && node.catchClause && !node.finallyBlock) {
|
|
1632
|
+
const catchNameNode = node.catchClause.variableDeclaration?.name;
|
|
1633
|
+
const [onlyStatement] = node.catchClause.block.statements;
|
|
1634
|
+
if (catchNameNode && ts.isIdentifier(catchNameNode) && node.catchClause.block.statements.length === 1 && onlyStatement && (isRethrowStatement(onlyStatement, catchNameNode.text) || isPromiseRejectRethrow(onlyStatement, catchNameNode.text))) diagnostics.push(makeDiagnostic(relativePath, lineFor(sourceFile, node.catchClause), "ai-slop/redundant-try-catch", "Catch block only rethrows the same error", "Remove the try/catch or add useful context, cleanup, or recovery. Rethrowing unchanged errors is usually defensive agent noise."));
|
|
1635
|
+
}
|
|
1636
|
+
ts.forEachChild(node, visit);
|
|
1637
|
+
};
|
|
1638
|
+
visit(sourceFile);
|
|
1639
|
+
return diagnostics;
|
|
1640
|
+
};
|
|
1641
|
+
const detectPrimitiveCoercions = (sourceFile, relativePath) => {
|
|
1642
|
+
const diagnostics = [];
|
|
1643
|
+
const scanFunctionBody = (node, params) => {
|
|
1644
|
+
const body = node.body;
|
|
1645
|
+
if (!body || params.size === 0) return;
|
|
1646
|
+
const visitBody = (child) => {
|
|
1647
|
+
if (child !== body && isFunctionNode(child)) return;
|
|
1648
|
+
if (ts.isCallExpression(child) && ts.isIdentifier(child.expression)) {
|
|
1649
|
+
const [arg] = child.arguments;
|
|
1650
|
+
if (arg && ts.isIdentifier(arg)) {
|
|
1651
|
+
const primitive = params.get(arg.text);
|
|
1652
|
+
if (primitive && child.expression.text === COERCION_CTORS[primitive]) diagnostics.push(makeDiagnostic(relativePath, lineFor(sourceFile, child), "ai-slop/redundant-type-coercion", `Parameter '${arg.text}' is already typed as ${primitive} but is coerced again`, "Trust the typed boundary or validate unknown input before this function. Re-coercing already typed parameters is usually defensive agent noise."));
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
ts.forEachChild(child, visitBody);
|
|
1656
|
+
};
|
|
1657
|
+
visitBody(body);
|
|
1658
|
+
};
|
|
1659
|
+
const visit = (node) => {
|
|
1660
|
+
if (isFunctionNode(node)) scanFunctionBody(node, primitiveParamsOf(node));
|
|
1661
|
+
ts.forEachChild(node, visit);
|
|
1662
|
+
};
|
|
1663
|
+
visit(sourceFile);
|
|
1664
|
+
return diagnostics;
|
|
1665
|
+
};
|
|
1666
|
+
const normalizedTypeDeclaration = (sourceFile, node) => sourceFile.text.slice(node.getStart(sourceFile), node.getEnd()).replace(/\bexport\b/g, "").replace(/\bdeclare\b/g, "").replace(/\s+/g, " ").trim();
|
|
1667
|
+
const exportedTypesOf = (parsed) => {
|
|
1668
|
+
const declarations = [];
|
|
1669
|
+
const visit = (node) => {
|
|
1670
|
+
if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && hasExportModifier(node)) declarations.push({
|
|
1671
|
+
name: node.name.text,
|
|
1672
|
+
signature: normalizedTypeDeclaration(parsed.sourceFile, node),
|
|
1673
|
+
filePath: parsed.relativePath,
|
|
1674
|
+
line: lineFor(parsed.sourceFile, node)
|
|
1675
|
+
});
|
|
1676
|
+
ts.forEachChild(node, visit);
|
|
1677
|
+
};
|
|
1678
|
+
visit(parsed.sourceFile);
|
|
1679
|
+
return declarations;
|
|
1680
|
+
};
|
|
1681
|
+
const duplicateTypeKeyOf = (declaration) => `${declaration.name}\0${declaration.signature}`;
|
|
1682
|
+
const detectDuplicateExportedTypes = (parsedSources) => {
|
|
1683
|
+
const diagnostics = [];
|
|
1684
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1685
|
+
for (const parsed of parsedSources) {
|
|
1686
|
+
if (!TS_EXTENSIONS.has(parsed.ext)) continue;
|
|
1687
|
+
for (const declaration of exportedTypesOf(parsed)) {
|
|
1688
|
+
const key = duplicateTypeKeyOf(declaration);
|
|
1689
|
+
const previous = seen.get(key);
|
|
1690
|
+
if (!previous) {
|
|
1691
|
+
seen.set(key, declaration);
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
if (previous.filePath === declaration.filePath) continue;
|
|
1695
|
+
diagnostics.push(makeDiagnostic(declaration.filePath, declaration.line, "ai-slop/duplicate-type-declaration", `Exported type '${declaration.name}' duplicates an existing declaration`, `Reuse or import the existing type from ${previous.filePath} instead of re-declaring the same shape in another file.`));
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
return diagnostics;
|
|
1699
|
+
};
|
|
1700
|
+
const detectDefensivePatterns = async (context) => {
|
|
1701
|
+
const diagnostics = [];
|
|
1702
|
+
const parsedSources = [];
|
|
1703
|
+
for (const filePath of getSourceFiles(context)) {
|
|
1704
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1705
|
+
let content;
|
|
1706
|
+
try {
|
|
1707
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1708
|
+
} catch {
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1712
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
1713
|
+
const ext = path.extname(filePath);
|
|
1714
|
+
if (!JS_TS_EXTENSIONS.has(ext)) continue;
|
|
1715
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindFor(ext));
|
|
1716
|
+
parsedSources.push({
|
|
1717
|
+
sourceFile,
|
|
1718
|
+
relativePath,
|
|
1719
|
+
ext
|
|
1720
|
+
});
|
|
1721
|
+
diagnostics.push(...detectRedundantTryCatch(sourceFile, relativePath));
|
|
1722
|
+
if (TS_EXTENSIONS.has(ext)) diagnostics.push(...detectPrimitiveCoercions(sourceFile, relativePath));
|
|
1723
|
+
}
|
|
1724
|
+
diagnostics.push(...detectDuplicateExportedTypes(parsedSources));
|
|
1725
|
+
return diagnostics;
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1542
1728
|
//#endregion
|
|
1543
1729
|
//#region src/engines/ai-slop/duplicate-imports.ts
|
|
1544
1730
|
const JS_EXTENSIONS$3 = new Set([
|
|
@@ -1549,7 +1735,16 @@ const JS_EXTENSIONS$3 = new Set([
|
|
|
1549
1735
|
".mjs",
|
|
1550
1736
|
".cjs"
|
|
1551
1737
|
]);
|
|
1552
|
-
const IMPORT_FROM_RE$1 = /^\s*import\s+[^;]*?from\s+["']([^"']+)["']/;
|
|
1738
|
+
const IMPORT_FROM_RE$1 = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
|
|
1739
|
+
const TYPE_ONLY_RE = /^\s*type\b/;
|
|
1740
|
+
const VALUE_BINDING_RE = /\{([^}]*)\}/;
|
|
1741
|
+
const isTypeOnly = (clause) => {
|
|
1742
|
+
if (TYPE_ONLY_RE.test(clause)) return true;
|
|
1743
|
+
const braces = VALUE_BINDING_RE.exec(clause);
|
|
1744
|
+
if (!braces) return false;
|
|
1745
|
+
const members = braces[1].split(",").map((member) => member.trim()).filter((member) => member.length > 0);
|
|
1746
|
+
return members.length > 0 && members.every((member) => /^type\b/.test(member));
|
|
1747
|
+
};
|
|
1553
1748
|
const extractImportLines = (content) => {
|
|
1554
1749
|
const lines = content.split("\n");
|
|
1555
1750
|
const results = [];
|
|
@@ -1558,8 +1753,9 @@ const extractImportLines = (content) => {
|
|
|
1558
1753
|
const match = IMPORT_FROM_RE$1.exec(line);
|
|
1559
1754
|
if (!match) continue;
|
|
1560
1755
|
results.push({
|
|
1561
|
-
spec: match[
|
|
1562
|
-
line: i + 1
|
|
1756
|
+
spec: match[2],
|
|
1757
|
+
line: i + 1,
|
|
1758
|
+
typeOnly: isTypeOnly(match[1])
|
|
1563
1759
|
});
|
|
1564
1760
|
}
|
|
1565
1761
|
return results;
|
|
@@ -1578,14 +1774,16 @@ const detectDuplicateImports = async (context) => {
|
|
|
1578
1774
|
}
|
|
1579
1775
|
const imports = extractImportLines(content);
|
|
1580
1776
|
if (imports.length < 2) continue;
|
|
1581
|
-
const
|
|
1777
|
+
const byBucket = /* @__PURE__ */ new Map();
|
|
1582
1778
|
for (const imp of imports) {
|
|
1583
|
-
const
|
|
1779
|
+
const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
1780
|
+
const list = byBucket.get(key) ?? [];
|
|
1584
1781
|
list.push(imp);
|
|
1585
|
-
|
|
1782
|
+
byBucket.set(key, list);
|
|
1586
1783
|
}
|
|
1587
1784
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
1588
|
-
for (const
|
|
1785
|
+
for (const occurrences of byBucket.values()) {
|
|
1786
|
+
const { spec } = occurrences[0];
|
|
1589
1787
|
if (occurrences.length < 2) continue;
|
|
1590
1788
|
for (const dup of occurrences.slice(1)) {
|
|
1591
1789
|
const firstLine = occurrences[0].line;
|
|
@@ -1790,6 +1988,130 @@ const detectGoPatterns = async (context) => {
|
|
|
1790
1988
|
return diagnostics;
|
|
1791
1989
|
};
|
|
1792
1990
|
|
|
1991
|
+
//#endregion
|
|
1992
|
+
//#region src/engines/ai-slop/hardcoded-config.ts
|
|
1993
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
1994
|
+
".ts",
|
|
1995
|
+
".tsx",
|
|
1996
|
+
".js",
|
|
1997
|
+
".jsx",
|
|
1998
|
+
".mjs",
|
|
1999
|
+
".cjs",
|
|
2000
|
+
".py",
|
|
2001
|
+
".go",
|
|
2002
|
+
".rs",
|
|
2003
|
+
".rb",
|
|
2004
|
+
".java",
|
|
2005
|
+
".php"
|
|
2006
|
+
]);
|
|
2007
|
+
const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
|
|
2008
|
+
const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
|
|
2009
|
+
const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
|
|
2010
|
+
const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
|
|
2011
|
+
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
2012
|
+
const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
|
|
2013
|
+
const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
|
|
2014
|
+
const PLACEHOLDER_HOSTS = new Set([
|
|
2015
|
+
"example.com",
|
|
2016
|
+
"example.org",
|
|
2017
|
+
"example.net"
|
|
2018
|
+
]);
|
|
2019
|
+
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2020
|
+
const HARDCODED_URL_FINDING = {
|
|
2021
|
+
rule: "ai-slop/hardcoded-url",
|
|
2022
|
+
message: "Hardcoded environment URL in production code",
|
|
2023
|
+
help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
|
|
2024
|
+
};
|
|
2025
|
+
const HARDCODED_ID_FINDING = {
|
|
2026
|
+
rule: "ai-slop/hardcoded-id",
|
|
2027
|
+
message: "Hardcoded provider/project ID in production code",
|
|
2028
|
+
help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
|
|
2029
|
+
};
|
|
2030
|
+
const makeFinding = (filePath, line, spec) => ({
|
|
2031
|
+
filePath,
|
|
2032
|
+
engine: "ai-slop",
|
|
2033
|
+
rule: spec.rule,
|
|
2034
|
+
severity: "warning",
|
|
2035
|
+
message: spec.message,
|
|
2036
|
+
help: spec.help,
|
|
2037
|
+
line,
|
|
2038
|
+
column: 0,
|
|
2039
|
+
category: "AI Slop",
|
|
2040
|
+
fixable: false
|
|
2041
|
+
});
|
|
2042
|
+
const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
2043
|
+
const commentStartsBefore = (line, index, ext) => {
|
|
2044
|
+
const prefix = line.slice(0, index);
|
|
2045
|
+
if (ext === ".py" || ext === ".rb") return prefix.includes("#");
|
|
2046
|
+
if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
|
|
2047
|
+
return prefix.includes("//") || prefix.includes("/*");
|
|
2048
|
+
};
|
|
2049
|
+
const safeUrlHost = (urlText) => {
|
|
2050
|
+
try {
|
|
2051
|
+
return new URL(urlText).hostname.toLowerCase();
|
|
2052
|
+
} catch {
|
|
2053
|
+
return null;
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
2057
|
+
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
2058
|
+
if (isEnvBackedLine(line)) return false;
|
|
2059
|
+
const host = safeUrlHost(urlText);
|
|
2060
|
+
if (!host) return false;
|
|
2061
|
+
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2062
|
+
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2063
|
+
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2064
|
+
};
|
|
2065
|
+
const hasUsefulIdShape = (value) => {
|
|
2066
|
+
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
2067
|
+
if (/^https?:\/\//i.test(value)) return false;
|
|
2068
|
+
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
2069
|
+
return /[0-9_-]/.test(value);
|
|
2070
|
+
};
|
|
2071
|
+
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
2072
|
+
const diagnostics = [];
|
|
2073
|
+
if (isCommentOnlyLine(line.trim())) return diagnostics;
|
|
2074
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
2075
|
+
let urlMatch;
|
|
2076
|
+
while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
|
|
2077
|
+
const urlText = urlMatch[2];
|
|
2078
|
+
if (commentStartsBefore(line, urlMatch.index, ext)) continue;
|
|
2079
|
+
if (!shouldFlagUrlLiteral(line, urlText)) continue;
|
|
2080
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
|
|
2081
|
+
}
|
|
2082
|
+
if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
|
|
2083
|
+
ID_LITERAL_RE.lastIndex = 0;
|
|
2084
|
+
let idMatch;
|
|
2085
|
+
while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
|
|
2086
|
+
const value = idMatch[2];
|
|
2087
|
+
if (commentStartsBefore(line, idMatch.index, ext)) continue;
|
|
2088
|
+
if (!hasUsefulIdShape(value)) continue;
|
|
2089
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
|
|
2090
|
+
}
|
|
2091
|
+
return diagnostics;
|
|
2092
|
+
};
|
|
2093
|
+
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
2094
|
+
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
2095
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
2096
|
+
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
2097
|
+
};
|
|
2098
|
+
const detectHardcodedConfigLiterals = async (context) => {
|
|
2099
|
+
const diagnostics = [];
|
|
2100
|
+
for (const filePath of getSourceFiles(context)) {
|
|
2101
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2102
|
+
let content;
|
|
2103
|
+
try {
|
|
2104
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2105
|
+
} catch {
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2109
|
+
const ext = path.extname(filePath);
|
|
2110
|
+
diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
|
|
2111
|
+
}
|
|
2112
|
+
return diagnostics;
|
|
2113
|
+
};
|
|
2114
|
+
|
|
1793
2115
|
//#endregion
|
|
1794
2116
|
//#region src/engines/ai-slop/js-import-aliases.ts
|
|
1795
2117
|
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
@@ -2057,23 +2379,87 @@ const PYTHON_STDLIB = new Set([
|
|
|
2057
2379
|
"zoneinfo"
|
|
2058
2380
|
]);
|
|
2059
2381
|
const PYTHON_IMPORT_TO_PIP = {
|
|
2060
|
-
yaml: "pyyaml",
|
|
2061
|
-
PIL: "pillow",
|
|
2062
|
-
dateutil: "python-dateutil",
|
|
2063
|
-
cv2:
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2382
|
+
yaml: ["pyyaml"],
|
|
2383
|
+
PIL: ["pillow"],
|
|
2384
|
+
dateutil: ["python-dateutil"],
|
|
2385
|
+
cv2: [
|
|
2386
|
+
"opencv-python",
|
|
2387
|
+
"opencv-python-headless",
|
|
2388
|
+
"opencv-contrib-python"
|
|
2389
|
+
],
|
|
2390
|
+
sklearn: ["scikit-learn"],
|
|
2391
|
+
bs4: ["beautifulsoup4"],
|
|
2392
|
+
typing_extensions: ["typing-extensions"],
|
|
2393
|
+
dotenv: ["python-dotenv"],
|
|
2394
|
+
genai: ["google-genai"],
|
|
2395
|
+
google: [
|
|
2396
|
+
"google-genai",
|
|
2397
|
+
"google-generativeai",
|
|
2398
|
+
"google-api-python-client",
|
|
2399
|
+
"google-cloud-storage",
|
|
2400
|
+
"google-cloud-aiplatform",
|
|
2401
|
+
"google-auth",
|
|
2402
|
+
"protobuf"
|
|
2403
|
+
],
|
|
2404
|
+
jose: ["python-jose"],
|
|
2405
|
+
jwt: ["pyjwt"],
|
|
2406
|
+
OpenSSL: ["pyopenssl"],
|
|
2407
|
+
Crypto: ["pycryptodome", "pycryptodomex"],
|
|
2408
|
+
Cryptodome: ["pycryptodomex", "pycryptodome"],
|
|
2409
|
+
magic: ["python-magic"],
|
|
2410
|
+
docx: ["python-docx"],
|
|
2411
|
+
pptx: ["python-pptx"],
|
|
2412
|
+
git: ["gitpython"],
|
|
2413
|
+
socks: ["pysocks"],
|
|
2414
|
+
redis: ["redis"],
|
|
2415
|
+
cairo: ["pycairo"],
|
|
2416
|
+
serial: ["pyserial"],
|
|
2417
|
+
usb: ["pyusb"],
|
|
2418
|
+
gi: ["pygobject"],
|
|
2419
|
+
Xlib: ["python-xlib"],
|
|
2420
|
+
ldap: ["python-ldap"],
|
|
2421
|
+
slugify: ["python-slugify"],
|
|
2422
|
+
memcache: ["python-memcached"],
|
|
2423
|
+
dns: ["dnspython"],
|
|
2424
|
+
attr: ["attrs"],
|
|
2425
|
+
attrs: ["attrs"],
|
|
2426
|
+
zoneinfo_data: ["tzdata"],
|
|
2427
|
+
pkg_resources: ["setuptools"],
|
|
2428
|
+
setuptools: ["setuptools"],
|
|
2429
|
+
wx: ["wxpython"],
|
|
2430
|
+
skimage: ["scikit-image"],
|
|
2431
|
+
OpenGL: ["pyopengl"],
|
|
2432
|
+
win32api: ["pywin32"],
|
|
2433
|
+
win32con: ["pywin32"],
|
|
2434
|
+
win32com: ["pywin32"],
|
|
2435
|
+
pythoncom: ["pywin32"],
|
|
2436
|
+
pywintypes: ["pywin32"],
|
|
2437
|
+
rest_framework: ["djangorestframework"],
|
|
2438
|
+
allauth: ["django-allauth"],
|
|
2439
|
+
corsheaders: ["django-cors-headers"],
|
|
2440
|
+
debug_toolbar: ["django-debug-toolbar"],
|
|
2441
|
+
environ: ["django-environ"],
|
|
2442
|
+
flask_cors: ["flask-cors"],
|
|
2443
|
+
flask_sqlalchemy: ["flask-sqlalchemy"],
|
|
2444
|
+
flask_migrate: ["flask-migrate"],
|
|
2445
|
+
flask_login: ["flask-login"],
|
|
2446
|
+
jwt_extended: ["flask-jwt-extended"],
|
|
2447
|
+
dateparser: ["dateparser"],
|
|
2448
|
+
yaml_include: ["pyyaml-include"],
|
|
2449
|
+
lxml_html_clean: ["lxml-html-clean"],
|
|
2450
|
+
grpc: ["grpcio"],
|
|
2451
|
+
grpc_status: ["grpcio-status"],
|
|
2452
|
+
google_crc32c: ["google-crc32c"],
|
|
2453
|
+
pkg_about: ["pkg-about"],
|
|
2454
|
+
mpl_toolkits: ["matplotlib"],
|
|
2455
|
+
dotmap: ["dotmap"],
|
|
2456
|
+
pydantic_settings: ["pydantic-settings"],
|
|
2457
|
+
telegram: ["python-telegram-bot"],
|
|
2458
|
+
discord: ["discord-py"],
|
|
2459
|
+
nacl: ["pynacl"],
|
|
2460
|
+
jwcrypto: ["jwcrypto"],
|
|
2461
|
+
humanfriendly: ["humanfriendly"],
|
|
2462
|
+
multipart: ["python-multipart"]
|
|
2077
2463
|
};
|
|
2078
2464
|
|
|
2079
2465
|
//#endregion
|
|
@@ -2112,6 +2498,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
2112
2498
|
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
2113
2499
|
if (m) addPyDep(pyDeps, m[1]);
|
|
2114
2500
|
}
|
|
2501
|
+
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
2502
|
+
if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2115
2503
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
2116
2504
|
let match = poetryRe.exec(content);
|
|
2117
2505
|
while (match !== null) {
|
|
@@ -2310,6 +2698,11 @@ const packageNameFromImport = (spec) => {
|
|
|
2310
2698
|
}
|
|
2311
2699
|
return spec.split("/")[0];
|
|
2312
2700
|
};
|
|
2701
|
+
const typesPackageName = (pkg) => {
|
|
2702
|
+
if (pkg.startsWith("@types/")) return pkg;
|
|
2703
|
+
if (pkg.startsWith("@")) return `@types/${pkg.slice(1).replace("/", "__")}`;
|
|
2704
|
+
return `@types/${pkg}`;
|
|
2705
|
+
};
|
|
2313
2706
|
const STATIC_IMPORT_RE = /^\s*import\s+(?:[\w*{},\s]+\s+from\s+)?["']([^"']+)["']/;
|
|
2314
2707
|
const DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*["']([^"']+)["']/g;
|
|
2315
2708
|
const extractJsImports = (content) => {
|
|
@@ -2374,15 +2767,16 @@ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
|
|
|
2374
2767
|
const realPkg = pkg.slice(7);
|
|
2375
2768
|
if (manifest.jsDeps.has(realPkg)) return null;
|
|
2376
2769
|
}
|
|
2770
|
+
if (manifest.jsDeps.has(typesPackageName(pkg))) return null;
|
|
2377
2771
|
return pkg;
|
|
2378
2772
|
};
|
|
2773
|
+
const normalizePyName = (name) => name.toLowerCase().replace(/_/g, "-");
|
|
2379
2774
|
const checkPyImport = (spec, manifest) => {
|
|
2380
2775
|
const root = spec.split(".")[0];
|
|
2381
2776
|
if (PYTHON_STDLIB.has(root)) return null;
|
|
2382
|
-
const normalized = root
|
|
2777
|
+
const normalized = normalizePyName(root);
|
|
2383
2778
|
if (manifest.pyDeps.has(normalized)) return null;
|
|
2384
|
-
|
|
2385
|
-
if (pipName && manifest.pyDeps.has(pipName)) return null;
|
|
2779
|
+
if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
|
|
2386
2780
|
return root;
|
|
2387
2781
|
};
|
|
2388
2782
|
const detectHallucinatedImports = async (context) => {
|
|
@@ -2532,6 +2926,85 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
2532
2926
|
return blocks;
|
|
2533
2927
|
};
|
|
2534
2928
|
|
|
2929
|
+
//#endregion
|
|
2930
|
+
//#region src/engines/ai-slop/meta-comment.ts
|
|
2931
|
+
const PLAN_REFERENCE_RES = [
|
|
2932
|
+
/\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
|
|
2933
|
+
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
2934
|
+
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
2935
|
+
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
2936
|
+
/\bfrom\s+the\s+(?:task|todo|plan|spec|ticket|prompt|requirements?)\b/i,
|
|
2937
|
+
/\bimplement(?:ing|s|ed)?\s+use\s*case\s+\d*/i,
|
|
2938
|
+
/\b(?:requirements?\s+doc|requirement\s+\d+)\b/i,
|
|
2939
|
+
/\bas\s+(?:instructed|specified|outlined)\s+(?:above|below|in\s+the)\b/i
|
|
2940
|
+
];
|
|
2941
|
+
const BEFORE_AFTER_RES = [
|
|
2942
|
+
/\bpreviously[,:]?\s+(?:this|we|it|the)\b/i,
|
|
2943
|
+
/\bused\s+to\s+(?:be|use|call|return|do|have|rely)\b/i,
|
|
2944
|
+
/\bchanged\s+(?:\w+\s+){0,3}from\s+.+\bto\b/i,
|
|
2945
|
+
/\bno\s+longer\s+(?:needed|used|required|necessary|calls?|returns?|does)\b/i,
|
|
2946
|
+
/\bthis\s+was\s+.+\bbut\s+now\b/i,
|
|
2947
|
+
/\bwe\s+(?:now|used\s+to)\s+(?:no\s+longer\s+)?(?:use|call|return|do|have)\b/i,
|
|
2948
|
+
/\breplaced\s+the\s+(?:old|previous|former)\b/i,
|
|
2949
|
+
/\b(?:was|were)\s+(?:renamed|moved|removed|refactored|extracted)\s+(?:from|to|out\s+of)\b/i
|
|
2950
|
+
];
|
|
2951
|
+
const WHY_OR_TODO_RE = /\b(?:because|since|otherwise|todo|fixme|hack|note:|reason:|workaround|see\s+(?:issue|#))\b/i;
|
|
2952
|
+
const looksLikeLicenseHeader$1 = (block) => {
|
|
2953
|
+
if (block.startLine !== 1) return false;
|
|
2954
|
+
const text = block.rawLines.join(" ").toLowerCase();
|
|
2955
|
+
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
2956
|
+
};
|
|
2957
|
+
const looksLikeSuppressDirective$1 = (block) => block.rawLines.some((line) => /\b(?:biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(line));
|
|
2958
|
+
const matchMetaSignal = (block) => {
|
|
2959
|
+
if (looksLikeLicenseHeader$1(block)) return null;
|
|
2960
|
+
if (looksLikeSuppressDirective$1(block)) return null;
|
|
2961
|
+
if (block.kind === "jsdoc" && block.hasMeaningfulJsdocTag) return null;
|
|
2962
|
+
if (block.isRustDoc) return null;
|
|
2963
|
+
const joined = block.prose.join(" ");
|
|
2964
|
+
if (joined.trim().length === 0) return null;
|
|
2965
|
+
if (WHY_OR_TODO_RE.test(joined)) return null;
|
|
2966
|
+
if (PLAN_REFERENCE_RES.some((re) => re.test(joined))) return "plan/process reference";
|
|
2967
|
+
if (BEFORE_AFTER_RES.some((re) => re.test(joined))) return "before/after state narration";
|
|
2968
|
+
return null;
|
|
2969
|
+
};
|
|
2970
|
+
const detectMetaComments = async (context) => {
|
|
2971
|
+
const files = getSourceFiles(context);
|
|
2972
|
+
const diagnostics = [];
|
|
2973
|
+
for (const filePath of files) {
|
|
2974
|
+
const ext = path.extname(filePath);
|
|
2975
|
+
if (!SUPPORTED_EXTS.has(ext)) continue;
|
|
2976
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2977
|
+
const syntax = getCommentSyntax(ext);
|
|
2978
|
+
if (!syntax) continue;
|
|
2979
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2980
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
2981
|
+
let content;
|
|
2982
|
+
try {
|
|
2983
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2984
|
+
} catch {
|
|
2985
|
+
continue;
|
|
2986
|
+
}
|
|
2987
|
+
const blocks = collectBlocks(content.split("\n"), syntax);
|
|
2988
|
+
for (const block of blocks) {
|
|
2989
|
+
const reason = matchMetaSignal(block);
|
|
2990
|
+
if (!reason) continue;
|
|
2991
|
+
diagnostics.push({
|
|
2992
|
+
filePath: relativePath,
|
|
2993
|
+
engine: "ai-slop",
|
|
2994
|
+
rule: "ai-slop/meta-comment",
|
|
2995
|
+
severity: "warning",
|
|
2996
|
+
message: `Meta/plan comment (${reason})`,
|
|
2997
|
+
help: "Remove — references to the build plan or before/after code state belong in PR descriptions and commit messages, not source.",
|
|
2998
|
+
line: block.startLine,
|
|
2999
|
+
column: 0,
|
|
3000
|
+
category: "Comments",
|
|
3001
|
+
fixable: false
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
return diagnostics;
|
|
3006
|
+
};
|
|
3007
|
+
|
|
2535
3008
|
//#endregion
|
|
2536
3009
|
//#region src/engines/ai-slop/narrative-comments.ts
|
|
2537
3010
|
const looksLikeDeclarationPreamble = (nextLine, ext) => {
|
|
@@ -3140,6 +3613,143 @@ const detectRustPatterns = async (context) => {
|
|
|
3140
3613
|
return diagnostics;
|
|
3141
3614
|
};
|
|
3142
3615
|
|
|
3616
|
+
//#endregion
|
|
3617
|
+
//#region src/engines/ai-slop/silent-recovery.ts
|
|
3618
|
+
const JS_EXTS$1 = new Set([
|
|
3619
|
+
".ts",
|
|
3620
|
+
".tsx",
|
|
3621
|
+
".js",
|
|
3622
|
+
".jsx",
|
|
3623
|
+
".mjs",
|
|
3624
|
+
".cjs"
|
|
3625
|
+
]);
|
|
3626
|
+
const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
|
|
3627
|
+
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3628
|
+
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3629
|
+
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3630
|
+
const extractCatchBody = (content, openBraceIndex) => {
|
|
3631
|
+
let depth = 0;
|
|
3632
|
+
let inString = null;
|
|
3633
|
+
for (let i = openBraceIndex; i < content.length; i += 1) {
|
|
3634
|
+
const ch = content[i];
|
|
3635
|
+
const prev = content[i - 1];
|
|
3636
|
+
if (inString) {
|
|
3637
|
+
if (ch === inString && prev !== "\\") inString = null;
|
|
3638
|
+
continue;
|
|
3639
|
+
}
|
|
3640
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
3641
|
+
inString = ch;
|
|
3642
|
+
continue;
|
|
3643
|
+
}
|
|
3644
|
+
if (ch === "{") depth += 1;
|
|
3645
|
+
else if (ch === "}") {
|
|
3646
|
+
depth -= 1;
|
|
3647
|
+
if (depth === 0) return content.slice(openBraceIndex + 1, i);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
return null;
|
|
3651
|
+
};
|
|
3652
|
+
const isLogOnlyBody = (body) => {
|
|
3653
|
+
const statements = stripBlockComments(body).split("\n").map((line) => line.replace(/\/\/.*$/, "").trim()).filter((line) => line.length > 0 && line !== ";");
|
|
3654
|
+
if (statements.length === 0) return false;
|
|
3655
|
+
if (statements.some((line) => HANDLING_TOKEN_RE.test(line))) return false;
|
|
3656
|
+
let sawLog = false;
|
|
3657
|
+
for (const statement of statements) {
|
|
3658
|
+
const normalized = statement.replace(/;+$/, "");
|
|
3659
|
+
if (LOG_STATEMENT_RE.test(normalized)) {
|
|
3660
|
+
sawLog = true;
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3663
|
+
if (/^[\w$"'`{[(),.\s+:-]+$/.test(normalized) && !/[=(]\s*(?:async\s+)?\(/.test(normalized)) continue;
|
|
3664
|
+
return false;
|
|
3665
|
+
}
|
|
3666
|
+
return sawLog;
|
|
3667
|
+
};
|
|
3668
|
+
const detectJsSilentRecovery = (content, relPath) => {
|
|
3669
|
+
const out = [];
|
|
3670
|
+
CATCH_HEAD_RE.lastIndex = 0;
|
|
3671
|
+
let match;
|
|
3672
|
+
while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
|
|
3673
|
+
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3674
|
+
if (body === null) continue;
|
|
3675
|
+
if (!isLogOnlyBody(body)) continue;
|
|
3676
|
+
const line = content.slice(0, match.index).split("\n").length;
|
|
3677
|
+
out.push({
|
|
3678
|
+
filePath: relPath,
|
|
3679
|
+
engine: "ai-slop",
|
|
3680
|
+
rule: "ai-slop/silent-recovery",
|
|
3681
|
+
severity: "warning",
|
|
3682
|
+
message: "Catch only logs then continues, leaving execution in a possibly broken state",
|
|
3683
|
+
help: "Handle the error: rethrow, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
|
|
3684
|
+
line,
|
|
3685
|
+
column: 0,
|
|
3686
|
+
category: "AI Slop",
|
|
3687
|
+
fixable: false
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
return out;
|
|
3691
|
+
};
|
|
3692
|
+
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3693
|
+
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3694
|
+
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3695
|
+
const detectPySilentRecovery = (content, relPath) => {
|
|
3696
|
+
const out = [];
|
|
3697
|
+
const lines = content.split("\n");
|
|
3698
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3699
|
+
const exceptMatch = PY_EXCEPT_RE.exec(lines[i]);
|
|
3700
|
+
if (!exceptMatch) continue;
|
|
3701
|
+
const indent = exceptMatch[1].length;
|
|
3702
|
+
const bodyLines = [];
|
|
3703
|
+
let j = i + 1;
|
|
3704
|
+
for (; j < lines.length; j += 1) {
|
|
3705
|
+
const raw = lines[j];
|
|
3706
|
+
if (raw.trim() === "") continue;
|
|
3707
|
+
if (raw.length - raw.trimStart().length <= indent) break;
|
|
3708
|
+
bodyLines.push(raw.trim());
|
|
3709
|
+
}
|
|
3710
|
+
if (bodyLines.length === 0) continue;
|
|
3711
|
+
if (bodyLines.some((line) => PY_HANDLING_TOKEN_RE.test(line))) continue;
|
|
3712
|
+
if (bodyLines.some((line) => line === "pass")) continue;
|
|
3713
|
+
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3714
|
+
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3715
|
+
if (!allLogs || !sawLog) continue;
|
|
3716
|
+
out.push({
|
|
3717
|
+
filePath: relPath,
|
|
3718
|
+
engine: "ai-slop",
|
|
3719
|
+
rule: "ai-slop/silent-recovery",
|
|
3720
|
+
severity: "warning",
|
|
3721
|
+
message: "except only logs then continues, leaving execution in a possibly broken state",
|
|
3722
|
+
help: "Handle the error: re-raise, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
|
|
3723
|
+
line: i + 1,
|
|
3724
|
+
column: 0,
|
|
3725
|
+
category: "AI Slop",
|
|
3726
|
+
fixable: false
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
return out;
|
|
3730
|
+
};
|
|
3731
|
+
const detectSilentRecovery = async (context) => {
|
|
3732
|
+
const files = getSourceFiles(context);
|
|
3733
|
+
const diagnostics = [];
|
|
3734
|
+
for (const filePath of files) {
|
|
3735
|
+
if (isAutoGenerated(filePath)) continue;
|
|
3736
|
+
const ext = path.extname(filePath);
|
|
3737
|
+
const isJs = JS_EXTS$1.has(ext);
|
|
3738
|
+
if (!isJs && !(ext === ".py")) continue;
|
|
3739
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3740
|
+
if (isNonProductionPath(relPath)) continue;
|
|
3741
|
+
let content;
|
|
3742
|
+
try {
|
|
3743
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
3744
|
+
} catch {
|
|
3745
|
+
continue;
|
|
3746
|
+
}
|
|
3747
|
+
if (isJs) diagnostics.push(...detectJsSilentRecovery(content, relPath));
|
|
3748
|
+
else diagnostics.push(...detectPySilentRecovery(content, relPath));
|
|
3749
|
+
}
|
|
3750
|
+
return diagnostics;
|
|
3751
|
+
};
|
|
3752
|
+
|
|
3143
3753
|
//#endregion
|
|
3144
3754
|
//#region src/engines/ai-slop/unused-imports.ts
|
|
3145
3755
|
const JS_EXTENSIONS$1 = new Set([
|
|
@@ -3329,15 +3939,19 @@ const aiSlopEngine = {
|
|
|
3329
3939
|
const results = await Promise.allSettled([
|
|
3330
3940
|
detectTrivialComments(context),
|
|
3331
3941
|
detectSwallowedExceptions(context),
|
|
3942
|
+
detectDefensivePatterns(context),
|
|
3332
3943
|
detectOverAbstraction(context),
|
|
3333
3944
|
detectDeadPatterns(context),
|
|
3334
3945
|
detectUnusedImports(context),
|
|
3335
3946
|
detectNarrativeComments(context),
|
|
3336
3947
|
detectDuplicateImports(context),
|
|
3948
|
+
detectHardcodedConfigLiterals(context),
|
|
3337
3949
|
detectPythonPatterns(context),
|
|
3338
3950
|
detectGoPatterns(context),
|
|
3339
3951
|
detectRustPatterns(context),
|
|
3340
|
-
detectHallucinatedImports(context)
|
|
3952
|
+
detectHallucinatedImports(context),
|
|
3953
|
+
detectSilentRecovery(context),
|
|
3954
|
+
detectMetaComments(context)
|
|
3341
3955
|
]);
|
|
3342
3956
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
3343
3957
|
return {
|
|
@@ -6236,7 +6850,7 @@ const RISKY_PATTERNS = [
|
|
|
6236
6850
|
help: "Use JSON, MessagePack, or other safe serialization formats for untrusted data"
|
|
6237
6851
|
},
|
|
6238
6852
|
{
|
|
6239
|
-
pattern: new RegExp(
|
|
6853
|
+
pattern: new RegExp(`(?<![\\w.>:\\\\])\\bexec\\s*\\(`, "g"),
|
|
6240
6854
|
extensions: [".py"],
|
|
6241
6855
|
name: "python-exec",
|
|
6242
6856
|
message: "Use of exec() can execute arbitrary code",
|
|
@@ -6940,7 +7554,7 @@ const parseClaudeStdin = (raw) => {
|
|
|
6940
7554
|
return {};
|
|
6941
7555
|
}
|
|
6942
7556
|
};
|
|
6943
|
-
const readStdin$
|
|
7557
|
+
const readStdin$3 = async () => {
|
|
6944
7558
|
if (process.stdin.isTTY) return "";
|
|
6945
7559
|
const chunks = [];
|
|
6946
7560
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
@@ -6958,7 +7572,7 @@ const renderClaudeOutput = (additional, block) => {
|
|
|
6958
7572
|
return out;
|
|
6959
7573
|
};
|
|
6960
7574
|
const runClaudeHook = async (deps = {}) => {
|
|
6961
|
-
const getStdin = deps.stdin ?? readStdin$
|
|
7575
|
+
const getStdin = deps.stdin ?? readStdin$3;
|
|
6962
7576
|
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
6963
7577
|
const input = parseClaudeStdin(await getStdin());
|
|
6964
7578
|
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
@@ -7005,7 +7619,7 @@ const parseClaudeFileChangedStdin = (raw) => {
|
|
|
7005
7619
|
}
|
|
7006
7620
|
};
|
|
7007
7621
|
const runClaudeFileChangedHook = async (deps = {}) => {
|
|
7008
|
-
const getStdin = deps.stdin ?? readStdin$
|
|
7622
|
+
const getStdin = deps.stdin ?? readStdin$3;
|
|
7009
7623
|
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7010
7624
|
const input = parseClaudeFileChangedStdin(await getStdin());
|
|
7011
7625
|
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
@@ -7041,7 +7655,7 @@ const parseClaudeStopStdin = (raw) => {
|
|
|
7041
7655
|
}
|
|
7042
7656
|
};
|
|
7043
7657
|
const runClaudeStopHook = async (deps = {}) => {
|
|
7044
|
-
const getStdin = deps.stdin ?? readStdin$
|
|
7658
|
+
const getStdin = deps.stdin ?? readStdin$3;
|
|
7045
7659
|
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7046
7660
|
const input = parseClaudeStopStdin(await getStdin());
|
|
7047
7661
|
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
@@ -7102,14 +7716,14 @@ const renderCursorOutput = (additional, event = "afterFileEdit") => ({ hookSpeci
|
|
|
7102
7716
|
hookEventName: event,
|
|
7103
7717
|
additionalContext: additional
|
|
7104
7718
|
} });
|
|
7105
|
-
const readStdin$
|
|
7719
|
+
const readStdin$2 = async () => {
|
|
7106
7720
|
if (process.stdin.isTTY) return "";
|
|
7107
7721
|
const chunks = [];
|
|
7108
7722
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7109
7723
|
return Buffer.concat(chunks).toString("utf-8");
|
|
7110
7724
|
};
|
|
7111
7725
|
const runCursorHook = async (deps = {}) => {
|
|
7112
|
-
const getStdin = deps.stdin ?? readStdin$
|
|
7726
|
+
const getStdin = deps.stdin ?? readStdin$2;
|
|
7113
7727
|
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7114
7728
|
const writeErr = deps.writeErr ?? ((s) => process.stderr.write(s));
|
|
7115
7729
|
const input = parseCursorStdin(await getStdin());
|
|
@@ -7165,14 +7779,14 @@ const renderGeminiOutput = (additional) => ({ hookSpecificOutput: {
|
|
|
7165
7779
|
hookEventName: "AfterTool",
|
|
7166
7780
|
additionalContext: additional
|
|
7167
7781
|
} });
|
|
7168
|
-
const readStdin = async () => {
|
|
7782
|
+
const readStdin$1 = async () => {
|
|
7169
7783
|
if (process.stdin.isTTY) return "";
|
|
7170
7784
|
const chunks = [];
|
|
7171
7785
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7172
7786
|
return Buffer.concat(chunks).toString("utf-8");
|
|
7173
7787
|
};
|
|
7174
7788
|
const runGeminiHook = async (deps = {}) => {
|
|
7175
|
-
const getStdin = deps.stdin ?? readStdin;
|
|
7789
|
+
const getStdin = deps.stdin ?? readStdin$1;
|
|
7176
7790
|
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7177
7791
|
const input = parseGeminiStdin(await getStdin());
|
|
7178
7792
|
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
@@ -7204,6 +7818,76 @@ const runGeminiHook = async (deps = {}) => {
|
|
|
7204
7818
|
}
|
|
7205
7819
|
};
|
|
7206
7820
|
|
|
7821
|
+
//#endregion
|
|
7822
|
+
//#region src/hooks/adapters/pi.ts
|
|
7823
|
+
const parsePiStdin = (raw) => {
|
|
7824
|
+
if (!raw.trim()) return {};
|
|
7825
|
+
try {
|
|
7826
|
+
return JSON.parse(raw);
|
|
7827
|
+
} catch {
|
|
7828
|
+
return {};
|
|
7829
|
+
}
|
|
7830
|
+
};
|
|
7831
|
+
const readStdin = async () => {
|
|
7832
|
+
if (process.stdin.isTTY) return "";
|
|
7833
|
+
const chunks = [];
|
|
7834
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7835
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
7836
|
+
};
|
|
7837
|
+
const formatPiMessage = (feedback) => {
|
|
7838
|
+
if (feedback.counts.total === 0 && !feedback.regressed) return "";
|
|
7839
|
+
const { error, warning } = feedback.counts;
|
|
7840
|
+
const header = `aislop: score ${feedback.score}/100${feedback.baseline != null ? ` (baseline ${feedback.baseline})` : ""}, ${error} error${error === 1 ? "" : "s"}, ${warning} warning${warning === 1 ? "" : "s"}.`;
|
|
7841
|
+
const lines = feedback.findings.map((f) => ` - ${f.file}:${f.line} [${f.severity}] ${f.ruleId}: ${f.message}`);
|
|
7842
|
+
if (feedback.elided && feedback.elided > 0) lines.push(` ...and ${feedback.elided} more.`);
|
|
7843
|
+
const tail = feedback.nextSteps.length > 0 ? `\n${feedback.nextSteps.join("\n")}` : "";
|
|
7844
|
+
return `${header}\n${lines.join("\n")}${tail}`;
|
|
7845
|
+
};
|
|
7846
|
+
const runPiHook = async (deps = {}) => {
|
|
7847
|
+
const getStdin = deps.stdin ?? readStdin;
|
|
7848
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7849
|
+
const input = parsePiStdin(await getStdin());
|
|
7850
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
7851
|
+
const files = resolveHookFiles(cwd, input.file_path ? [input.file_path] : []);
|
|
7852
|
+
if (files.length === 0) return 0;
|
|
7853
|
+
const release = acquireHookLock(cwd);
|
|
7854
|
+
if (!release) return 0;
|
|
7855
|
+
try {
|
|
7856
|
+
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
7857
|
+
const baseline = readBaseline(cwd);
|
|
7858
|
+
appendSessionFiles(cwd, files);
|
|
7859
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline ? {
|
|
7860
|
+
score: baseline.score,
|
|
7861
|
+
findingFingerprints: baseline.findingFingerprints
|
|
7862
|
+
} : void 0, {
|
|
7863
|
+
agent: "pi",
|
|
7864
|
+
touchedFiles: files
|
|
7865
|
+
});
|
|
7866
|
+
track({
|
|
7867
|
+
event: "hook_scan_completed",
|
|
7868
|
+
properties: buildHookScanCompletedProps({
|
|
7869
|
+
agent: "pi",
|
|
7870
|
+
score,
|
|
7871
|
+
scoreDelta: baseline ? score - baseline.score : null,
|
|
7872
|
+
findingCount: diagnostics.length,
|
|
7873
|
+
fileCount: files.length
|
|
7874
|
+
})
|
|
7875
|
+
});
|
|
7876
|
+
const output = {
|
|
7877
|
+
schema: "aislop.hook.v2",
|
|
7878
|
+
block: feedback.counts.error > 0 || feedback.regressed,
|
|
7879
|
+
message: formatPiMessage(feedback),
|
|
7880
|
+
feedback
|
|
7881
|
+
};
|
|
7882
|
+
write(JSON.stringify(output));
|
|
7883
|
+
return 0;
|
|
7884
|
+
} catch {
|
|
7885
|
+
return 0;
|
|
7886
|
+
} finally {
|
|
7887
|
+
release();
|
|
7888
|
+
}
|
|
7889
|
+
};
|
|
7890
|
+
|
|
7207
7891
|
//#endregion
|
|
7208
7892
|
//#region src/hooks/assets.ts
|
|
7209
7893
|
const AISLOP_MD_BODY = `# aislop — agent instructions
|
|
@@ -7774,6 +8458,67 @@ const uninstallKilocode = (opts) => {
|
|
|
7774
8458
|
return uninstallRulesOnly(opts, resolveKilocodePaths(opts));
|
|
7775
8459
|
};
|
|
7776
8460
|
|
|
8461
|
+
//#endregion
|
|
8462
|
+
//#region src/hooks/install/pi.ts
|
|
8463
|
+
const resolvePiPaths = (opts) => {
|
|
8464
|
+
return { extension: opts.scope === "project" ? path.join(opts.cwd, ".pi", "extensions", "aislop.js") : path.join(opts.home, ".pi", "agent", "extensions", "aislop.js") };
|
|
8465
|
+
};
|
|
8466
|
+
const PI_EXTENSION_SOURCE = `// aislop — auto-generated pi extension. Do not edit by hand.
|
|
8467
|
+
// Reinstall with: aislop hook install --pi
|
|
8468
|
+
import { spawnSync } from "node:child_process";
|
|
8469
|
+
|
|
8470
|
+
export default function (pi) {
|
|
8471
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
8472
|
+
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
8473
|
+
if (event.isError) return;
|
|
8474
|
+
const filePath = event.input && event.input.path;
|
|
8475
|
+
if (typeof filePath !== "string" || filePath.length === 0) return;
|
|
8476
|
+
|
|
8477
|
+
const bin = process.env.AISLOP_BIN || "aislop";
|
|
8478
|
+
const payload = JSON.stringify({
|
|
8479
|
+
cwd: ctx.cwd,
|
|
8480
|
+
file_path: filePath,
|
|
8481
|
+
tool_name: event.toolName,
|
|
8482
|
+
});
|
|
8483
|
+
|
|
8484
|
+
let out;
|
|
8485
|
+
try {
|
|
8486
|
+
const res = spawnSync(bin, ["hook", "pi"], {
|
|
8487
|
+
input: payload,
|
|
8488
|
+
encoding: "utf-8",
|
|
8489
|
+
timeout: 15000,
|
|
8490
|
+
});
|
|
8491
|
+
if (res.status !== 0 || !res.stdout) return;
|
|
8492
|
+
out = JSON.parse(res.stdout);
|
|
8493
|
+
} catch {
|
|
8494
|
+
return;
|
|
8495
|
+
}
|
|
8496
|
+
if (!out || !out.message) return;
|
|
8497
|
+
|
|
8498
|
+
return {
|
|
8499
|
+
content: [...event.content, { type: "text", text: out.message }],
|
|
8500
|
+
isError: event.isError,
|
|
8501
|
+
};
|
|
8502
|
+
});
|
|
8503
|
+
}
|
|
8504
|
+
`;
|
|
8505
|
+
const installPi = (opts) => {
|
|
8506
|
+
const paths = resolvePiPaths(opts);
|
|
8507
|
+
const result = emptyResult();
|
|
8508
|
+
applyContent(result, opts, paths.extension, PI_EXTENSION_SOURCE, "write pi aislop extension");
|
|
8509
|
+
return result;
|
|
8510
|
+
};
|
|
8511
|
+
const uninstallPi = (opts) => {
|
|
8512
|
+
const paths = resolvePiPaths(opts);
|
|
8513
|
+
const result = {
|
|
8514
|
+
removed: [],
|
|
8515
|
+
skipped: []
|
|
8516
|
+
};
|
|
8517
|
+
if (readIfExists(paths.extension) != null) applyRemoval(result, opts, paths.extension, null);
|
|
8518
|
+
else result.skipped.push(paths.extension);
|
|
8519
|
+
return result;
|
|
8520
|
+
};
|
|
8521
|
+
|
|
7777
8522
|
//#endregion
|
|
7778
8523
|
//#region src/hooks/install/windsurf.ts
|
|
7779
8524
|
const resolveWindsurfPaths = (opts) => ({ rules: path.join(opts.cwd, ".windsurfrules") });
|
|
@@ -7802,6 +8547,7 @@ const ALL_AGENTS = [
|
|
|
7802
8547
|
"claude",
|
|
7803
8548
|
"cursor",
|
|
7804
8549
|
"gemini",
|
|
8550
|
+
"pi",
|
|
7805
8551
|
"codex",
|
|
7806
8552
|
"windsurf",
|
|
7807
8553
|
"cline",
|
|
@@ -7820,6 +8566,7 @@ const AGENTS_SUPPORTING_BOTH_SCOPES = [
|
|
|
7820
8566
|
"claude",
|
|
7821
8567
|
"cursor",
|
|
7822
8568
|
"gemini",
|
|
8569
|
+
"pi",
|
|
7823
8570
|
"codex"
|
|
7824
8571
|
];
|
|
7825
8572
|
const paths = {
|
|
@@ -7843,6 +8590,7 @@ const paths = {
|
|
|
7843
8590
|
p.geminiMd
|
|
7844
8591
|
];
|
|
7845
8592
|
},
|
|
8593
|
+
pi: (opts) => [resolvePiPaths(opts).extension],
|
|
7846
8594
|
codex: (opts) => [resolveCodexPaths(opts).rules],
|
|
7847
8595
|
windsurf: (opts) => [resolveWindsurfPaths(opts).rules],
|
|
7848
8596
|
cline: (opts) => [resolveClinePaths(opts).rules, resolveRooPaths(opts).rules],
|
|
@@ -7866,6 +8614,11 @@ const REGISTRY = {
|
|
|
7866
8614
|
uninstall: uninstallGemini,
|
|
7867
8615
|
paths: paths.gemini
|
|
7868
8616
|
},
|
|
8617
|
+
pi: {
|
|
8618
|
+
install: installPi,
|
|
8619
|
+
uninstall: uninstallPi,
|
|
8620
|
+
paths: paths.pi
|
|
8621
|
+
},
|
|
7869
8622
|
codex: {
|
|
7870
8623
|
install: installCodex,
|
|
7871
8624
|
uninstall: uninstallCodex,
|
|
@@ -7986,6 +8739,10 @@ const AGENT_LABELS = {
|
|
|
7986
8739
|
label: "Gemini CLI",
|
|
7987
8740
|
hint: "AfterTool, runtime"
|
|
7988
8741
|
},
|
|
8742
|
+
pi: {
|
|
8743
|
+
label: "pi",
|
|
8744
|
+
hint: "extension, runtime"
|
|
8745
|
+
},
|
|
7989
8746
|
codex: {
|
|
7990
8747
|
label: "Codex CLI",
|
|
7991
8748
|
hint: "rules-only"
|
|
@@ -8092,6 +8849,7 @@ const hookRun = async (agent, flags) => {
|
|
|
8092
8849
|
else exitCode = await runClaudeHook();
|
|
8093
8850
|
else if (agent === "cursor") exitCode = await runCursorHook();
|
|
8094
8851
|
else if (agent === "gemini") exitCode = await runGeminiHook();
|
|
8852
|
+
else if (agent === "pi") exitCode = await runPiHook();
|
|
8095
8853
|
else {
|
|
8096
8854
|
process.stderr.write(`hook: agent "${agent}" has no runtime adapter (rules-file-only)\n`);
|
|
8097
8855
|
process.exit(0);
|
|
@@ -8154,6 +8912,7 @@ const AGENT_NAMES = [
|
|
|
8154
8912
|
"claude",
|
|
8155
8913
|
"cursor",
|
|
8156
8914
|
"gemini",
|
|
8915
|
+
"pi",
|
|
8157
8916
|
"codex",
|
|
8158
8917
|
"windsurf",
|
|
8159
8918
|
"cline",
|
|
@@ -8276,6 +9035,9 @@ const registerCallbacks = (hook) => {
|
|
|
8276
9035
|
hook.command("gemini").description("Internal: Gemini CLI AfterTool callback (reads stdin)").action(async () => {
|
|
8277
9036
|
await hookRun("gemini");
|
|
8278
9037
|
});
|
|
9038
|
+
hook.command("pi").description("Internal: pi extension tool_result callback (reads stdin)").action(async () => {
|
|
9039
|
+
await hookRun("pi");
|
|
9040
|
+
});
|
|
8279
9041
|
};
|
|
8280
9042
|
const registerHookCommand = (program) => {
|
|
8281
9043
|
const hook = program.command("hook").description("Install or invoke AI-agent integration hooks");
|
|
@@ -8608,6 +9370,30 @@ const printEngineStatus = (result) => {
|
|
|
8608
9370
|
}
|
|
8609
9371
|
};
|
|
8610
9372
|
|
|
9373
|
+
//#endregion
|
|
9374
|
+
//#region src/scoring/rule-severity.ts
|
|
9375
|
+
/**
|
|
9376
|
+
* Apply per-rule severity overrides from config: "off" drops the diagnostic,
|
|
9377
|
+
* "error"/"warning" rewrite its severity before scoring and rendering.
|
|
9378
|
+
*/
|
|
9379
|
+
const applyRuleSeverities = (diagnostics, overrides) => {
|
|
9380
|
+
if (Object.keys(overrides).length === 0) return diagnostics;
|
|
9381
|
+
const result = [];
|
|
9382
|
+
for (const diagnostic of diagnostics) {
|
|
9383
|
+
const override = overrides[diagnostic.rule];
|
|
9384
|
+
if (!override) {
|
|
9385
|
+
result.push(diagnostic);
|
|
9386
|
+
continue;
|
|
9387
|
+
}
|
|
9388
|
+
if (override === "off") continue;
|
|
9389
|
+
result.push(override === diagnostic.severity ? diagnostic : {
|
|
9390
|
+
...diagnostic,
|
|
9391
|
+
severity: override
|
|
9392
|
+
});
|
|
9393
|
+
}
|
|
9394
|
+
return result;
|
|
9395
|
+
};
|
|
9396
|
+
|
|
8611
9397
|
//#endregion
|
|
8612
9398
|
//#region src/ui/header.ts
|
|
8613
9399
|
const TAGLINE = "the quality gate for agentic coding";
|
|
@@ -8785,6 +9571,11 @@ const RULE_LABELS = {
|
|
|
8785
9571
|
"knip/types": "Unused type",
|
|
8786
9572
|
"ai-slop/trivial-comment": "Trivial restating comment",
|
|
8787
9573
|
"ai-slop/swallowed-exception": "Empty catch (swallowed error)",
|
|
9574
|
+
"ai-slop/silent-recovery": "Catch logs then continues",
|
|
9575
|
+
"ai-slop/meta-comment": "Meta/plan comment",
|
|
9576
|
+
"ai-slop/redundant-try-catch": "Redundant try/catch",
|
|
9577
|
+
"ai-slop/redundant-type-coercion": "Redundant type coercion",
|
|
9578
|
+
"ai-slop/duplicate-type-declaration": "Duplicate exported type",
|
|
8788
9579
|
"ai-slop/thin-wrapper": "Thin function wrapper",
|
|
8789
9580
|
"ai-slop/generic-naming": "Generic/vague identifier name",
|
|
8790
9581
|
"ai-slop/unused-import": "Unused import",
|
|
@@ -8798,6 +9589,8 @@ const RULE_LABELS = {
|
|
|
8798
9589
|
"ai-slop/ts-directive": "@ts-ignore / @ts-expect-error",
|
|
8799
9590
|
"ai-slop/narrative-comment": "Narrative comment block",
|
|
8800
9591
|
"ai-slop/duplicate-import": "Duplicate import statement",
|
|
9592
|
+
"ai-slop/hardcoded-url": "Hardcoded URL",
|
|
9593
|
+
"ai-slop/hardcoded-id": "Hardcoded provider ID",
|
|
8801
9594
|
"ai-slop/python-bare-except": "Bare except",
|
|
8802
9595
|
"ai-slop/python-broad-except": "Broad except",
|
|
8803
9596
|
"ai-slop/python-mutable-default": "Mutable default argument",
|
|
@@ -8924,8 +9717,54 @@ const renderCleanRun = (input, deps = {}) => {
|
|
|
8924
9717
|
return `\n ${parts.join(` ${sep} `)}\n`;
|
|
8925
9718
|
};
|
|
8926
9719
|
|
|
9720
|
+
//#endregion
|
|
9721
|
+
//#region src/utils/history.ts
|
|
9722
|
+
const HISTORY_FILE = "history.jsonl";
|
|
9723
|
+
const isHistoryDisabled = (env = process.env) => env.AISLOP_NO_HISTORY === "1";
|
|
9724
|
+
const historyPath = (directory) => path.join(path.resolve(directory), CONFIG_DIR, HISTORY_FILE);
|
|
9725
|
+
/**
|
|
9726
|
+
* Append a compact scan record to .aislop/history.jsonl. Best-effort: never
|
|
9727
|
+
* throws, so a read-only checkout or missing config dir can't break a scan.
|
|
9728
|
+
*/
|
|
9729
|
+
const appendHistory = (input) => {
|
|
9730
|
+
if (isHistoryDisabled()) return;
|
|
9731
|
+
const configDir = path.join(path.resolve(input.directory), CONFIG_DIR);
|
|
9732
|
+
if (!fs.existsSync(configDir)) return;
|
|
9733
|
+
const record = {
|
|
9734
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9735
|
+
score: input.score,
|
|
9736
|
+
errors: input.errors,
|
|
9737
|
+
warnings: input.warnings,
|
|
9738
|
+
files: input.files,
|
|
9739
|
+
cliVersion: APP_VERSION
|
|
9740
|
+
};
|
|
9741
|
+
try {
|
|
9742
|
+
fs.appendFileSync(historyPath(input.directory), `${JSON.stringify(record)}\n`);
|
|
9743
|
+
} catch {}
|
|
9744
|
+
};
|
|
9745
|
+
const isHistoryRecord = (value) => {
|
|
9746
|
+
if (!value || typeof value !== "object") return false;
|
|
9747
|
+
const record = value;
|
|
9748
|
+
return typeof record.timestamp === "string" && typeof record.score === "number" && typeof record.errors === "number" && typeof record.warnings === "number" && typeof record.files === "number" && typeof record.cliVersion === "string";
|
|
9749
|
+
};
|
|
9750
|
+
const readHistory = (directory) => {
|
|
9751
|
+
const file = historyPath(directory);
|
|
9752
|
+
if (!fs.existsSync(file)) return [];
|
|
9753
|
+
const records = [];
|
|
9754
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
9755
|
+
const trimmed = line.trim();
|
|
9756
|
+
if (!trimmed) continue;
|
|
9757
|
+
try {
|
|
9758
|
+
const parsed = JSON.parse(trimmed);
|
|
9759
|
+
if (isHistoryRecord(parsed)) records.push(parsed);
|
|
9760
|
+
} catch {}
|
|
9761
|
+
}
|
|
9762
|
+
return records;
|
|
9763
|
+
};
|
|
9764
|
+
|
|
8927
9765
|
//#endregion
|
|
8928
9766
|
//#region src/commands/scan.ts
|
|
9767
|
+
const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
|
|
8929
9768
|
const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
|
|
8930
9769
|
const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
|
|
8931
9770
|
const BREAKDOWN_TOP_N = 10;
|
|
@@ -9040,17 +9879,18 @@ const scanCommand = async (directory, config, options) => {
|
|
|
9040
9879
|
const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
9041
9880
|
const startTime = performance.now();
|
|
9042
9881
|
const showHeader = options.showHeader !== false;
|
|
9043
|
-
const
|
|
9882
|
+
const machineOutput = isMachineOutput(options);
|
|
9883
|
+
const useLiveProgress = !machineOutput && shouldUseSpinner();
|
|
9044
9884
|
let files;
|
|
9045
9885
|
if (options.staged) {
|
|
9046
9886
|
files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
|
|
9047
|
-
if (!
|
|
9887
|
+
if (!machineOutput) log.muted(`Scope: ${files.length} staged file(s)`);
|
|
9048
9888
|
} else if (options.changes) {
|
|
9049
9889
|
files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], config.exclude);
|
|
9050
|
-
if (!
|
|
9890
|
+
if (!machineOutput) log.muted(`Scope: ${files.length} changed file(s)`);
|
|
9051
9891
|
} else {
|
|
9052
9892
|
files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], config.exclude);
|
|
9053
|
-
if (!
|
|
9893
|
+
if (!machineOutput) log.muted(`Scope: ${files.length} file(s) after exclusions`);
|
|
9054
9894
|
}
|
|
9055
9895
|
const configDir = findConfigDir(resolvedDir);
|
|
9056
9896
|
const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
|
|
@@ -9067,7 +9907,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
9067
9907
|
}));
|
|
9068
9908
|
const progressRenderer = useLiveProgress ? new LiveGrid(gridRows) : null;
|
|
9069
9909
|
progressRenderer?.start();
|
|
9070
|
-
const
|
|
9910
|
+
const rawResults = await runEngines({
|
|
9071
9911
|
rootDirectory: resolvedDir,
|
|
9072
9912
|
languages: projectInfo.languages,
|
|
9073
9913
|
frameworks: projectInfo.frameworks,
|
|
@@ -9100,9 +9940,13 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
9100
9940
|
elapsedMs: result.elapsed
|
|
9101
9941
|
});
|
|
9102
9942
|
}
|
|
9103
|
-
if (!
|
|
9943
|
+
if (!machineOutput && !progressRenderer) printEngineStatus(result);
|
|
9104
9944
|
});
|
|
9105
9945
|
progressRenderer?.stop();
|
|
9946
|
+
const results = rawResults.map((result) => ({
|
|
9947
|
+
...result,
|
|
9948
|
+
diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
|
|
9949
|
+
}));
|
|
9106
9950
|
const allDiagnostics = results.flatMap((r) => r.diagnostics);
|
|
9107
9951
|
const elapsedMs = performance.now() - startTime;
|
|
9108
9952
|
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
@@ -9123,12 +9967,24 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
9123
9967
|
engineIssues,
|
|
9124
9968
|
engineTimings
|
|
9125
9969
|
};
|
|
9970
|
+
if (options.sarif) {
|
|
9971
|
+
const { buildSarifLog } = await import("./sarif-CZVuavf_.js");
|
|
9972
|
+
console.log(JSON.stringify(buildSarifLog(results), null, 2));
|
|
9973
|
+
return completion;
|
|
9974
|
+
}
|
|
9126
9975
|
if (options.json) {
|
|
9127
9976
|
const { buildJsonOutput } = await import("./json-OIzja7OM.js");
|
|
9128
9977
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
9129
9978
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
9130
9979
|
return completion;
|
|
9131
9980
|
}
|
|
9981
|
+
if (!options.staged && !options.changes && options.command !== "ci" && !isCiEnv()) appendHistory({
|
|
9982
|
+
directory: resolvedDir,
|
|
9983
|
+
score: scoreResult.score,
|
|
9984
|
+
errors: completion.errorCount,
|
|
9985
|
+
warnings: completion.warningCount,
|
|
9986
|
+
files: projectInfo.sourceFileCount
|
|
9987
|
+
});
|
|
9132
9988
|
const projectName = projectInfo.projectName ?? "project";
|
|
9133
9989
|
const language = projectInfo.languages[0] ?? "unknown";
|
|
9134
9990
|
process.stdout.write(buildScanRender({
|
|
@@ -9155,7 +10011,8 @@ const ciCommand = async (directory, config, options = {}) => {
|
|
|
9155
10011
|
changes: false,
|
|
9156
10012
|
staged: false,
|
|
9157
10013
|
verbose: false,
|
|
9158
|
-
json: !options.human,
|
|
10014
|
+
json: !options.human && !options.sarif,
|
|
10015
|
+
sarif: options.sarif,
|
|
9159
10016
|
command: "ci"
|
|
9160
10017
|
});
|
|
9161
10018
|
} catch (error) {
|
|
@@ -9681,6 +10538,16 @@ const AGENT_CONFIGS = {
|
|
|
9681
10538
|
bin: "goose",
|
|
9682
10539
|
args: (p) => ["run", p]
|
|
9683
10540
|
},
|
|
10541
|
+
pi: {
|
|
10542
|
+
type: "cli",
|
|
10543
|
+
bin: "pi",
|
|
10544
|
+
args: (p) => ["-p", p]
|
|
10545
|
+
},
|
|
10546
|
+
crush: {
|
|
10547
|
+
type: "cli",
|
|
10548
|
+
bin: "crush",
|
|
10549
|
+
args: (p) => ["run", p]
|
|
10550
|
+
},
|
|
9684
10551
|
cursor: {
|
|
9685
10552
|
type: "editor",
|
|
9686
10553
|
bin: "cursor"
|
|
@@ -11535,6 +12402,11 @@ const BUILTIN_RULES = [
|
|
|
11535
12402
|
rules: [
|
|
11536
12403
|
"ai-slop/trivial-comment",
|
|
11537
12404
|
"ai-slop/swallowed-exception",
|
|
12405
|
+
"ai-slop/silent-recovery",
|
|
12406
|
+
"ai-slop/meta-comment",
|
|
12407
|
+
"ai-slop/redundant-try-catch",
|
|
12408
|
+
"ai-slop/redundant-type-coercion",
|
|
12409
|
+
"ai-slop/duplicate-type-declaration",
|
|
11538
12410
|
"ai-slop/thin-wrapper",
|
|
11539
12411
|
"ai-slop/generic-naming",
|
|
11540
12412
|
"ai-slop/unused-import",
|
|
@@ -11548,6 +12420,8 @@ const BUILTIN_RULES = [
|
|
|
11548
12420
|
"ai-slop/ts-directive",
|
|
11549
12421
|
"ai-slop/narrative-comment",
|
|
11550
12422
|
"ai-slop/duplicate-import",
|
|
12423
|
+
"ai-slop/hardcoded-url",
|
|
12424
|
+
"ai-slop/hardcoded-id",
|
|
11551
12425
|
"ai-slop/python-bare-except",
|
|
11552
12426
|
"ai-slop/python-broad-except",
|
|
11553
12427
|
"ai-slop/python-mutable-default",
|
|
@@ -11714,6 +12588,73 @@ const interactiveCommand = async (directory, config) => {
|
|
|
11714
12588
|
}
|
|
11715
12589
|
};
|
|
11716
12590
|
|
|
12591
|
+
//#endregion
|
|
12592
|
+
//#region src/commands/trend.ts
|
|
12593
|
+
const SPARK_TICKS = [
|
|
12594
|
+
"▁",
|
|
12595
|
+
"▂",
|
|
12596
|
+
"▃",
|
|
12597
|
+
"▄",
|
|
12598
|
+
"▅",
|
|
12599
|
+
"▆",
|
|
12600
|
+
"▇",
|
|
12601
|
+
"█"
|
|
12602
|
+
];
|
|
12603
|
+
const DEFAULT_LIMIT = 20;
|
|
12604
|
+
const renderSparkline = (scores) => {
|
|
12605
|
+
if (scores.length === 0) return "";
|
|
12606
|
+
const min = Math.min(...scores);
|
|
12607
|
+
const span = Math.max(...scores) - min;
|
|
12608
|
+
return scores.map((score) => {
|
|
12609
|
+
if (span === 0) return SPARK_TICKS[SPARK_TICKS.length - 1];
|
|
12610
|
+
const ratio = (score - min) / span;
|
|
12611
|
+
return SPARK_TICKS[Math.round(ratio * (SPARK_TICKS.length - 1))];
|
|
12612
|
+
}).join("");
|
|
12613
|
+
};
|
|
12614
|
+
const formatDate = (timestamp) => {
|
|
12615
|
+
const date = new Date(timestamp);
|
|
12616
|
+
if (Number.isNaN(date.getTime())) return timestamp;
|
|
12617
|
+
return date.toISOString().slice(0, 16).replace("T", " ");
|
|
12618
|
+
};
|
|
12619
|
+
const delta = (current, previous) => {
|
|
12620
|
+
if (previous === void 0) return "";
|
|
12621
|
+
const diff = current - previous;
|
|
12622
|
+
if (diff > 0) return style(theme, "success", `+${diff}`);
|
|
12623
|
+
if (diff < 0) return style(theme, "danger", `${diff}`);
|
|
12624
|
+
return style(theme, "muted", "0");
|
|
12625
|
+
};
|
|
12626
|
+
const buildTrendRender = (input) => {
|
|
12627
|
+
const header = renderHeader({
|
|
12628
|
+
version: APP_VERSION,
|
|
12629
|
+
command: "trend",
|
|
12630
|
+
context: [],
|
|
12631
|
+
brand: input.printBrand !== false
|
|
12632
|
+
});
|
|
12633
|
+
if (input.records.length === 0) return `${header}\n ${style(theme, "muted", "No score history yet. Run a scan to start tracking trends.")}\n`;
|
|
12634
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
12635
|
+
const recent = input.records.slice(-limit);
|
|
12636
|
+
const scores = recent.map((r) => r.score);
|
|
12637
|
+
const lines = [header];
|
|
12638
|
+
lines.push(` ${style(theme, "dim", padEnd("Date", 18))}${style(theme, "dim", padEnd("Score", 8))}${style(theme, "dim", padEnd("Δ", 6))}${style(theme, "dim", padEnd("Err", 6))}${style(theme, "dim", "Warn")}`);
|
|
12639
|
+
recent.forEach((record, index) => {
|
|
12640
|
+
const previous = index > 0 ? recent[index - 1]?.score : void 0;
|
|
12641
|
+
lines.push(` ${padEnd(formatDate(record.timestamp), 18)}${padEnd(String(record.score), 8)}${padEnd(delta(record.score, previous), 6)}${padEnd(String(record.errors), 6)}${record.warnings}`);
|
|
12642
|
+
});
|
|
12643
|
+
const latest = recent[recent.length - 1];
|
|
12644
|
+
lines.push("");
|
|
12645
|
+
lines.push(` ${style(theme, "accent", renderSparkline(scores))}`);
|
|
12646
|
+
lines.push(` ${style(theme, "muted", `${recent.length} run(s), latest score ${latest?.score}`)}`);
|
|
12647
|
+
lines.push(renderHintLine("Run aislop scan to add a new data point").trimEnd());
|
|
12648
|
+
return `${lines.join("\n")}\n`;
|
|
12649
|
+
};
|
|
12650
|
+
const trendCommand = (directory, limit) => {
|
|
12651
|
+
const records = readHistory(directory);
|
|
12652
|
+
process.stdout.write(buildTrendRender({
|
|
12653
|
+
records,
|
|
12654
|
+
limit
|
|
12655
|
+
}));
|
|
12656
|
+
};
|
|
12657
|
+
|
|
11717
12658
|
//#endregion
|
|
11718
12659
|
//#region src/cli.ts
|
|
11719
12660
|
process.on("SIGINT", () => process.exit(0));
|
|
@@ -11729,17 +12670,22 @@ const commaSeparatedParser = (value, previous = []) => {
|
|
|
11729
12670
|
const parts = value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
11730
12671
|
return [...previous, ...parts];
|
|
11731
12672
|
};
|
|
12673
|
+
const wantsSarif = (flags) => Boolean(flags.sarif) || flags.format === "sarif";
|
|
12674
|
+
const wantsJson = (flags) => Boolean(flags.json) || flags.format === "json";
|
|
11732
12675
|
const runScan = async (directory, flags) => {
|
|
11733
12676
|
const config = loadConfig(directory);
|
|
11734
|
-
const
|
|
12677
|
+
const finalConfig = {
|
|
11735
12678
|
...config,
|
|
11736
12679
|
exclude: [...config.exclude ?? [], ...flags.exclude ?? []],
|
|
11737
12680
|
include: [...config.include ?? [], ...flags.include ?? []]
|
|
11738
|
-
}
|
|
12681
|
+
};
|
|
12682
|
+
const sarif = wantsSarif(flags);
|
|
12683
|
+
const { exitCode } = await scanCommand(directory, finalConfig, {
|
|
11739
12684
|
changes: Boolean(flags.changes),
|
|
11740
12685
|
staged: Boolean(flags.staged),
|
|
11741
12686
|
verbose: Boolean(flags.verbose),
|
|
11742
|
-
json:
|
|
12687
|
+
json: !sarif && wantsJson(flags),
|
|
12688
|
+
sarif,
|
|
11743
12689
|
exclude: flags.exclude,
|
|
11744
12690
|
include: flags.include
|
|
11745
12691
|
});
|
|
@@ -11748,8 +12694,8 @@ const runScan = async (directory, flags) => {
|
|
|
11748
12694
|
process.exitCode = exitCode;
|
|
11749
12695
|
}
|
|
11750
12696
|
};
|
|
11751
|
-
const noFlagsPassed = (flags) => !flags.changes && !flags.staged && !flags.verbose && !flags.json && !(flags.exclude && flags.exclude.length > 0) && !(flags.include && flags.include.length > 0);
|
|
11752
|
-
const program = new Command().name("aislop").description("The unified code quality CLI").version(APP_VERSION, "-v, --version").argument("[directory]", "project directory to scan", ".").option("--changes", "only scan changed files (git diff)").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON instead of terminal UI").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory, flags) => {
|
|
12697
|
+
const noFlagsPassed = (flags) => !flags.changes && !flags.staged && !flags.verbose && !flags.json && !flags.sarif && !flags.format && !(flags.exclude && flags.exclude.length > 0) && !(flags.include && flags.include.length > 0);
|
|
12698
|
+
const program = new Command().name("aislop").description("The unified code quality CLI").version(APP_VERSION, "-v, --version").argument("[directory]", "project directory to scan", ".").option("--changes", "only scan changed files (git diff)").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON instead of terminal UI").option("--sarif", "output SARIF 2.1.0 (for GitHub code scanning)").option("--format <format>", "output format: json or sarif").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory, flags) => {
|
|
11753
12699
|
if (noFlagsPassed(flags) && process.stdin.isTTY) try {
|
|
11754
12700
|
await interactiveCommand(directory, loadConfig(directory));
|
|
11755
12701
|
return;
|
|
@@ -11767,6 +12713,7 @@ ${style(theme, "dim", "Commands:")}
|
|
|
11767
12713
|
npx aislop doctor [dir] Check installed tools
|
|
11768
12714
|
npx aislop ci [dir] CI-friendly JSON output
|
|
11769
12715
|
npx aislop rules [dir] List all rules
|
|
12716
|
+
npx aislop trend [dir] Show score history trend
|
|
11770
12717
|
|
|
11771
12718
|
${style(theme, "dim", "Examples:")}
|
|
11772
12719
|
npx aislop Interactive menu
|
|
@@ -11780,12 +12727,14 @@ ${style(theme, "dim", "Examples:")}
|
|
|
11780
12727
|
npx aislop fix --cursor Open Cursor + copy prompt to clipboard
|
|
11781
12728
|
npx aislop fix -p Print a prompt to paste into any coding agent
|
|
11782
12729
|
npx aislop ci JSON output for CI pipelines
|
|
12730
|
+
npx aislop scan --sarif SARIF 2.1.0 for GitHub code scanning
|
|
12731
|
+
npx aislop trend Show score history over time
|
|
11783
12732
|
npx aislop scan --exclude node_modules
|
|
11784
12733
|
npx aislop scan --exclude node_modules,dist,file.txt
|
|
11785
12734
|
npx aislop scan --exclude node_modules --exclude dist --exclude **/*.ts
|
|
11786
12735
|
${renderHintLine("Run npx aislop scan to scan your project").trimEnd()}
|
|
11787
12736
|
`);
|
|
11788
|
-
program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory = ".", _flags, command) => {
|
|
12737
|
+
program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").option("--sarif", "output SARIF 2.1.0 (for GitHub code scanning)").option("--format <format>", "output format: json or sarif").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory = ".", _flags, command) => {
|
|
11789
12738
|
await runScan(directory, command.optsWithGlobals());
|
|
11790
12739
|
});
|
|
11791
12740
|
const FIX_AGENT_FLAGS = [
|
|
@@ -11858,6 +12807,16 @@ const FIX_AGENT_FLAGS = [
|
|
|
11858
12807
|
flag: "goose",
|
|
11859
12808
|
name: "goose",
|
|
11860
12809
|
help: "open Goose to fix remaining issues"
|
|
12810
|
+
},
|
|
12811
|
+
{
|
|
12812
|
+
flag: "pi",
|
|
12813
|
+
name: "pi",
|
|
12814
|
+
help: "open pi to fix remaining issues"
|
|
12815
|
+
},
|
|
12816
|
+
{
|
|
12817
|
+
flag: "crush",
|
|
12818
|
+
name: "crush",
|
|
12819
|
+
help: "open Crush to fix remaining issues"
|
|
11861
12820
|
}
|
|
11862
12821
|
];
|
|
11863
12822
|
const matchFixAgent = (flags) => {
|
|
@@ -11893,9 +12852,12 @@ program.command("doctor [directory]").description("Check installed tools and env
|
|
|
11893
12852
|
return { exitCode: 0 };
|
|
11894
12853
|
});
|
|
11895
12854
|
});
|
|
11896
|
-
program.command("ci [directory]").description("CI-friendly JSON output with exit codes").option("--human", "render the human-friendly scan design instead of JSON").action(async (directory = ".", _flags, command) => {
|
|
12855
|
+
program.command("ci [directory]").description("CI-friendly JSON output with exit codes").option("--human", "render the human-friendly scan design instead of JSON").option("--sarif", "output SARIF 2.1.0 (for GitHub code scanning)").option("--format <format>", "output format: json or sarif").action(async (directory = ".", _flags, command) => {
|
|
11897
12856
|
const flags = command.optsWithGlobals();
|
|
11898
|
-
const { exitCode } = await ciCommand(directory, loadConfig(directory), {
|
|
12857
|
+
const { exitCode } = await ciCommand(directory, loadConfig(directory), {
|
|
12858
|
+
human: Boolean(flags.human),
|
|
12859
|
+
sarif: Boolean(flags.sarif) || flags.format === "sarif"
|
|
12860
|
+
});
|
|
11899
12861
|
if (exitCode !== 0) {
|
|
11900
12862
|
await flushTelemetry();
|
|
11901
12863
|
process.exitCode = exitCode;
|
|
@@ -11931,6 +12893,16 @@ program.command("badge [directory]").description("Print the public score badge U
|
|
|
11931
12893
|
process.exit(1);
|
|
11932
12894
|
}
|
|
11933
12895
|
});
|
|
12896
|
+
program.command("trend [directory]").description("Show score history trend from .aislop/history.jsonl").option("--limit <n>", "number of recent runs to show", (v) => Number.parseInt(v, 10)).action(async (directory = ".", _flags, command) => {
|
|
12897
|
+
const flags = command.optsWithGlobals();
|
|
12898
|
+
await withCommandLifecycle({
|
|
12899
|
+
command: "trend",
|
|
12900
|
+
config: loadConfig(directory).telemetry
|
|
12901
|
+
}, async () => {
|
|
12902
|
+
trendCommand(directory, flags.limit);
|
|
12903
|
+
return { exitCode: 0 };
|
|
12904
|
+
});
|
|
12905
|
+
});
|
|
11934
12906
|
registerHookCommand(program);
|
|
11935
12907
|
const main = async () => {
|
|
11936
12908
|
fireInstalledOnce();
|