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.
Files changed (117) hide show
  1. package/BENCHMARK.md +39 -0
  2. package/CHANGELOG.md +41 -0
  3. package/CONTRIBUTING.md +43 -0
  4. package/DEMO.md +106 -0
  5. package/LICENSE +21 -0
  6. package/README.md +268 -0
  7. package/SECURITY.md +17 -0
  8. package/action.yml +182 -0
  9. package/dist/baseline.d.ts +13 -0
  10. package/dist/baseline.d.ts.map +1 -0
  11. package/dist/baseline.js +23 -0
  12. package/dist/baseline.js.map +1 -0
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +245 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/config.d.ts +31 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/config.js +127 -0
  20. package/dist/config.js.map +1 -0
  21. package/dist/ecoLLM.d.ts +33 -0
  22. package/dist/ecoLLM.d.ts.map +1 -0
  23. package/dist/ecoLLM.js +98 -0
  24. package/dist/ecoLLM.js.map +1 -0
  25. package/dist/impact.d.ts +22 -0
  26. package/dist/impact.d.ts.map +1 -0
  27. package/dist/impact.js +80 -0
  28. package/dist/impact.js.map +1 -0
  29. package/dist/index.d.ts +16 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +14 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/models.d.ts +21 -0
  34. package/dist/models.d.ts.map +1 -0
  35. package/dist/models.js +58 -0
  36. package/dist/models.js.map +1 -0
  37. package/dist/prComment.d.ts +36 -0
  38. package/dist/prComment.d.ts.map +1 -0
  39. package/dist/prComment.js +45 -0
  40. package/dist/prComment.js.map +1 -0
  41. package/dist/reporters/impactTracker.d.ts +21 -0
  42. package/dist/reporters/impactTracker.d.ts.map +1 -0
  43. package/dist/reporters/impactTracker.js +54 -0
  44. package/dist/reporters/impactTracker.js.map +1 -0
  45. package/dist/reporters/jsonReporter.d.ts +4 -0
  46. package/dist/reporters/jsonReporter.d.ts.map +1 -0
  47. package/dist/reporters/jsonReporter.js +34 -0
  48. package/dist/reporters/jsonReporter.js.map +1 -0
  49. package/dist/reporters/markdownReporter.d.ts +4 -0
  50. package/dist/reporters/markdownReporter.d.ts.map +1 -0
  51. package/dist/reporters/markdownReporter.js +97 -0
  52. package/dist/reporters/markdownReporter.js.map +1 -0
  53. package/dist/reporters/sarifReporter.d.ts +4 -0
  54. package/dist/reporters/sarifReporter.d.ts.map +1 -0
  55. package/dist/reporters/sarifReporter.js +63 -0
  56. package/dist/reporters/sarifReporter.js.map +1 -0
  57. package/dist/reporters/terminalReporter.d.ts +10 -0
  58. package/dist/reporters/terminalReporter.d.ts.map +1 -0
  59. package/dist/reporters/terminalReporter.js +80 -0
  60. package/dist/reporters/terminalReporter.js.map +1 -0
  61. package/dist/rules/agentLoopWithoutBudget.d.ts +3 -0
  62. package/dist/rules/agentLoopWithoutBudget.d.ts.map +1 -0
  63. package/dist/rules/agentLoopWithoutBudget.js +105 -0
  64. package/dist/rules/agentLoopWithoutBudget.js.map +1 -0
  65. package/dist/rules/expensiveModelSimpleTask.d.ts +3 -0
  66. package/dist/rules/expensiveModelSimpleTask.d.ts.map +1 -0
  67. package/dist/rules/expensiveModelSimpleTask.js +119 -0
  68. package/dist/rules/expensiveModelSimpleTask.js.map +1 -0
  69. package/dist/rules/frequentCron.d.ts +3 -0
  70. package/dist/rules/frequentCron.d.ts.map +1 -0
  71. package/dist/rules/frequentCron.js +64 -0
  72. package/dist/rules/frequentCron.js.map +1 -0
  73. package/dist/rules/helpers.d.ts +65 -0
  74. package/dist/rules/helpers.d.ts.map +1 -0
  75. package/dist/rules/helpers.js +383 -0
  76. package/dist/rules/helpers.js.map +1 -0
  77. package/dist/rules/hugeContext.d.ts +3 -0
  78. package/dist/rules/hugeContext.d.ts.map +1 -0
  79. package/dist/rules/hugeContext.js +74 -0
  80. package/dist/rules/hugeContext.js.map +1 -0
  81. package/dist/rules/imageGenerationLoop.d.ts +3 -0
  82. package/dist/rules/imageGenerationLoop.d.ts.map +1 -0
  83. package/dist/rules/imageGenerationLoop.js +72 -0
  84. package/dist/rules/imageGenerationLoop.js.map +1 -0
  85. package/dist/rules/index.d.ts +14 -0
  86. package/dist/rules/index.d.ts.map +1 -0
  87. package/dist/rules/index.js +24 -0
  88. package/dist/rules/index.js.map +1 -0
  89. package/dist/rules/missingRateLimit.d.ts +3 -0
  90. package/dist/rules/missingRateLimit.d.ts.map +1 -0
  91. package/dist/rules/missingRateLimit.js +78 -0
  92. package/dist/rules/missingRateLimit.js.map +1 -0
  93. package/dist/rules/noLlmCache.d.ts +3 -0
  94. package/dist/rules/noLlmCache.d.ts.map +1 -0
  95. package/dist/rules/noLlmCache.js +71 -0
  96. package/dist/rules/noLlmCache.js.map +1 -0
  97. package/dist/rules/noTokenLimit.d.ts +3 -0
  98. package/dist/rules/noTokenLimit.d.ts.map +1 -0
  99. package/dist/rules/noTokenLimit.js +59 -0
  100. package/dist/rules/noTokenLimit.js.map +1 -0
  101. package/dist/rules/repeatedEmbeddings.d.ts +3 -0
  102. package/dist/rules/repeatedEmbeddings.d.ts.map +1 -0
  103. package/dist/rules/repeatedEmbeddings.js +81 -0
  104. package/dist/rules/repeatedEmbeddings.js.map +1 -0
  105. package/dist/rules/sequentialLlmCalls.d.ts +3 -0
  106. package/dist/rules/sequentialLlmCalls.d.ts.map +1 -0
  107. package/dist/rules/sequentialLlmCalls.js +69 -0
  108. package/dist/rules/sequentialLlmCalls.js.map +1 -0
  109. package/dist/scanner.d.ts +23 -0
  110. package/dist/scanner.d.ts.map +1 -0
  111. package/dist/scanner.js +217 -0
  112. package/dist/scanner.js.map +1 -0
  113. package/dist/types.d.ts +93 -0
  114. package/dist/types.d.ts.map +1 -0
  115. package/dist/types.js +11 -0
  116. package/dist/types.js.map +1 -0
  117. package/package.json +75 -0
package/action.yml ADDED
@@ -0,0 +1,182 @@
1
+ name: "Trimference"
2
+ description: "Review LLM application code for missing efficiency safeguards."
3
+ author: "Trimference contributors"
4
+ branding:
5
+ icon: "wind"
6
+ color: "green"
7
+
8
+ inputs:
9
+ path:
10
+ description: "Path to scan"
11
+ required: false
12
+ default: "."
13
+ min-severity:
14
+ description: "Minimum severity to report (low | medium | high)"
15
+ required: false
16
+ default: "low"
17
+ format:
18
+ description: "Report format (markdown | json)"
19
+ required: false
20
+ default: "markdown"
21
+ comment:
22
+ description: "Post or update a Trimference comment on pull requests"
23
+ required: false
24
+ default: "false"
25
+ baseline:
26
+ description: "Optional Trimference baseline path, relative to the scanned path"
27
+ required: false
28
+ default: ""
29
+ fail-on:
30
+ description: "Fail after reporting at this severity (none | low | medium | high)"
31
+ required: false
32
+ default: "none"
33
+ changed-since:
34
+ description: "Optional local Git ref; scan only files changed since it"
35
+ required: false
36
+ default: ""
37
+
38
+ runs:
39
+ using: "composite"
40
+ steps:
41
+ - name: Run Trimference
42
+ id: trimference
43
+ shell: bash
44
+ env:
45
+ INPUT_PATH: ${{ inputs.path }}
46
+ INPUT_MIN_SEVERITY: ${{ inputs.min-severity }}
47
+ INPUT_FORMAT: ${{ inputs.format }}
48
+ INPUT_BASELINE: ${{ inputs.baseline }}
49
+ INPUT_CHANGED_SINCE: ${{ inputs.changed-since }}
50
+ run: |
51
+ REPORT="${RUNNER_TEMP:-.}/trimference-report.md"
52
+
53
+ # Always generate a Markdown report file for the job summary and any PR
54
+ # comment. Markdown reports include every finding.
55
+ EXTRA_ARGS=()
56
+ if [ -n "$INPUT_BASELINE" ]; then
57
+ EXTRA_ARGS+=(--baseline "$INPUT_BASELINE")
58
+ fi
59
+ if [ -n "$INPUT_CHANGED_SINCE" ]; then
60
+ EXTRA_ARGS+=(--changed-since "$INPUT_CHANGED_SINCE")
61
+ fi
62
+
63
+ npx --yes trimference@0.3.0 scan \
64
+ --path "$INPUT_PATH" \
65
+ --min-severity "$INPUT_MIN_SEVERITY" \
66
+ --markdown \
67
+ --output "$REPORT" \
68
+ "${EXTRA_ARGS[@]}"
69
+
70
+ # The CLI skips writing the file when there are no scannable files;
71
+ # fall back to a minimal report so the summary and comment steps work.
72
+ if [ ! -f "$REPORT" ]; then
73
+ printf '# Trimference Report\n\nNo scannable files were found under `%s`.\n' \
74
+ "$INPUT_PATH" > "$REPORT"
75
+ fi
76
+
77
+ # Print the report to the workflow log in the requested display format.
78
+ if [ "$INPUT_FORMAT" = "json" ]; then
79
+ npx --yes trimference@0.3.0 scan \
80
+ --path "$INPUT_PATH" \
81
+ --min-severity "$INPUT_MIN_SEVERITY" \
82
+ --json \
83
+ "${EXTRA_ARGS[@]}" || true
84
+ else
85
+ cat "$REPORT"
86
+ fi
87
+
88
+ # Publish the report to the job summary.
89
+ if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
90
+ cat "$REPORT" >> "$GITHUB_STEP_SUMMARY"
91
+ fi
92
+
93
+ echo "report-path=$REPORT" >> "$GITHUB_OUTPUT"
94
+
95
+ - name: Post or update PR comment
96
+ if: ${{ inputs.comment == 'true' && github.event_name == 'pull_request' }}
97
+ uses: actions/github-script@v7
98
+ env:
99
+ REPORT_PATH: ${{ steps.trimference.outputs.report-path }}
100
+ with:
101
+ github-token: ${{ github.token }}
102
+ script: |
103
+ // Mirrors src/prComment.ts (buildCommentBody). Keep the two in sync.
104
+ const fs = require("fs");
105
+ const MARKER = "<!-- trimference-report -->";
106
+ const LEGACY_MARKER = "<!-- ecolint-ai-report -->";
107
+ const MAX_LENGTH = 60000;
108
+
109
+ const report = fs.readFileSync(process.env.REPORT_PATH, "utf8").trimEnd();
110
+
111
+ let body;
112
+ if (report.length <= MAX_LENGTH) {
113
+ body = `${MARKER}\n${report}\n`;
114
+ } else {
115
+ const slice = report.slice(0, MAX_LENGTH);
116
+ const lastNewline = slice.lastIndexOf("\n");
117
+ const truncated = (lastNewline > 0 ? slice.slice(0, lastNewline) : slice).trimEnd();
118
+ const note =
119
+ "> ⚠️ This report was truncated to fit a PR comment. " +
120
+ "See the full Trimference report in the workflow **job summary**.";
121
+ body = `${MARKER}\n${truncated}\n\n---\n\n${note}\n`;
122
+ }
123
+
124
+ const { owner, repo } = context.repo;
125
+ const issue_number = context.issue.number;
126
+
127
+ const comments = await github.paginate(github.rest.issues.listComments, {
128
+ owner,
129
+ repo,
130
+ issue_number,
131
+ per_page: 100,
132
+ });
133
+ const existing = comments.find(
134
+ (c) => c.body && (c.body.includes(MARKER) || c.body.includes(LEGACY_MARKER)),
135
+ );
136
+
137
+ if (existing) {
138
+ await github.rest.issues.updateComment({
139
+ owner,
140
+ repo,
141
+ comment_id: existing.id,
142
+ body,
143
+ });
144
+ } else {
145
+ await github.rest.issues.createComment({
146
+ owner,
147
+ repo,
148
+ issue_number,
149
+ body,
150
+ });
151
+ }
152
+
153
+ - name: PR comment skipped (not a pull request)
154
+ if: ${{ inputs.comment == 'true' && github.event_name != 'pull_request' }}
155
+ shell: bash
156
+ run: |
157
+ echo "Trimference: PR comments are only available on pull_request events." \
158
+ "The report was written to the job summary instead."
159
+
160
+ - name: Enforce Trimference threshold
161
+ if: ${{ inputs.fail-on != 'none' }}
162
+ shell: bash
163
+ env:
164
+ INPUT_PATH: ${{ inputs.path }}
165
+ INPUT_MIN_SEVERITY: ${{ inputs.min-severity }}
166
+ INPUT_BASELINE: ${{ inputs.baseline }}
167
+ INPUT_FAIL_ON: ${{ inputs.fail-on }}
168
+ INPUT_CHANGED_SINCE: ${{ inputs.changed-since }}
169
+ run: |
170
+ EXTRA_ARGS=()
171
+ if [ -n "$INPUT_BASELINE" ]; then
172
+ EXTRA_ARGS+=(--baseline "$INPUT_BASELINE")
173
+ fi
174
+ if [ -n "$INPUT_CHANGED_SINCE" ]; then
175
+ EXTRA_ARGS+=(--changed-since "$INPUT_CHANGED_SINCE")
176
+ fi
177
+ npx --yes trimference@0.3.0 scan \
178
+ --path "$INPUT_PATH" \
179
+ --min-severity "$INPUT_MIN_SEVERITY" \
180
+ --summary \
181
+ --fail-on "$INPUT_FAIL_ON" \
182
+ "${EXTRA_ARGS[@]}"
@@ -0,0 +1,13 @@
1
+ import type { Finding } from "./types.js";
2
+ export declare const BASELINE_VERSION = 1;
3
+ export type TrimferenceBaseline = {
4
+ version: typeof BASELINE_VERSION;
5
+ fingerprints: string[];
6
+ };
7
+ /** @deprecated Use TrimferenceBaseline. */
8
+ export type EcoLintBaseline = TrimferenceBaseline;
9
+ /** Read and validate a Trimference baseline file. */
10
+ export declare function readBaseline(filePath: string): Promise<string[]>;
11
+ /** Write the current findings as accepted baseline debt. */
12
+ export declare function writeBaseline(filePath: string, findings: Finding[]): Promise<void>;
13
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,OAAO,gBAAgB,CAAC;IACjC,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF,2CAA2C;AAC3C,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAElD,qDAAqD;AACrD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAatE;AAED,4DAA4D;AAC5D,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAMf"}
@@ -0,0 +1,23 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { findingFingerprint } from "./scanner.js";
3
+ export const BASELINE_VERSION = 1;
4
+ /** Read and validate a Trimference baseline file. */
5
+ export async function readBaseline(filePath) {
6
+ const raw = await fs.readFile(filePath, "utf8");
7
+ const parsed = JSON.parse(raw);
8
+ if (parsed.version !== BASELINE_VERSION ||
9
+ !Array.isArray(parsed.fingerprints) ||
10
+ !parsed.fingerprints.every((item) => typeof item === "string")) {
11
+ throw new Error(`Invalid baseline at ${filePath}. Expected version ${BASELINE_VERSION}.`);
12
+ }
13
+ return parsed.fingerprints;
14
+ }
15
+ /** Write the current findings as accepted baseline debt. */
16
+ export async function writeBaseline(filePath, findings) {
17
+ const baseline = {
18
+ version: BASELINE_VERSION,
19
+ fingerprints: [...new Set(findings.map(findingFingerprint))].sort(),
20
+ };
21
+ await fs.writeFile(filePath, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
22
+ }
23
+ //# sourceMappingURL=baseline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAUlC,qDAAqD;AACrD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiC,CAAC;IAC/D,IACE,MAAM,CAAC,OAAO,KAAK,gBAAgB;QACnC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC;QACnC,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,EAC9D,CAAC;QACD,MAAM,IAAI,KAAK,CACb,uBAAuB,QAAQ,sBAAsB,gBAAgB,GAAG,CACzE,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,YAAY,CAAC;AAC7B,CAAC;AAED,4DAA4D;AAC5D,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAgB,EAChB,QAAmB;IAEnB,MAAM,QAAQ,GAAwB;QACpC,OAAO,EAAE,gBAAgB;QACzB,YAAY,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;KACpE,CAAC;IACF,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACjF,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ import { promises as fs, readFileSync } from "node:fs";
3
+ import { execFile } from "node:child_process";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { fileURLToPath } from "node:url";
7
+ import { Command } from "commander";
8
+ import pc from "picocolors";
9
+ import { readBaseline, writeBaseline } from "./baseline.js";
10
+ import { SEVERITY_WEIGHT } from "./impact.js";
11
+ import { CONFIG_FILENAME, SAMPLE_CONFIG, loadConfig } from "./config.js";
12
+ import { scan } from "./scanner.js";
13
+ import { renderTerminalReport, } from "./reporters/terminalReporter.js";
14
+ import { renderJsonReport } from "./reporters/jsonReporter.js";
15
+ import { renderSarifReport } from "./reporters/sarifReporter.js";
16
+ import { renderMarkdownReport } from "./reporters/markdownReporter.js";
17
+ /** Read the package version so `--version` always matches package.json. */
18
+ function readVersion() {
19
+ try {
20
+ const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
21
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
22
+ return pkg.version ?? "0.0.0";
23
+ }
24
+ catch {
25
+ return "0.0.0";
26
+ }
27
+ }
28
+ const VALID_SEVERITIES = ["low", "medium", "high"];
29
+ const VALID_PROVIDERS = [
30
+ "openai",
31
+ "anthropic",
32
+ "google",
33
+ "mistral",
34
+ "unknown",
35
+ ];
36
+ const execFileAsync = promisify(execFile);
37
+ function parseMaxFindings(value) {
38
+ const n = Number(value);
39
+ if (!Number.isInteger(n) || n < 0) {
40
+ throw new Error(`Invalid --max-findings "${value}". Expected a non-negative integer (0 = show all).`);
41
+ }
42
+ return n;
43
+ }
44
+ function parseSeverity(value) {
45
+ const lower = value.toLowerCase();
46
+ if (!VALID_SEVERITIES.includes(lower)) {
47
+ throw new Error(`Invalid --min-severity "${value}". Expected one of: ${VALID_SEVERITIES.join(", ")}.`);
48
+ }
49
+ return lower;
50
+ }
51
+ function parseFailOn(value) {
52
+ if (value.toLowerCase() === "none")
53
+ return "none";
54
+ return parseSeverity(value);
55
+ }
56
+ function parseProvider(value) {
57
+ const lower = value.toLowerCase();
58
+ if (!VALID_PROVIDERS.includes(lower)) {
59
+ throw new Error(`Invalid --provider "${value}". Expected one of: ${VALID_PROVIDERS.join(", ")}.`);
60
+ }
61
+ return lower;
62
+ }
63
+ function renderFor(format, result, terminalOptions = {}) {
64
+ switch (format) {
65
+ case "json":
66
+ return renderJsonReport(result);
67
+ case "markdown":
68
+ return renderMarkdownReport(result);
69
+ case "sarif":
70
+ return renderSarifReport(result);
71
+ case "terminal":
72
+ return renderTerminalReport(result, terminalOptions);
73
+ }
74
+ }
75
+ async function runScan(options) {
76
+ const targetPath = options.path ?? ".";
77
+ // Fail clearly if the path does not exist.
78
+ const stat = await fs.stat(path.resolve(targetPath)).catch(() => null);
79
+ if (!stat) {
80
+ console.error(pc.red(`Error: path does not exist: ${targetPath}`));
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+ // Load optional config; CLI flags take precedence over config values.
85
+ const config = await loadConfig(targetPath);
86
+ const minSeverity = options.minSeverity
87
+ ? parseSeverity(options.minSeverity)
88
+ : config.minSeverity ?? "low";
89
+ const provider = options.provider
90
+ ? parseProvider(options.provider)
91
+ : config.provider;
92
+ const failOn = parseFailOn(options.failOn ?? config.failOn ?? "none");
93
+ const baselineSetting = options.writeBaseline
94
+ ? undefined
95
+ : options.baseline ?? config.baseline;
96
+ const baselinePath = baselineSetting
97
+ ? path.resolve(stat.isDirectory() ? targetPath : path.dirname(targetPath), baselineSetting)
98
+ : undefined;
99
+ const baselineKeys = baselinePath ? await readBaseline(baselinePath) : undefined;
100
+ const includedPaths = options.changedSince
101
+ ? await changedPaths(targetPath, stat.isDirectory(), options.changedSince)
102
+ : undefined;
103
+ const result = await scan({
104
+ path: targetPath,
105
+ minSeverity,
106
+ provider,
107
+ ignoredRules: config.ignoredRules,
108
+ ignoredPaths: config.ignoredPaths,
109
+ baselineKeys,
110
+ includedPaths,
111
+ });
112
+ if (result.summary.filesScanned === 0) {
113
+ console.error(pc.yellow(options.changedSince
114
+ ? `No changed scannable files found since "${options.changedSince}" under "${targetPath}".`
115
+ : `No scannable files found under "${targetPath}". Supported: .ts, .tsx, .js, .jsx, .mjs, .cjs, .py`));
116
+ // Not an error exit — just nothing to do.
117
+ return;
118
+ }
119
+ if (options.writeBaseline) {
120
+ const target = path.resolve(stat.isDirectory() ? targetPath : path.dirname(targetPath), options.writeBaseline);
121
+ await writeBaseline(target, result.findings);
122
+ console.log(pc.green(`Trimference: wrote baseline to ${target} (${result.findings.length} findings).`));
123
+ return;
124
+ }
125
+ const format = options.json
126
+ ? "json"
127
+ : options.sarif
128
+ ? "sarif"
129
+ : options.markdown
130
+ ? "markdown"
131
+ : "terminal";
132
+ // When writing to a file, default to markdown unless json is explicitly set.
133
+ // File reports (markdown/JSON) always include every finding.
134
+ if (options.output) {
135
+ const fileFormat = options.json
136
+ ? "json"
137
+ : options.sarif
138
+ ? "sarif"
139
+ : "markdown";
140
+ const content = renderFor(fileFormat, result);
141
+ await fs.writeFile(options.output, content, "utf8");
142
+ console.log(pc.green(`Trimference: wrote ${fileFormat} report to ${options.output} ` +
143
+ `(${result.summary.totalFindings} findings).`));
144
+ setFailureExitCode(result, failOn);
145
+ return;
146
+ }
147
+ const terminalOptions = {
148
+ summary: options.summary,
149
+ maxFindings: options.maxFindings
150
+ ? parseMaxFindings(options.maxFindings)
151
+ : undefined,
152
+ };
153
+ console.log(renderFor(format, result, terminalOptions));
154
+ setFailureExitCode(result, failOn);
155
+ }
156
+ async function changedPaths(targetPath, targetIsDirectory, reference) {
157
+ const absoluteTarget = path.resolve(targetPath);
158
+ const gitCwd = targetIsDirectory ? absoluteTarget : path.dirname(absoluteTarget);
159
+ try {
160
+ const { stdout: rootOutput } = await execFileAsync("git", ["-C", gitCwd, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
161
+ const repoRoot = rootOutput.trim();
162
+ const { stdout } = await execFileAsync("git", [
163
+ "-C",
164
+ repoRoot,
165
+ "diff",
166
+ "--name-only",
167
+ "--diff-filter=ACMR",
168
+ `${reference}...HEAD`,
169
+ "--",
170
+ ], { encoding: "utf8" });
171
+ const targetRoot = targetIsDirectory ? absoluteTarget : path.dirname(absoluteTarget);
172
+ return stdout
173
+ .split(/\r?\n/)
174
+ .filter(Boolean)
175
+ .map((relative) => path.resolve(repoRoot, relative))
176
+ .filter((absolute) => targetIsDirectory
177
+ ? !path.relative(targetRoot, absolute).startsWith("..")
178
+ : absolute === absoluteTarget)
179
+ .map((absolute) => path.relative(targetRoot, absolute).split(path.sep).join("/"));
180
+ }
181
+ catch (error) {
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ throw new Error(`Could not determine files changed since "${reference}". ` +
184
+ `Ensure the Git reference is available locally. ${message}`);
185
+ }
186
+ }
187
+ function setFailureExitCode(result, failOn) {
188
+ if (failOn === "none")
189
+ return;
190
+ if (result.findings.some((finding) => SEVERITY_WEIGHT[finding.severity] >= SEVERITY_WEIGHT[failOn])) {
191
+ process.exitCode = 2;
192
+ }
193
+ }
194
+ async function runInit() {
195
+ const target = path.resolve(CONFIG_FILENAME);
196
+ const exists = await fs
197
+ .stat(target)
198
+ .then(() => true)
199
+ .catch(() => false);
200
+ if (exists) {
201
+ console.error(pc.yellow(`Config already exists at ${target}. Not overwriting.`));
202
+ return;
203
+ }
204
+ await fs.writeFile(target, SAMPLE_CONFIG, "utf8");
205
+ console.log(pc.green(`Created ${target}`));
206
+ }
207
+ async function main() {
208
+ const program = new Command();
209
+ program
210
+ .name("trimference")
211
+ .description("Review LLM application code for missing efficiency safeguards.")
212
+ .version(readVersion());
213
+ program
214
+ .command("scan")
215
+ .description("Review a codebase for missing AI efficiency safeguards")
216
+ .option("-p, --path <path>", "Path to scan", ".")
217
+ .option("--json", "Output findings as JSON")
218
+ .option("--markdown", "Output findings as Markdown")
219
+ .option("--sarif", "Output SARIF 2.1.0 for code scanning")
220
+ .option("-o, --output <file>", "Write the report to a file")
221
+ .option("--min-severity <level>", "Minimum severity to report (low | medium | high). Overrides config.")
222
+ .option("--provider <name>", "Provider hint for model suggestions (openai | anthropic | google | mistral). Overrides config.")
223
+ .option("--max-findings <number>", "Max detailed findings to show in the terminal (0 = show all). Default 8.")
224
+ .option("--summary", "Show only the high-level summary (no detailed findings).")
225
+ .option("--baseline <file>", "Hide findings recorded in a baseline file (path is relative to --path).")
226
+ .option("--write-baseline <file>", "Write current findings to a baseline file relative to --path, then exit.")
227
+ .option("--fail-on <level>", "Exit 2 when findings meet this severity (none | low | medium | high).")
228
+ .option("--changed-since <git-ref>", "Scan only files changed since a local Git reference.")
229
+ .action(async (opts) => {
230
+ await runScan(opts);
231
+ });
232
+ program
233
+ .command("init")
234
+ .description(`Create a sample ${CONFIG_FILENAME} config file`)
235
+ .action(async () => {
236
+ await runInit();
237
+ });
238
+ await program.parseAsync(process.argv);
239
+ }
240
+ main().catch((err) => {
241
+ const message = err instanceof Error ? err.message : String(err);
242
+ console.error(pc.red(`Trimference error: ${message}`));
243
+ process.exitCode = 1;
244
+ });
245
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { Severity } from "./types.js";
2
+ import type { Provider } from "./models.js";
3
+ /**
4
+ * Optional on-disk config. The CLI works without it; when present it provides
5
+ * defaults that CLI flags override.
6
+ */
7
+ export type TrimferenceFileConfig = {
8
+ minSeverity?: Severity;
9
+ ignoredRules?: string[];
10
+ ignoredPaths?: string[];
11
+ provider?: Provider;
12
+ baseline?: string;
13
+ failOn?: Severity | "none";
14
+ };
15
+ /** @deprecated Use TrimferenceFileConfig. */
16
+ export type EcoLintFileConfig = TrimferenceFileConfig;
17
+ export declare const SCANNABLE_EXTENSIONS: string[];
18
+ export declare const IGNORE_GLOBS: string[];
19
+ export declare const DEFAULT_CONFIG: Required<TrimferenceFileConfig>;
20
+ export declare const CONFIG_FILENAME = "trimference.config.json";
21
+ export declare const LEGACY_CONFIG_FILENAME = "ecolint.config.json";
22
+ /** Contents written by `trimference init`. */
23
+ export declare const SAMPLE_CONFIG = "{\n \"minSeverity\": \"low\",\n \"ignoredRules\": [],\n \"ignoredPaths\": [],\n \"provider\": \"openai\",\n \"failOn\": \"none\"\n}\n";
24
+ /**
25
+ * Load `trimference.config.json`, searching the scan path first and then the
26
+ * current working directory. The former `ecolint.config.json` name is accepted
27
+ * as a migration fallback. Returns an empty object (not defaults) when no file
28
+ * exists, so callers can tell "unset" from "set to default".
29
+ */
30
+ export declare function loadConfig(scanPath: string): Promise<TrimferenceFileConfig>;
31
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,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,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CAC5B,CAAC;AAEF,6CAA6C;AAC7C,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,CAAC;AAEtD,eAAO,MAAM,oBAAoB,UAQhC,CAAC;AAEF,eAAO,MAAM,YAAY,UAWxB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,qBAAqB,CAO1D,CAAC;AAEF,eAAO,MAAM,eAAe,4BAA4B,CAAC;AACzD,eAAO,MAAM,sBAAsB,wBAAwB,CAAC;AAE5D,8CAA8C;AAC9C,eAAO,MAAM,aAAa,+IAOzB,CAAC;AAWF;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAmBjF"}
package/dist/config.js ADDED
@@ -0,0 +1,127 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ export const SCANNABLE_EXTENSIONS = [
4
+ ".ts",
5
+ ".tsx",
6
+ ".js",
7
+ ".jsx",
8
+ ".mjs",
9
+ ".cjs",
10
+ ".py",
11
+ ];
12
+ export const IGNORE_GLOBS = [
13
+ "**/node_modules/**",
14
+ "**/.git/**",
15
+ "**/dist/**",
16
+ "**/build/**",
17
+ "**/.next/**",
18
+ "**/coverage/**",
19
+ "**/*.lock",
20
+ "**/package-lock.json",
21
+ "**/pnpm-lock.yaml",
22
+ "**/yarn.lock",
23
+ ];
24
+ export const DEFAULT_CONFIG = {
25
+ minSeverity: "low",
26
+ ignoredRules: [],
27
+ ignoredPaths: [],
28
+ provider: "unknown",
29
+ baseline: "",
30
+ failOn: "none",
31
+ };
32
+ export const CONFIG_FILENAME = "trimference.config.json";
33
+ export const LEGACY_CONFIG_FILENAME = "ecolint.config.json";
34
+ /** Contents written by `trimference init`. */
35
+ export const SAMPLE_CONFIG = `{
36
+ "minSeverity": "low",
37
+ "ignoredRules": [],
38
+ "ignoredPaths": [],
39
+ "provider": "openai",
40
+ "failOn": "none"
41
+ }
42
+ `;
43
+ const VALID_SEVERITIES = ["low", "medium", "high"];
44
+ const VALID_PROVIDERS = [
45
+ "openai",
46
+ "anthropic",
47
+ "google",
48
+ "mistral",
49
+ "unknown",
50
+ ];
51
+ /**
52
+ * Load `trimference.config.json`, searching the scan path first and then the
53
+ * current working directory. The former `ecolint.config.json` name is accepted
54
+ * as a migration fallback. Returns an empty object (not defaults) when no file
55
+ * exists, so callers can tell "unset" from "set to default".
56
+ */
57
+ export async function loadConfig(scanPath) {
58
+ const candidates = [...new Set([
59
+ path.resolve(scanPath, CONFIG_FILENAME),
60
+ path.resolve(scanPath, LEGACY_CONFIG_FILENAME),
61
+ path.resolve(process.cwd(), CONFIG_FILENAME),
62
+ path.resolve(process.cwd(), LEGACY_CONFIG_FILENAME),
63
+ ])];
64
+ for (const candidate of candidates) {
65
+ const raw = await fs.readFile(candidate, "utf8").catch(() => null);
66
+ if (raw === null)
67
+ continue;
68
+ try {
69
+ return normalizeConfig(JSON.parse(raw), candidate);
70
+ }
71
+ catch (err) {
72
+ const message = err instanceof Error ? err.message : String(err);
73
+ throw new Error(`Invalid config at ${candidate}: ${message}`);
74
+ }
75
+ }
76
+ return {};
77
+ }
78
+ function normalizeConfig(input, source) {
79
+ if (typeof input !== "object" || input === null) {
80
+ throw new Error("expected a JSON object");
81
+ }
82
+ const obj = input;
83
+ const config = {};
84
+ if (obj.minSeverity !== undefined) {
85
+ if (!VALID_SEVERITIES.includes(obj.minSeverity)) {
86
+ throw new Error(`minSeverity must be one of ${VALID_SEVERITIES.join(", ")}`);
87
+ }
88
+ config.minSeverity = obj.minSeverity;
89
+ }
90
+ if (obj.ignoredRules !== undefined) {
91
+ if (!isStringArray(obj.ignoredRules)) {
92
+ throw new Error("ignoredRules must be an array of strings");
93
+ }
94
+ config.ignoredRules = obj.ignoredRules;
95
+ }
96
+ if (obj.ignoredPaths !== undefined) {
97
+ if (!isStringArray(obj.ignoredPaths)) {
98
+ throw new Error("ignoredPaths must be an array of strings");
99
+ }
100
+ config.ignoredPaths = obj.ignoredPaths;
101
+ }
102
+ if (obj.provider !== undefined) {
103
+ if (!VALID_PROVIDERS.includes(obj.provider)) {
104
+ throw new Error(`provider must be one of ${VALID_PROVIDERS.join(", ")}`);
105
+ }
106
+ config.provider = obj.provider;
107
+ }
108
+ if (obj.baseline !== undefined) {
109
+ if (typeof obj.baseline !== "string") {
110
+ throw new Error("baseline must be a string path");
111
+ }
112
+ config.baseline = obj.baseline;
113
+ }
114
+ if (obj.failOn !== undefined) {
115
+ if (obj.failOn !== "none" &&
116
+ !VALID_SEVERITIES.includes(obj.failOn)) {
117
+ throw new Error(`failOn must be none or one of ${VALID_SEVERITIES.join(", ")}`);
118
+ }
119
+ config.failOn = obj.failOn;
120
+ }
121
+ void source;
122
+ return config;
123
+ }
124
+ function isStringArray(value) {
125
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
126
+ }
127
+ //# sourceMappingURL=config.js.map