trimference 0.3.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/BENCHMARK.md +39 -0
- package/CHANGELOG.md +41 -0
- package/CONTRIBUTING.md +43 -0
- package/DEMO.md +106 -0
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/SECURITY.md +17 -0
- package/action.yml +182 -0
- package/dist/baseline.d.ts +13 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +23 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +245 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +127 -0
- package/dist/config.js.map +1 -0
- package/dist/ecoLLM.d.ts +33 -0
- package/dist/ecoLLM.d.ts.map +1 -0
- package/dist/ecoLLM.js +98 -0
- package/dist/ecoLLM.js.map +1 -0
- package/dist/impact.d.ts +22 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +80 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +21 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +58 -0
- package/dist/models.js.map +1 -0
- package/dist/prComment.d.ts +36 -0
- package/dist/prComment.d.ts.map +1 -0
- package/dist/prComment.js +45 -0
- package/dist/prComment.js.map +1 -0
- package/dist/reporters/impactTracker.d.ts +21 -0
- package/dist/reporters/impactTracker.d.ts.map +1 -0
- package/dist/reporters/impactTracker.js +54 -0
- package/dist/reporters/impactTracker.js.map +1 -0
- package/dist/reporters/jsonReporter.d.ts +4 -0
- package/dist/reporters/jsonReporter.d.ts.map +1 -0
- package/dist/reporters/jsonReporter.js +34 -0
- package/dist/reporters/jsonReporter.js.map +1 -0
- package/dist/reporters/markdownReporter.d.ts +4 -0
- package/dist/reporters/markdownReporter.d.ts.map +1 -0
- package/dist/reporters/markdownReporter.js +97 -0
- package/dist/reporters/markdownReporter.js.map +1 -0
- package/dist/reporters/sarifReporter.d.ts +4 -0
- package/dist/reporters/sarifReporter.d.ts.map +1 -0
- package/dist/reporters/sarifReporter.js +63 -0
- package/dist/reporters/sarifReporter.js.map +1 -0
- package/dist/reporters/terminalReporter.d.ts +10 -0
- package/dist/reporters/terminalReporter.d.ts.map +1 -0
- package/dist/reporters/terminalReporter.js +80 -0
- package/dist/reporters/terminalReporter.js.map +1 -0
- package/dist/rules/agentLoopWithoutBudget.d.ts +3 -0
- package/dist/rules/agentLoopWithoutBudget.d.ts.map +1 -0
- package/dist/rules/agentLoopWithoutBudget.js +105 -0
- package/dist/rules/agentLoopWithoutBudget.js.map +1 -0
- package/dist/rules/expensiveModelSimpleTask.d.ts +3 -0
- package/dist/rules/expensiveModelSimpleTask.d.ts.map +1 -0
- package/dist/rules/expensiveModelSimpleTask.js +119 -0
- package/dist/rules/expensiveModelSimpleTask.js.map +1 -0
- package/dist/rules/frequentCron.d.ts +3 -0
- package/dist/rules/frequentCron.d.ts.map +1 -0
- package/dist/rules/frequentCron.js +64 -0
- package/dist/rules/frequentCron.js.map +1 -0
- package/dist/rules/helpers.d.ts +65 -0
- package/dist/rules/helpers.d.ts.map +1 -0
- package/dist/rules/helpers.js +383 -0
- package/dist/rules/helpers.js.map +1 -0
- package/dist/rules/hugeContext.d.ts +3 -0
- package/dist/rules/hugeContext.d.ts.map +1 -0
- package/dist/rules/hugeContext.js +74 -0
- package/dist/rules/hugeContext.js.map +1 -0
- package/dist/rules/imageGenerationLoop.d.ts +3 -0
- package/dist/rules/imageGenerationLoop.d.ts.map +1 -0
- package/dist/rules/imageGenerationLoop.js +72 -0
- package/dist/rules/imageGenerationLoop.js.map +1 -0
- package/dist/rules/index.d.ts +14 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +24 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/missingRateLimit.d.ts +3 -0
- package/dist/rules/missingRateLimit.d.ts.map +1 -0
- package/dist/rules/missingRateLimit.js +78 -0
- package/dist/rules/missingRateLimit.js.map +1 -0
- package/dist/rules/noLlmCache.d.ts +3 -0
- package/dist/rules/noLlmCache.d.ts.map +1 -0
- package/dist/rules/noLlmCache.js +71 -0
- package/dist/rules/noLlmCache.js.map +1 -0
- package/dist/rules/noTokenLimit.d.ts +3 -0
- package/dist/rules/noTokenLimit.d.ts.map +1 -0
- package/dist/rules/noTokenLimit.js +59 -0
- package/dist/rules/noTokenLimit.js.map +1 -0
- package/dist/rules/repeatedEmbeddings.d.ts +3 -0
- package/dist/rules/repeatedEmbeddings.d.ts.map +1 -0
- package/dist/rules/repeatedEmbeddings.js +81 -0
- package/dist/rules/repeatedEmbeddings.js.map +1 -0
- package/dist/rules/sequentialLlmCalls.d.ts +3 -0
- package/dist/rules/sequentialLlmCalls.d.ts.map +1 -0
- package/dist/rules/sequentialLlmCalls.js +69 -0
- package/dist/rules/sequentialLlmCalls.js.map +1 -0
- package/dist/scanner.d.ts +23 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +217 -0
- package/dist/scanner.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Rule } from "../types.js";
|
|
2
|
+
import { noLlmCacheRule } from "./noLlmCache.js";
|
|
3
|
+
import { hugeContextRule } from "./hugeContext.js";
|
|
4
|
+
import { expensiveModelSimpleTaskRule } from "./expensiveModelSimpleTask.js";
|
|
5
|
+
import { repeatedEmbeddingsRule } from "./repeatedEmbeddings.js";
|
|
6
|
+
import { imageGenerationLoopRule } from "./imageGenerationLoop.js";
|
|
7
|
+
import { frequentCronRule } from "./frequentCron.js";
|
|
8
|
+
import { noTokenLimitRule } from "./noTokenLimit.js";
|
|
9
|
+
import { sequentialLlmCallsRule } from "./sequentialLlmCalls.js";
|
|
10
|
+
import { agentLoopWithoutBudgetRule } from "./agentLoopWithoutBudget.js";
|
|
11
|
+
import { missingRateLimitRule } from "./missingRateLimit.js";
|
|
12
|
+
export declare const rules: Rule[];
|
|
13
|
+
export { noLlmCacheRule, hugeContextRule, expensiveModelSimpleTaskRule, repeatedEmbeddingsRule, imageGenerationLoopRule, frequentCronRule, noTokenLimitRule, sequentialLlmCallsRule, agentLoopWithoutBudgetRule, missingRateLimitRule, };
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,eAAO,MAAM,KAAK,EAAE,IAAI,EAWvB,CAAC;AAEF,OAAO,EACL,cAAc,EACd,eAAe,EACf,4BAA4B,EAC5B,sBAAsB,EACtB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,0BAA0B,EAC1B,oBAAoB,GACrB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { noLlmCacheRule } from "./noLlmCache.js";
|
|
2
|
+
import { hugeContextRule } from "./hugeContext.js";
|
|
3
|
+
import { expensiveModelSimpleTaskRule } from "./expensiveModelSimpleTask.js";
|
|
4
|
+
import { repeatedEmbeddingsRule } from "./repeatedEmbeddings.js";
|
|
5
|
+
import { imageGenerationLoopRule } from "./imageGenerationLoop.js";
|
|
6
|
+
import { frequentCronRule } from "./frequentCron.js";
|
|
7
|
+
import { noTokenLimitRule } from "./noTokenLimit.js";
|
|
8
|
+
import { sequentialLlmCallsRule } from "./sequentialLlmCalls.js";
|
|
9
|
+
import { agentLoopWithoutBudgetRule } from "./agentLoopWithoutBudget.js";
|
|
10
|
+
import { missingRateLimitRule } from "./missingRateLimit.js";
|
|
11
|
+
export const rules = [
|
|
12
|
+
noLlmCacheRule,
|
|
13
|
+
hugeContextRule,
|
|
14
|
+
expensiveModelSimpleTaskRule,
|
|
15
|
+
repeatedEmbeddingsRule,
|
|
16
|
+
imageGenerationLoopRule,
|
|
17
|
+
frequentCronRule,
|
|
18
|
+
noTokenLimitRule,
|
|
19
|
+
sequentialLlmCallsRule,
|
|
20
|
+
agentLoopWithoutBudgetRule,
|
|
21
|
+
missingRateLimitRule,
|
|
22
|
+
];
|
|
23
|
+
export { noLlmCacheRule, hugeContextRule, expensiveModelSimpleTaskRule, repeatedEmbeddingsRule, imageGenerationLoopRule, frequentCronRule, noTokenLimitRule, sequentialLlmCallsRule, agentLoopWithoutBudgetRule, missingRateLimitRule, };
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,CAAC,MAAM,KAAK,GAAW;IAC3B,cAAc;IACd,eAAe;IACf,4BAA4B;IAC5B,sBAAsB;IACtB,uBAAuB;IACvB,gBAAgB;IAChB,gBAAgB;IAChB,sBAAsB;IACtB,0BAA0B;IAC1B,oBAAoB;CACrB,CAAC;AAEF,OAAO,EACL,cAAc,EACd,eAAe,EACf,4BAA4B,EAC5B,sBAAsB,EACtB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,0BAA0B,EAC1B,oBAAoB,GACrB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missingRateLimit.d.ts","sourceRoot":"","sources":["../../src/rules/missingRateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAc,MAAM,aAAa,CAAC;AA6C7D,eAAO,MAAM,oBAAoB,EAAE,IA6ClC,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { makeImpact } from "../impact.js";
|
|
2
|
+
import { LLM_CALL_PATTERNS, createFinding, findCodeMatchesInFile, } from "./helpers.js";
|
|
3
|
+
/** Patterns suggesting this file is a public request handler / API route. */
|
|
4
|
+
const ROUTE_INDICATORS = [
|
|
5
|
+
/export\s+async\s+function\s+(POST|GET|PUT|PATCH|DELETE)\b/,
|
|
6
|
+
/export\s+function\s+(POST|GET|PUT|PATCH|DELETE)\b/,
|
|
7
|
+
/NextResponse/,
|
|
8
|
+
/Response\.json/,
|
|
9
|
+
];
|
|
10
|
+
/** File-path hints for framework route conventions. */
|
|
11
|
+
const ROUTE_PATH_HINTS = ["app/api", "pages/api"];
|
|
12
|
+
/**
|
|
13
|
+
* Terms that suggest a rate limit / quota / abuse control is already present.
|
|
14
|
+
* Broad on purpose — a false negative (staying quiet) is safer than noise here.
|
|
15
|
+
*/
|
|
16
|
+
const RATE_LIMIT_TERMS = [
|
|
17
|
+
"rateLimit",
|
|
18
|
+
"ratelimit",
|
|
19
|
+
"rateLimiter",
|
|
20
|
+
"quota",
|
|
21
|
+
"throttle",
|
|
22
|
+
"upstash",
|
|
23
|
+
"redis",
|
|
24
|
+
];
|
|
25
|
+
const FIX_RECIPE = [
|
|
26
|
+
"Identify the caller using user ID, API key, or IP.",
|
|
27
|
+
"Check a rate limit before calling the model.",
|
|
28
|
+
"Return a 429 response when the limit is exceeded.",
|
|
29
|
+
"Log excessive usage for review.",
|
|
30
|
+
];
|
|
31
|
+
function looksLikeRoute(file) {
|
|
32
|
+
if (ROUTE_PATH_HINTS.some((hint) => file.path.includes(hint)))
|
|
33
|
+
return true;
|
|
34
|
+
return findCodeMatchesInFile(file, ROUTE_INDICATORS).length > 0;
|
|
35
|
+
}
|
|
36
|
+
export const missingRateLimitRule = {
|
|
37
|
+
id: "missing-rate-limit",
|
|
38
|
+
title: "Public AI route without obvious rate limit",
|
|
39
|
+
severity: "medium",
|
|
40
|
+
wasteCategory: "repeated-inference",
|
|
41
|
+
description: "Flags files that look like public API routes and call a model without a nearby rate limit or quota check.",
|
|
42
|
+
recommendation: "Add per-user/IP rate limits, quotas, or abuse controls around public AI endpoints.",
|
|
43
|
+
fixRecipe: FIX_RECIPE,
|
|
44
|
+
scan(file) {
|
|
45
|
+
if (!looksLikeRoute(file))
|
|
46
|
+
return [];
|
|
47
|
+
const llmCalls = findCodeMatchesInFile(file, LLM_CALL_PATTERNS);
|
|
48
|
+
if (llmCalls.length === 0)
|
|
49
|
+
return [];
|
|
50
|
+
// If any rate-limit/quota term appears in the file, assume it's covered.
|
|
51
|
+
if (findCodeMatchesInFile(file, RATE_LIMIT_TERMS).length > 0)
|
|
52
|
+
return [];
|
|
53
|
+
// Report once per file, anchored on the first model call.
|
|
54
|
+
return [
|
|
55
|
+
createFinding({
|
|
56
|
+
ruleId: this.id,
|
|
57
|
+
title: this.title,
|
|
58
|
+
severity: this.severity,
|
|
59
|
+
wasteCategory: this.wasteCategory,
|
|
60
|
+
file,
|
|
61
|
+
index: llmCalls[0].index,
|
|
62
|
+
message: "This public AI route calls a model without an obvious rate limit or quota check.",
|
|
63
|
+
recommendation: this.recommendation,
|
|
64
|
+
fixRecipe: this.fixRecipe,
|
|
65
|
+
impact: makeImpact({
|
|
66
|
+
computeWaste: "medium",
|
|
67
|
+
carbonImpact: "low",
|
|
68
|
+
waterImpact: "low",
|
|
69
|
+
costImpact: "high",
|
|
70
|
+
confidence: "low",
|
|
71
|
+
score: 68,
|
|
72
|
+
explanation: "Public AI endpoints without rate limits can be abused into large volumes of avoidable model calls.",
|
|
73
|
+
}),
|
|
74
|
+
}),
|
|
75
|
+
];
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=missingRateLimit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missingRateLimit.js","sourceRoot":"","sources":["../../src/rules/missingRateLimit.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,qBAAqB,GACtB,MAAM,cAAc,CAAC;AAEtB,6EAA6E;AAC7E,MAAM,gBAAgB,GAA2B;IAC/C,2DAA2D;IAC3D,mDAAmD;IACnD,cAAc;IACd,gBAAgB;CACjB,CAAC;AAEF,uDAAuD;AACvD,MAAM,gBAAgB,GAAG,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAElD;;;GAGG;AACH,MAAM,gBAAgB,GAA2B;IAC/C,WAAW;IACX,WAAW;IACX,aAAa;IACb,OAAO;IACP,UAAU;IACV,SAAS;IACT,OAAO;CACR,CAAC;AAEF,MAAM,UAAU,GAAG;IACjB,oDAAoD;IACpD,8CAA8C;IAC9C,mDAAmD;IACnD,iCAAiC;CAClC,CAAC;AAEF,SAAS,cAAc,CAAC,IAAgB;IACtC,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3E,OAAO,qBAAqB,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAS;IACxC,EAAE,EAAE,oBAAoB;IACxB,KAAK,EAAE,4CAA4C;IACnD,QAAQ,EAAE,QAAQ;IAClB,aAAa,EAAE,oBAAoB;IACnC,WAAW,EACT,2GAA2G;IAC7G,cAAc,EACZ,oFAAoF;IACtF,SAAS,EAAE,UAAU;IACrB,IAAI,CAAC,IAAgB;QACnB,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,CAAC;QAErC,MAAM,QAAQ,GAAG,qBAAqB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;QAChE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAErC,yEAAyE;QACzE,IAAI,qBAAqB,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,EAAE,CAAC;QAExE,0DAA0D;QAC1D,OAAO;YACL,aAAa,CAAC;gBACZ,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,aAAa,EAAE,IAAI,CAAC,aAAa;gBACjC,IAAI;gBACJ,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAE,CAAC,KAAK;gBACzB,OAAO,EACL,kFAAkF;gBACpF,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,UAAU,CAAC;oBACjB,YAAY,EAAE,QAAQ;oBACtB,YAAY,EAAE,KAAK;oBACnB,WAAW,EAAE,KAAK;oBAClB,UAAU,EAAE,MAAM;oBAClB,UAAU,EAAE,KAAK;oBACjB,KAAK,EAAE,EAAE;oBACT,WAAW,EACT,oGAAoG;iBACvG,CAAC;aACH,CAAC;SACH,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noLlmCache.d.ts","sourceRoot":"","sources":["../../src/rules/noLlmCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAc,MAAM,aAAa,CAAC;AA2C7D,eAAO,MAAM,cAAc,EAAE,IA+C5B,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { makeImpact } from "../impact.js";
|
|
2
|
+
import { LLM_CALL_PATTERNS, createFinding, dedupeFindings, findCodeMatchesInFile, hasNearby, hasNearbyCode, isTestFile, } from "./helpers.js";
|
|
3
|
+
const CACHE_TERMS = [
|
|
4
|
+
"cache",
|
|
5
|
+
"cached",
|
|
6
|
+
"redis",
|
|
7
|
+
"kv",
|
|
8
|
+
"upstash",
|
|
9
|
+
"unstable_cache",
|
|
10
|
+
"memoize",
|
|
11
|
+
"dedupe",
|
|
12
|
+
"lru",
|
|
13
|
+
"Map(",
|
|
14
|
+
"getCached",
|
|
15
|
+
"setCached",
|
|
16
|
+
];
|
|
17
|
+
const FIX_RECIPE = [
|
|
18
|
+
"Create a cache key from model, system prompt, user prompt, and relevant parameters.",
|
|
19
|
+
"Check Redis, KV, or an in-memory cache before calling the provider.",
|
|
20
|
+
"Store successful deterministic responses with a sensible TTL.",
|
|
21
|
+
"Skip caching for highly personalized, sensitive, or rapidly changing outputs.",
|
|
22
|
+
];
|
|
23
|
+
const REUSE_SIGNALS = [
|
|
24
|
+
/classif/i,
|
|
25
|
+
/extract/i,
|
|
26
|
+
/categor/i,
|
|
27
|
+
/tag/i,
|
|
28
|
+
/template/i,
|
|
29
|
+
/deterministic/i,
|
|
30
|
+
];
|
|
31
|
+
export const noLlmCacheRule = {
|
|
32
|
+
id: "no-llm-cache",
|
|
33
|
+
title: "LLM call without obvious caching",
|
|
34
|
+
severity: "medium",
|
|
35
|
+
wasteCategory: "repeated-inference",
|
|
36
|
+
description: "Flags likely LLM calls that have no nearby caching, deduping, or memoization terms.",
|
|
37
|
+
recommendation: "Add request-level or semantic caching for repeated prompts, deterministic tasks, and expensive generations.",
|
|
38
|
+
fixRecipe: FIX_RECIPE,
|
|
39
|
+
scan(file) {
|
|
40
|
+
const findings = [];
|
|
41
|
+
const testFile = isTestFile(file.path);
|
|
42
|
+
for (const match of findCodeMatchesInFile(file, LLM_CALL_PATTERNS)) {
|
|
43
|
+
if (hasNearbyCode(file, match.index, CACHE_TERMS, 20))
|
|
44
|
+
continue;
|
|
45
|
+
const likelyReusable = hasNearby(file, match.index, REUSE_SIGNALS, 12);
|
|
46
|
+
findings.push(createFinding({
|
|
47
|
+
ruleId: this.id,
|
|
48
|
+
title: this.title,
|
|
49
|
+
// Downgrade noisy hits in test files instead of over-reporting.
|
|
50
|
+
severity: testFile ? "low" : likelyReusable ? "high" : "medium",
|
|
51
|
+
wasteCategory: this.wasteCategory,
|
|
52
|
+
file,
|
|
53
|
+
index: match.index,
|
|
54
|
+
message: "This LLM call does not appear to use caching. Repeated prompts may trigger avoidable model inference.",
|
|
55
|
+
recommendation: this.recommendation,
|
|
56
|
+
fixRecipe: this.fixRecipe,
|
|
57
|
+
impact: makeImpact({
|
|
58
|
+
computeWaste: "high",
|
|
59
|
+
carbonImpact: "medium",
|
|
60
|
+
waterImpact: "medium",
|
|
61
|
+
costImpact: "high",
|
|
62
|
+
confidence: testFile || !likelyReusable ? "low" : "medium",
|
|
63
|
+
score: testFile ? 30 : likelyReusable ? 80 : 55,
|
|
64
|
+
explanation: "Repeated uncached model calls can create avoidable token processing and infrastructure demand.",
|
|
65
|
+
}),
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
return dedupeFindings(findings);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=noLlmCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noLlmCache.js","sourceRoot":"","sources":["../../src/rules/noLlmCache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,SAAS,EACT,aAAa,EACb,UAAU,GACX,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,GAA2B;IAC1C,OAAO;IACP,QAAQ;IACR,OAAO;IACP,IAAI;IACJ,SAAS;IACT,gBAAgB;IAChB,SAAS;IACT,QAAQ;IACR,KAAK;IACL,MAAM;IACN,WAAW;IACX,WAAW;CACZ,CAAC;AAEF,MAAM,UAAU,GAAG;IACjB,qFAAqF;IACrF,qEAAqE;IACrE,+DAA+D;IAC/D,+EAA+E;CAChF,CAAC;AAEF,MAAM,aAAa,GAA2B;IAC5C,UAAU;IACV,UAAU;IACV,UAAU;IACV,MAAM;IACN,WAAW;IACX,gBAAgB;CACjB,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAS;IAClC,EAAE,EAAE,cAAc;IAClB,KAAK,EAAE,kCAAkC;IACzC,QAAQ,EAAE,QAAQ;IAClB,aAAa,EAAE,oBAAoB;IACnC,WAAW,EACT,qFAAqF;IACvF,cAAc,EACZ,6GAA6G;IAC/G,SAAS,EAAE,UAAU;IACrB,IAAI,CAAC,IAAgB;QACnB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEvC,KAAK,MAAM,KAAK,IAAI,qBAAqB,CAAC,IAAI,EAAE,iBAAiB,CAAC,EAAE,CAAC;YACnE,IAAI,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,CAAC;gBAAE,SAAS;YAChE,MAAM,cAAc,GAAG,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC;YAEvE,QAAQ,CAAC,IAAI,CACX,aAAa,CAAC;gBACZ,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,gEAAgE;gBAChE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;gBAC/D,aAAa,EAAE,IAAI,CAAC,aAAa;gBACjC,IAAI;gBACJ,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,OAAO,EACL,uGAAuG;gBACzG,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,UAAU,CAAC;oBACjB,YAAY,EAAE,MAAM;oBACpB,YAAY,EAAE,QAAQ;oBACtB,WAAW,EAAE,QAAQ;oBACrB,UAAU,EAAE,MAAM;oBAClB,UAAU,EAAE,QAAQ,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ;oBAC1D,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;oBAC/C,WAAW,EACT,gGAAgG;iBACnG,CAAC;aACH,CAAC,CACH,CAAC;QACJ,CAAC;QAED,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;IAClC,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noTokenLimit.d.ts","sourceRoot":"","sources":["../../src/rules/noTokenLimit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAc,MAAM,aAAa,CAAC;AAyB7D,eAAO,MAAM,gBAAgB,EAAE,IAkD9B,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { makeImpact } from "../impact.js";
|
|
2
|
+
import { LLM_CALL_PATTERNS, createFinding, dedupeFindings, findCodeMatchesInFile, hasNearbyCode, } from "./helpers.js";
|
|
3
|
+
const TOKEN_LIMIT_TERMS = [
|
|
4
|
+
"max_tokens",
|
|
5
|
+
"maxTokens",
|
|
6
|
+
"max_output_tokens",
|
|
7
|
+
"maxOutputTokens",
|
|
8
|
+
"max_completion_tokens",
|
|
9
|
+
];
|
|
10
|
+
const FIX_RECIPE = [
|
|
11
|
+
"Add an explicit output token limit.",
|
|
12
|
+
"Pick the limit based on the task type.",
|
|
13
|
+
"Use stricter limits for classification and extraction.",
|
|
14
|
+
"Add tests for runaway or unexpectedly long outputs.",
|
|
15
|
+
];
|
|
16
|
+
export const noTokenLimitRule = {
|
|
17
|
+
id: "no-token-limit",
|
|
18
|
+
title: "No obvious output token limit",
|
|
19
|
+
severity: "low",
|
|
20
|
+
wasteCategory: "unbounded-generation",
|
|
21
|
+
description: "Flags LLM calls that have no explicit output token limit set nearby.",
|
|
22
|
+
recommendation: "Set an output token cap appropriate for the task to reduce runaway generations.",
|
|
23
|
+
fixRecipe: FIX_RECIPE,
|
|
24
|
+
scan(file) {
|
|
25
|
+
const findings = [];
|
|
26
|
+
const llmCalls = findCodeMatchesInFile(file, LLM_CALL_PATTERNS);
|
|
27
|
+
// Only flag when the file clearly contains at least one LLM call.
|
|
28
|
+
if (llmCalls.length === 0)
|
|
29
|
+
return findings;
|
|
30
|
+
for (const match of llmCalls) {
|
|
31
|
+
// A token limit anywhere within ~25 lines counts as covered.
|
|
32
|
+
if (hasNearbyCode(file, match.index, TOKEN_LIMIT_TERMS, 25))
|
|
33
|
+
continue;
|
|
34
|
+
findings.push(createFinding({
|
|
35
|
+
ruleId: this.id,
|
|
36
|
+
title: this.title,
|
|
37
|
+
severity: this.severity,
|
|
38
|
+
wasteCategory: this.wasteCategory,
|
|
39
|
+
file,
|
|
40
|
+
index: match.index,
|
|
41
|
+
message: "No explicit output token limit was detected near this model call.",
|
|
42
|
+
recommendation: this.recommendation,
|
|
43
|
+
fixRecipe: this.fixRecipe,
|
|
44
|
+
impact: makeImpact({
|
|
45
|
+
computeWaste: "medium",
|
|
46
|
+
carbonImpact: "low",
|
|
47
|
+
waterImpact: "low",
|
|
48
|
+
costImpact: "medium",
|
|
49
|
+
confidence: "medium",
|
|
50
|
+
score: 45,
|
|
51
|
+
explanation: "Output limits help prevent unnecessarily long generations and unexpected token usage.",
|
|
52
|
+
}),
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
// Keep this rule quiet: at most one finding per file.
|
|
56
|
+
return dedupeFindings(findings).slice(0, 1);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=noTokenLimit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noTokenLimit.js","sourceRoot":"","sources":["../../src/rules/noTokenLimit.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,aAAa,GACd,MAAM,cAAc,CAAC;AAEtB,MAAM,iBAAiB,GAA2B;IAChD,YAAY;IACZ,WAAW;IACX,mBAAmB;IACnB,iBAAiB;IACjB,uBAAuB;CACxB,CAAC;AAEF,MAAM,UAAU,GAAG;IACjB,qCAAqC;IACrC,wCAAwC;IACxC,wDAAwD;IACxD,qDAAqD;CACtD,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAS;IACpC,EAAE,EAAE,gBAAgB;IACpB,KAAK,EAAE,+BAA+B;IACtC,QAAQ,EAAE,KAAK;IACf,aAAa,EAAE,sBAAsB;IACrC,WAAW,EACT,sEAAsE;IACxE,cAAc,EACZ,iFAAiF;IACnF,SAAS,EAAE,UAAU;IACrB,IAAI,CAAC,IAAgB;QACnB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,qBAAqB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;QAEhE,kEAAkE;QAClE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,QAAQ,CAAC;QAE3C,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,6DAA6D;YAC7D,IAAI,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,iBAAiB,EAAE,EAAE,CAAC;gBAAE,SAAS;YAEtE,QAAQ,CAAC,IAAI,CACX,aAAa,CAAC;gBACZ,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,aAAa,EAAE,IAAI,CAAC,aAAa;gBACjC,IAAI;gBACJ,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,OAAO,EACL,mEAAmE;gBACrE,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,UAAU,CAAC;oBACjB,YAAY,EAAE,QAAQ;oBACtB,YAAY,EAAE,KAAK;oBACnB,WAAW,EAAE,KAAK;oBAClB,UAAU,EAAE,QAAQ;oBACpB,UAAU,EAAE,QAAQ;oBACpB,KAAK,EAAE,EAAE;oBACT,WAAW,EACT,uFAAuF;iBAC1F,CAAC;aACH,CAAC,CACH,CAAC;QACJ,CAAC;QAED,sDAAsD;QACtD,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9C,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repeatedEmbeddings.d.ts","sourceRoot":"","sources":["../../src/rules/repeatedEmbeddings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAc,MAAM,aAAa,CAAC;AAiD7D,eAAO,MAAM,sBAAsB,EAAE,IAkDpC,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { makeImpact } from "../impact.js";
|
|
2
|
+
import { createFinding, dedupeFindings, findCodeMatchesInFile, hasNearbyCode, } from "./helpers.js";
|
|
3
|
+
const EMBEDDING_PATTERNS = [
|
|
4
|
+
/embeddings\.create/i,
|
|
5
|
+
/createEmbedding/i,
|
|
6
|
+
/\bembedMany\s*\(/i,
|
|
7
|
+
/\bembed\s*\(/i,
|
|
8
|
+
];
|
|
9
|
+
const LOOP_TERMS = [
|
|
10
|
+
/for\s*\(/i,
|
|
11
|
+
/for\s+await/i,
|
|
12
|
+
/while\s*\(/i,
|
|
13
|
+
/\.map\s*\(/i,
|
|
14
|
+
/forEach\s*\(/i,
|
|
15
|
+
];
|
|
16
|
+
const PERSISTENCE_TERMS = [
|
|
17
|
+
"vectorStore",
|
|
18
|
+
"pinecone",
|
|
19
|
+
"weaviate",
|
|
20
|
+
"qdrant",
|
|
21
|
+
"chroma",
|
|
22
|
+
"supabase",
|
|
23
|
+
"pgvector",
|
|
24
|
+
"db.",
|
|
25
|
+
"database",
|
|
26
|
+
"insert",
|
|
27
|
+
"upsert",
|
|
28
|
+
"cache",
|
|
29
|
+
"exists",
|
|
30
|
+
"findUnique",
|
|
31
|
+
"findFirst",
|
|
32
|
+
];
|
|
33
|
+
const FIX_RECIPE = [
|
|
34
|
+
"Hash the source text or document ID.",
|
|
35
|
+
"Check whether an embedding already exists before generating a new one.",
|
|
36
|
+
"Persist embeddings in a vector store or database.",
|
|
37
|
+
"Re-embed only when the source content changes.",
|
|
38
|
+
];
|
|
39
|
+
export const repeatedEmbeddingsRule = {
|
|
40
|
+
id: "repeated-embeddings",
|
|
41
|
+
title: "Embeddings may be regenerated repeatedly",
|
|
42
|
+
severity: "high",
|
|
43
|
+
wasteCategory: "redundant-embedding",
|
|
44
|
+
description: "Flags embedding calls near loops that have no obvious persistence, reuse, or existence check.",
|
|
45
|
+
recommendation: "Persist embeddings for unchanged text and check for existing vectors before generating new ones.",
|
|
46
|
+
fixRecipe: FIX_RECIPE,
|
|
47
|
+
scan(file) {
|
|
48
|
+
const findings = [];
|
|
49
|
+
for (const match of findCodeMatchesInFile(file, EMBEDDING_PATTERNS)) {
|
|
50
|
+
const inLoop = hasNearbyCode(file, match.index, LOOP_TERMS, 20);
|
|
51
|
+
const persisted = hasNearbyCode(file, match.index, PERSISTENCE_TERMS, 20);
|
|
52
|
+
// If embeddings are persisted / reused nearby, there's no waste to flag.
|
|
53
|
+
if (persisted)
|
|
54
|
+
continue;
|
|
55
|
+
// Highest concern is embeddings inside a loop; a lone uncached call is a
|
|
56
|
+
// lower-confidence nudge.
|
|
57
|
+
findings.push(createFinding({
|
|
58
|
+
ruleId: this.id,
|
|
59
|
+
title: this.title,
|
|
60
|
+
severity: this.severity,
|
|
61
|
+
wasteCategory: this.wasteCategory,
|
|
62
|
+
file,
|
|
63
|
+
index: match.index,
|
|
64
|
+
message: "Embeddings appear to be generated repeatedly without obvious persistence or reuse.",
|
|
65
|
+
recommendation: this.recommendation,
|
|
66
|
+
fixRecipe: this.fixRecipe,
|
|
67
|
+
impact: makeImpact({
|
|
68
|
+
computeWaste: "high",
|
|
69
|
+
carbonImpact: "medium",
|
|
70
|
+
waterImpact: "medium",
|
|
71
|
+
costImpact: "medium",
|
|
72
|
+
confidence: inLoop ? "medium" : "low",
|
|
73
|
+
score: inLoop ? 80 : 60,
|
|
74
|
+
explanation: "Re-embedding unchanged content creates avoidable repeated compute.",
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
return dedupeFindings(findings);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=repeatedEmbeddings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repeatedEmbeddings.js","sourceRoot":"","sources":["../../src/rules/repeatedEmbeddings.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,aAAa,GACd,MAAM,cAAc,CAAC;AAEtB,MAAM,kBAAkB,GAA2B;IACjD,qBAAqB;IACrB,kBAAkB;IAClB,mBAAmB;IACnB,eAAe;CAChB,CAAC;AAEF,MAAM,UAAU,GAA2B;IACzC,WAAW;IACX,cAAc;IACd,aAAa;IACb,aAAa;IACb,eAAe;CAChB,CAAC;AAEF,MAAM,iBAAiB,GAA2B;IAChD,aAAa;IACb,UAAU;IACV,UAAU;IACV,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,UAAU;IACV,KAAK;IACL,UAAU;IACV,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,YAAY;IACZ,WAAW;CACZ,CAAC;AAEF,MAAM,UAAU,GAAG;IACjB,sCAAsC;IACtC,wEAAwE;IACxE,mDAAmD;IACnD,gDAAgD;CACjD,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAS;IAC1C,EAAE,EAAE,qBAAqB;IACzB,KAAK,EAAE,0CAA0C;IACjD,QAAQ,EAAE,MAAM;IAChB,aAAa,EAAE,qBAAqB;IACpC,WAAW,EACT,+FAA+F;IACjG,cAAc,EACZ,kGAAkG;IACpG,SAAS,EAAE,UAAU;IACrB,IAAI,CAAC,IAAgB;QACnB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,KAAK,IAAI,qBAAqB,CAAC,IAAI,EAAE,kBAAkB,CAAC,EAAE,CAAC;YACpE,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;YAChE,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,iBAAiB,EAAE,EAAE,CAAC,CAAC;YAE1E,yEAAyE;YACzE,IAAI,SAAS;gBAAE,SAAS;YAExB,yEAAyE;YACzE,0BAA0B;YAC1B,QAAQ,CAAC,IAAI,CACX,aAAa,CAAC;gBACZ,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,aAAa,EAAE,IAAI,CAAC,aAAa;gBACjC,IAAI;gBACJ,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,OAAO,EACL,oFAAoF;gBACtF,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,UAAU,CAAC;oBACjB,YAAY,EAAE,MAAM;oBACpB,YAAY,EAAE,QAAQ;oBACtB,WAAW,EAAE,QAAQ;oBACrB,UAAU,EAAE,QAAQ;oBACpB,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK;oBACrC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;oBACvB,WAAW,EACT,oEAAoE;iBACvE,CAAC;aACH,CAAC,CACH,CAAC;QACJ,CAAC;QAED,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;IAClC,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequentialLlmCalls.d.ts","sourceRoot":"","sources":["../../src/rules/sequentialLlmCalls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAc,MAAM,aAAa,CAAC;AAiB7D,eAAO,MAAM,sBAAsB,EAAE,IAuCpC,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { makeImpact } from "../impact.js";
|
|
2
|
+
import { LLM_CALL_PATTERNS, createFinding, findCodeMatchesInFile, } from "./helpers.js";
|
|
3
|
+
const WINDOW_LINES = 80;
|
|
4
|
+
const FIX_RECIPE = [
|
|
5
|
+
"Check whether the calls can be combined into one prompt.",
|
|
6
|
+
"Cache intermediate outputs.",
|
|
7
|
+
"Parallelize independent calls.",
|
|
8
|
+
"Replace simple follow-up calls with rules or smaller models.",
|
|
9
|
+
];
|
|
10
|
+
export const sequentialLlmCallsRule = {
|
|
11
|
+
id: "sequential-llm-calls",
|
|
12
|
+
title: "Nearby LLM calls may multiply work",
|
|
13
|
+
severity: "medium",
|
|
14
|
+
wasteCategory: "repeated-inference",
|
|
15
|
+
description: "Flags files where two or more likely LLM calls appear within a close range of each other.",
|
|
16
|
+
recommendation: "Consider combining prompts, batching, parallelizing independent calls, or caching intermediate results.",
|
|
17
|
+
fixRecipe: FIX_RECIPE,
|
|
18
|
+
scan(file) {
|
|
19
|
+
const calls = findCodeMatchesInFile(file, LLM_CALL_PATTERNS);
|
|
20
|
+
if (calls.length < 2)
|
|
21
|
+
return [];
|
|
22
|
+
// Report once per cluster of >= 2 calls within WINDOW_LINES, anchored on
|
|
23
|
+
// the second call in the cluster.
|
|
24
|
+
const findings = [];
|
|
25
|
+
let clusterStartLine = calls[0].line;
|
|
26
|
+
let clusterCount = 1;
|
|
27
|
+
let reported = false;
|
|
28
|
+
for (let i = 1; i < calls.length; i++) {
|
|
29
|
+
const call = calls[i];
|
|
30
|
+
if (call.line - clusterStartLine <= WINDOW_LINES) {
|
|
31
|
+
clusterCount++;
|
|
32
|
+
if (clusterCount === 2 && !reported) {
|
|
33
|
+
findings.push(buildFinding(file, call.index));
|
|
34
|
+
reported = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Start a new cluster.
|
|
39
|
+
clusterStartLine = call.line;
|
|
40
|
+
clusterCount = 1;
|
|
41
|
+
reported = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return findings;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
function buildFinding(file, index) {
|
|
48
|
+
return createFinding({
|
|
49
|
+
ruleId: sequentialLlmCallsRule.id,
|
|
50
|
+
title: sequentialLlmCallsRule.title,
|
|
51
|
+
severity: sequentialLlmCallsRule.severity,
|
|
52
|
+
wasteCategory: sequentialLlmCallsRule.wasteCategory,
|
|
53
|
+
file,
|
|
54
|
+
index,
|
|
55
|
+
message: "Multiple LLM calls appear close together. Verify whether they run in the same execution flow.",
|
|
56
|
+
recommendation: sequentialLlmCallsRule.recommendation,
|
|
57
|
+
fixRecipe: sequentialLlmCallsRule.fixRecipe,
|
|
58
|
+
impact: makeImpact({
|
|
59
|
+
computeWaste: "medium",
|
|
60
|
+
carbonImpact: "medium",
|
|
61
|
+
waterImpact: "medium",
|
|
62
|
+
costImpact: "high",
|
|
63
|
+
confidence: "low",
|
|
64
|
+
score: 70,
|
|
65
|
+
explanation: "Multi-call chains can multiply token usage and latency if not carefully designed.",
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=sequentialLlmCalls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sequentialLlmCalls.js","sourceRoot":"","sources":["../../src/rules/sequentialLlmCalls.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,qBAAqB,GACtB,MAAM,cAAc,CAAC;AAEtB,MAAM,YAAY,GAAG,EAAE,CAAC;AAExB,MAAM,UAAU,GAAG;IACjB,0DAA0D;IAC1D,6BAA6B;IAC7B,gCAAgC;IAChC,8DAA8D;CAC/D,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAS;IAC1C,EAAE,EAAE,sBAAsB;IAC1B,KAAK,EAAE,oCAAoC;IAC3C,QAAQ,EAAE,QAAQ;IAClB,aAAa,EAAE,oBAAoB;IACnC,WAAW,EACT,2FAA2F;IAC7F,cAAc,EACZ,yGAAyG;IAC3G,SAAS,EAAE,UAAU;IACrB,IAAI,CAAC,IAAgB;QACnB,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;QAC7D,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,EAAE,CAAC;QAEhC,yEAAyE;QACzE,kCAAkC;QAClC,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,IAAI,gBAAgB,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC;QACtC,IAAI,YAAY,GAAG,CAAC,CAAC;QACrB,IAAI,QAAQ,GAAG,KAAK,CAAC;QAErB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;YACvB,IAAI,IAAI,CAAC,IAAI,GAAG,gBAAgB,IAAI,YAAY,EAAE,CAAC;gBACjD,YAAY,EAAE,CAAC;gBACf,IAAI,YAAY,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACpC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;oBAC9C,QAAQ,GAAG,IAAI,CAAC;gBAClB,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,uBAAuB;gBACvB,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC;gBAC7B,YAAY,GAAG,CAAC,CAAC;gBACjB,QAAQ,GAAG,KAAK,CAAC;YACnB,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC;AAEF,SAAS,YAAY,CAAC,IAAgB,EAAE,KAAa;IACnD,OAAO,aAAa,CAAC;QACnB,MAAM,EAAE,sBAAsB,CAAC,EAAE;QACjC,KAAK,EAAE,sBAAsB,CAAC,KAAK;QACnC,QAAQ,EAAE,sBAAsB,CAAC,QAAQ;QACzC,aAAa,EAAE,sBAAsB,CAAC,aAAa;QACnD,IAAI;QACJ,KAAK;QACL,OAAO,EACL,+FAA+F;QACjG,cAAc,EAAE,sBAAsB,CAAC,cAAc;QACrD,SAAS,EAAE,sBAAsB,CAAC,SAAS;QAC3C,MAAM,EAAE,UAAU,CAAC;YACjB,YAAY,EAAE,QAAQ;YACtB,YAAY,EAAE,QAAQ;YACtB,WAAW,EAAE,QAAQ;YACrB,UAAU,EAAE,MAAM;YAClB,UAAU,EAAE,KAAK;YACjB,KAAK,EAAE,EAAE;YACT,WAAW,EACT,mFAAmF;SACtF,CAAC;KACH,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Provider } from "./models.js";
|
|
2
|
+
import type { Finding, RuleContext, ScanResult, Severity, SourceFile } from "./types.js";
|
|
3
|
+
export type ScannerOptions = {
|
|
4
|
+
path: string;
|
|
5
|
+
minSeverity?: Severity;
|
|
6
|
+
ignoredRules?: string[];
|
|
7
|
+
ignoredPaths?: string[];
|
|
8
|
+
provider?: Provider;
|
|
9
|
+
/** Stable finding fingerprints to suppress as accepted baseline debt. */
|
|
10
|
+
baselineKeys?: Iterable<string>;
|
|
11
|
+
/** Optional exact relative paths to scan, used by changed-files mode. */
|
|
12
|
+
includedPaths?: Iterable<string>;
|
|
13
|
+
};
|
|
14
|
+
/** Discover scannable files under a directory (or a single file). */
|
|
15
|
+
export declare function discoverFiles(rootPath: string): Promise<string[]>;
|
|
16
|
+
/** Run all rules against a single in-memory source file. */
|
|
17
|
+
export declare function runRules(file: SourceFile, context?: RuleContext, ignoredRules?: Set<string>, onRuleError?: (ruleId: string, error: unknown) => void): Finding[];
|
|
18
|
+
export declare function sortFindings(findings: Finding[]): Finding[];
|
|
19
|
+
/** Scan a path and return sorted findings plus a summary. */
|
|
20
|
+
export declare function scan(options: ScannerOptions): Promise<ScanResult>;
|
|
21
|
+
/** Stable-enough key for accepting existing findings in a baseline file. */
|
|
22
|
+
export declare function findingFingerprint(finding: Finding): string;
|
|
23
|
+
//# sourceMappingURL=scanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,KAAK,EACV,OAAO,EACP,WAAW,EACX,UAAU,EAEV,QAAQ,EACR,UAAU,EAEX,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,yEAAyE;IACzE,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,yEAAyE;IACzE,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CAClC,CAAC;AASF,qEAAqE;AACrE,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAqBvE;AAwBD,4DAA4D;AAC5D,wBAAgB,QAAQ,CACtB,IAAI,EAAE,UAAU,EAChB,OAAO,CAAC,EAAE,WAAW,EACrB,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAC1B,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,GACrD,OAAO,EAAE,CAiBX;AAiBD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAS3D;AA6ED,6DAA6D;AAC7D,wBAAsB,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAkDvE;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAM3D"}
|